Kiashirio kamili cha bare-C I/O

Kiashirio kamili cha bare-C I/O

Utangulizi

Kiteo cha I/O ( thread moja kitanzi cha tukio) ni muundo wa kuandika programu yenye mzigo mkubwa, inayotumiwa katika suluhisho nyingi maarufu:

Katika makala hii, tutaangalia ins na nje ya reactor ya I/O na jinsi inavyofanya kazi, andika utekelezaji chini ya mistari 200 ya msimbo, na ufanye mchakato rahisi wa seva ya HTTP zaidi ya maombi milioni 40/min.

utangulizi

  • Makala yaliandikwa ili kusaidia kuelewa utendakazi wa kinu cha I/O, na kwa hivyo kuelewa hatari wakati wa kuitumia.
  • Ujuzi wa mambo ya msingi unahitajika ili kuelewa kifungu. C lugha na uzoefu fulani katika ukuzaji wa programu za mtandao.
  • Nambari zote zimeandikwa kwa lugha C madhubuti kulingana na (tahadhari: PDF ndefu) kwa kiwango cha C11 kwa Linux na inapatikana kwenye GitHub.

Kwa nini hii ni muhimu?

Kwa umaarufu unaokua wa Mtandao, seva za wavuti zilianza kuhitaji kushughulikia idadi kubwa ya viunganisho wakati huo huo, na kwa hivyo njia mbili zilijaribiwa: kuzuia I / O kwa idadi kubwa ya nyuzi za OS na I/O isiyozuia pamoja na mfumo wa arifa za tukio, pia huitwa "kichaguzi cha mfumo" (epoll/foleni/IOCP/na kadhalika).

Njia ya kwanza ilihusisha kuunda thread mpya ya OS kwa kila muunganisho unaoingia. Hasara yake ni scalability mbaya: mfumo wa uendeshaji utalazimika kutekeleza mengi mabadiliko ya muktadha и simu za mfumo. Ni shughuli za gharama kubwa na zinaweza kusababisha ukosefu wa RAM ya bure na idadi ya kuvutia ya viunganisho.

Toleo lililorekebishwa linaangazia idadi maalum ya nyuzi (dimbwi la nyuzi), na hivyo kuzuia mfumo kuacha kutekeleza, lakini wakati huo huo kuanzisha shida mpya: ikiwa bwawa la nyuzi kwa sasa limezuiwa na shughuli za kusoma kwa muda mrefu, basi soketi zingine ambazo tayari zinaweza kupokea data hazitaweza. fanya hivyo.

Njia ya pili hutumia mfumo wa arifa za tukio (kichaguzi cha mfumo) kilichotolewa na OS. Makala haya yanajadili aina ya kawaida ya kiteuzi cha mfumo, kulingana na arifa (matukio, arifa) kuhusu utayari wa shughuli za I/O, badala ya arifa kuhusu kukamilika kwao. Mfano rahisi wa matumizi yake unaweza kuwakilishwa na mchoro wa block ufuatao:

Kiashirio kamili cha bare-C I/O

Tofauti kati ya mbinu hizi ni kama ifuatavyo:

  • Kuzuia shughuli za I/O kusimamisha mtiririko wa mtumiaji mpakampaka OS iko sawa defragments zinazoingia Pakiti za IP kwa mtiririko (TCP, kupokea data) au hakutakuwa na nafasi ya kutosha katika vihifadhi vya uandishi wa ndani kwa ajili ya kutuma kupitia NIC (kutuma data).
  • Kiteuzi cha mfumo baada ya muda inaarifu programu ambayo OS tayari pakiti za IP zilizotenganishwa (TCP, mapokezi ya data) au nafasi ya kutosha katika bafa za uandishi wa ndani tayari inapatikana (kutuma data).

Ili kuhitimisha, kuhifadhi uzi wa OS kwa kila I/O ni upotezaji wa nguvu ya kompyuta, kwa sababu kwa ukweli, nyuzi hazifanyi kazi muhimu (kwa hivyo neno. "kukatiza programu") Kiteuzi cha mfumo hutatua tatizo hili, na kuruhusu programu ya mtumiaji kutumia rasilimali za CPU kiuchumi zaidi.

Muundo wa kinu cha I/O

Reactor ya I/O hufanya kama safu kati ya kiteuzi cha mfumo na msimbo wa mtumiaji. Kanuni ya operesheni yake inaelezewa na mchoro wa block ufuatao:

Kiashirio kamili cha bare-C I/O

  • Acha nikukumbushe kwamba tukio ni arifa kwamba tundu fulani linaweza kufanya operesheni isiyo ya kuzuia I/O.
  • Kidhibiti tukio ni chaguo la kukokotoa linaloitwa na kiboreshaji cha I/O tukio linapopokelewa, ambalo kisha hufanya operesheni isiyozuia ya I/O.

Ni muhimu kutambua kwamba kiboreshaji cha I/O kwa ufafanuzi ni cha nyuzi moja, lakini hakuna kitu kinachozuia wazo hilo kutumiwa katika mazingira yenye nyuzi nyingi kwa uwiano wa nyuzi 1: kinu 1, na hivyo kuchakata cores zote za CPU.

Utekelezaji

Tutaweka kiolesura cha umma kwenye faili reactor.h, na utekelezaji - ndani reactor.c. reactor.h itakuwa na matangazo yafuatayo:

Onyesha matamko katika kinu.h

typedef struct reactor Reactor;

/*
 * Указатель на функцию, которая будет вызываться I/O реактором при поступлении
 * события от системного селектора.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Возвращает `NULL` в случае ошибки, не-`NULL` указатель на `Reactor` в
 * противном случае.
 */
Reactor *reactor_new(void);

/*
 * Освобождает системный селектор, все зарегистрированные сокеты в данный момент
 * времени и сам I/O реактор.
 *
 * Следующие функции возвращают -1 в случае ошибки, 0 в случае успеха.
 */
int reactor_destroy(Reactor *reactor);

int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
                     Callback callback, void *callback_arg);
int reactor_deregister(const Reactor *reactor, int fd);
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
                       Callback callback, void *callback_arg);

/*
 * Запускает цикл событий с тайм-аутом `timeout`.
 *
 * Эта функция передаст управление вызывающему коду если отведённое время вышло
 * или/и при отсутствии зарегистрированных сокетов.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

Muundo wa kinu cha I/O kinajumuisha kielezi cha faili kiteuzi epoll и meza za hashi GHashTable, ambayo huweka kila tundu CallbackData (muundo wa kidhibiti tukio na hoja ya mtumiaji kwake).

Onyesha Reactor na CallbackData

struct reactor {
    int epoll_fd;
    GHashTable *table; // (int, CallbackData)
};

typedef struct {
    Callback callback;
    void *arg;
} CallbackData;

Tafadhali kumbuka kuwa tumewezesha uwezo wa kushughulikia aina isiyo kamili kulingana na index. KATIKA reactor.h tunatangaza muundo reactor, na ndani reactor.c tunaifafanua, na hivyo kumzuia mtumiaji kubadilisha sehemu zake kwa uwazi. Hii ni moja ya mifumo kuficha data, ambayo inalingana kwa ufupi katika semantiki C.

Kazi reactor_register, reactor_deregister и reactor_reregister sasisha orodha ya soketi zinazovutia na vidhibiti vya matukio sambamba katika kiteuzi cha mfumo na jedwali la hashi.

Onyesha vipengele vya usajili

#define REACTOR_CTL(reactor, op, fd, interest)                                 
    if (epoll_ctl(reactor->epoll_fd, op, fd,                                   
                  &(struct epoll_event){.events = interest,                    
                                        .data = {.fd = fd}}) == -1) {          
        perror("epoll_ctl");                                                   
        return -1;                                                             
    }

int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
                     Callback callback, void *callback_arg) {
    REACTOR_CTL(reactor, EPOLL_CTL_ADD, fd, interest)
    g_hash_table_insert(reactor->table, int_in_heap(fd),
                        callback_data_new(callback, callback_arg));
    return 0;
}

int reactor_deregister(const Reactor *reactor, int fd) {
    REACTOR_CTL(reactor, EPOLL_CTL_DEL, fd, 0)
    g_hash_table_remove(reactor->table, &fd);
    return 0;
}

int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
                       Callback callback, void *callback_arg) {
    REACTOR_CTL(reactor, EPOLL_CTL_MOD, fd, interest)
    g_hash_table_insert(reactor->table, int_in_heap(fd),
                        callback_data_new(callback, callback_arg));
    return 0;
}

Baada ya kitendawili cha I/O kukatiza tukio na kifafanuzi fd, huita kidhibiti cha tukio kinacholingana, ambacho hupita fd, mask kidogo matukio yanayotokana na kielekezi cha mtumiaji void.

Onyesha kitendakazi cha reactor_run()

int reactor_run(const Reactor *reactor, time_t timeout) {
    int result;
    struct epoll_event *events;
    if ((events = calloc(MAX_EVENTS, sizeof(*events))) == NULL)
        abort();

    time_t start = time(NULL);

    while (true) {
        time_t passed = time(NULL) - start;
        int nfds =
            epoll_wait(reactor->epoll_fd, events, MAX_EVENTS, timeout - passed);

        switch (nfds) {
        // Ошибка
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Время вышло
        case 0:
            result = 0;
            goto cleanup;
        // Успешная операция
        default:
            // Вызвать обработчиков событий
            for (int i = 0; i < nfds; i++) {
                int fd = events[i].data.fd;

                CallbackData *callback =
                    g_hash_table_lookup(reactor->table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

Kwa muhtasari, msururu wa simu za chaguo za kukokotoa katika msimbo wa mtumiaji utachukua fomu ifuatayo:

Kiashirio kamili cha bare-C I/O

Seva yenye thread moja

Ili kujaribu kiboreshaji cha I/O chini ya upakiaji wa juu, tutaandika seva rahisi ya wavuti ya HTTP ambayo hujibu ombi lolote kwa picha.

Rejeleo la haraka la itifaki ya HTTP

HTTP - hii ni itifaki kiwango cha maombi, kimsingi hutumika kwa mwingiliano wa kivinjari-seva.

HTTP inaweza kutumika kwa urahisi usafiri itifaki TCP, kutuma na kupokea ujumbe katika umbizo lililobainishwa vipimo.

Umbizo la Ombi

<КОМАНДА> <URI> <ВЕРСИЯ HTTP>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • CRLF ni mlolongo wa wahusika wawili: r и n, ikitenganisha mstari wa kwanza wa ombi, vichwa na data.
  • <КОМАНДА> - moja ya CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Kivinjari kitatuma amri kwa seva yetu GET, ikimaanisha "Nitumie yaliyomo kwenye faili."
  • <URI> - kitambulisho cha rasilimali sare. Kwa mfano, ikiwa URI = /index.html, kisha mteja anaomba ukurasa kuu wa tovuti.
  • <ВЕРСИЯ HTTP> - toleo la itifaki ya HTTP katika umbizo HTTP/X.Y. Toleo linalotumiwa zaidi leo ni HTTP/1.1.
  • <ЗАГОЛОВОК N> ni jozi ya thamani-msingi katika umbizo <КЛЮЧ>: <ЗНАЧЕНИЕ>, imetumwa kwa seva kwa uchambuzi zaidi.
  • <ДАННЫЕ> — data inayohitajika na seva kufanya operesheni. Mara nyingi ni rahisi JSON au muundo mwingine wowote.

Umbizo la Majibu

<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>

  • <КОД СТАТУСА> ni nambari inayowakilisha matokeo ya operesheni. Seva yetu itarudisha hali 200 kila wakati (operesheni iliyofanikiwa).
  • <ОПИСАНИЕ СТАТУСА> — uwakilishi wa mfuatano wa msimbo wa hali. Kwa msimbo wa hali 200 hii ni OK.
  • <ЗАГОЛОВОК N> - kichwa cha umbizo sawa na katika ombi. Tutarudisha vyeo Content-Length (saizi ya faili) na Content-Type: text/html (rejesha aina ya data).
  • <ДАННЫЕ> - data iliyoombwa na mtumiaji. Kwa upande wetu, hii ndio njia ya picha ndani HTML.

file http_server.c (seva yenye nyuzi moja) inajumuisha faili common.h, ambayo ina prototypes zifuatazo za kazi:

Onyesha prototypes za utendakazi zinazofanana.h

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять новое соединение.
 */
static void on_accept(void *arg, int fd, uint32_t events);

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов отправить HTTP ответ.
 */
static void on_send(void *arg, int fd, uint32_t events);

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять часть HTTP запроса.
 */
static void on_recv(void *arg, int fd, uint32_t events);

/*
 * Переводит входящее соединение в неблокирующий режим.
 */
static void set_nonblocking(int fd);

/*
 * Печатает переданные аргументы в stderr и выходит из процесса с
 * кодом `EXIT_FAILURE`.
 */
static noreturn void fail(const char *format, ...);

/*
 * Возвращает файловый дескриптор сокета, способного принимать новые
 * TCP соединения.
 */
static int new_server(bool reuse_port);

Macro inayofanya kazi pia imeelezewa SAFE_CALL() na kazi imefafanuliwa fail(). Jumla inalinganisha thamani ya usemi na kosa, na ikiwa hali ni kweli, huita chaguo la kukokotoa fail():

#define SAFE_CALL(call, error)                                                 
    do {                                                                       
        if ((call) == error) {                                                   
            fail("%s", #call);                                                 
        }                                                                      
    } while (false)

Kazi fail() huchapisha hoja zilizopitishwa kwa terminal (kama printf()) na kusitisha programu na msimbo EXIT_FAILURE:

static noreturn void fail(const char *format, ...) {
    va_list args;
    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);
    fprintf(stderr, ": %sn", strerror(errno));
    exit(EXIT_FAILURE);
}

Kazi new_server() inarudisha maelezo ya faili ya soketi ya "seva" iliyoundwa na simu za mfumo socket(), bind() и listen() na yenye uwezo wa kukubali miunganisho inayoingia katika hali isiyozuia.

Onyesha kitendakazi kipya_server().

static int new_server(bool reuse_port) {
    int fd;
    SAFE_CALL((fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)),
              -1);

    if (reuse_port) {
        SAFE_CALL(
            setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)),
            -1);
    }

    struct sockaddr_in addr = {.sin_family = AF_INET,
                               .sin_port = htons(SERVER_PORT),
                               .sin_addr = {.s_addr = inet_addr(SERVER_IPV4)},
                               .sin_zero = {0}};

    SAFE_CALL(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), -1);
    SAFE_CALL(listen(fd, SERVER_BACKLOG), -1);
    return fd;
}

  • Kumbuka kuwa tundu limeundwa awali katika hali ya kutozuia kwa kutumia bendera SOCK_NONBLOCKili katika utendaji on_accept() (soma zaidi) simu ya mfumo accept() haikusimamisha utekelezaji wa thread.
  • Kama reuse_port ni sawa na true, basi kazi hii itasanidi tundu na chaguo SO_REUSEPORT kupitia setsockopt()kutumia mlango huo huo katika mazingira yenye nyuzi nyingi (angalia sehemu ya "Seva yenye nyuzi nyingi").

Kidhibiti Tukio on_accept() inaitwa baada ya OS kutoa tukio EPOLLIN, katika kesi hii ina maana kwamba muunganisho mpya unaweza kukubaliwa. on_accept() inakubali muunganisho mpya, kuibadilisha hadi hali isiyozuia na kusajili na kidhibiti tukio on_recv() katika kinu cha I/O.

Onyesha on_accept() kitendakazi

static void on_accept(void *arg, int fd, uint32_t events) {
    int incoming_conn;
    SAFE_CALL((incoming_conn = accept(fd, NULL, NULL)), -1);
    set_nonblocking(incoming_conn);
    SAFE_CALL(reactor_register(reactor, incoming_conn, EPOLLIN, on_recv,
                               request_buffer_new()),
              -1);
}

Kidhibiti Tukio on_recv() inaitwa baada ya OS kutoa tukio EPOLLIN, katika kesi hii inamaanisha kuwa muunganisho umesajiliwa on_accept(), tayari kupokea data.

on_recv() inasoma data kutoka kwa unganisho hadi ombi la HTTP limepokelewa kabisa, kisha husajili kidhibiti on_send() kutuma jibu la HTTP. Ikiwa mteja atavunja uunganisho, tundu limefutwa na kufungwa kwa kutumia close().

Onyesha chaguo za kukokotoa kwenye_recv()

static void on_recv(void *arg, int fd, uint32_t events) {
    RequestBuffer *buffer = arg;

    // Принимаем входные данные до тех пор, что recv возвратит 0 или ошибку
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Клиент оборвал соединение
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read вернул ошибку, отличную от ошибки, при которой вызов заблокирует
    // поток
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Получен полный HTTP запрос от клиента. Теперь регистрируем обработчика
    // событий для отправки данных
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

Kidhibiti Tukio on_send() inaitwa baada ya OS kutoa tukio EPOLLOUT, ikimaanisha kuwa muunganisho umesajiliwa on_recv(), tayari kutuma data. Chaguo hili la kukokotoa hutuma jibu la HTTP lililo na HTML na picha kwa mteja na kisha kubadilisha kidhibiti cha tukio kuwa on_recv().

Onyesha on_send() kitendakazi

static void on_send(void *arg, int fd, uint32_t events) {
    const char *content = "<img "
                          "src="https://habrastorage.org/webt/oh/wl/23/"
                          "ohwl23va3b-dioerobq_mbx4xaw.jpeg">";
    char response[1024];
    sprintf(response,
            "HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "
            "text/html" DOUBLE_CRLF "%s",
            strlen(content), content);

    SAFE_CALL(send(fd, response, strlen(response), 0), -1);
    SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);
}

Na hatimaye, katika faili http_server.c, katika utendaji main() tunaunda kinu cha I/O kwa kutumia reactor_new(), unda tundu la seva na uiandikishe, anza reactor kutumia reactor_run() kwa dakika moja, na kisha tunatoa rasilimali na kuondoka kwenye programu.

Onyesha http_server.c

#include "reactor.h"

static Reactor *reactor;

#include "common.h"

int main(void) {
    SAFE_CALL((reactor = reactor_new()), NULL);
    SAFE_CALL(
        reactor_register(reactor, new_server(false), EPOLLIN, on_accept, NULL),
        -1);
    SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
    SAFE_CALL(reactor_destroy(reactor), -1);
}

Wacha tuangalie ikiwa kila kitu kinafanya kazi kama inavyotarajiwa. Kukusanya (chmod a+x compile.sh && ./compile.sh kwenye mzizi wa mradi) na uzindua seva iliyoandikwa kibinafsi, fungua http://127.0.0.1:18470 kwenye kivinjari na uone tulichotarajia:

Kiashirio kamili cha bare-C I/O

Kipimo cha utendaji

Onyesha vipimo vya gari langu

$ screenfetch
 MMMMMMMMMMMMMMMMMMMMMMMMMmds+.        OS: Mint 19.1 tessa
 MMm----::-://////////////oymNMd+`     Kernel: x86_64 Linux 4.15.0-20-generic
 MMd      /++                -sNMd:    Uptime: 2h 34m
 MMNso/`  dMM    `.::-. .-::.` .hMN:   Packages: 2217
 ddddMMh  dMM   :hNMNMNhNMNMNh: `NMm   Shell: bash 4.4.20
     NMm  dMM  .NMN/-+MMM+-/NMN` dMM   Resolution: 1920x1080
     NMm  dMM  -MMm  `MMM   dMM. dMM   DE: Cinnamon 4.0.10
     NMm  dMM  -MMm  `MMM   dMM. dMM   WM: Muffin
     NMm  dMM  .mmd  `mmm   yMM. dMM   WM Theme: Mint-Y-Dark (Mint-Y)
     NMm  dMM`  ..`   ...   ydm. dMM   GTK Theme: Mint-Y [GTK2/3]
     hMM- +MMd/-------...-:sdds  dMM   Icon Theme: Mint-Y
     -NMm- :hNMNNNmdddddddddy/`  dMM   Font: Noto Sans 9
      -dMNs-``-::::-------.``    dMM   CPU: Intel Core i7-6700 @ 8x 4GHz [52.0°C]
       `/dMNmy+/:-------------:/yMMM   GPU: NV136
          ./ydNMMMMMMMMMMMMMMMMMMMMM   RAM: 2544MiB / 7926MiB
             .MMMMMMMMMMMMMMMMMMM

Wacha tupime utendakazi wa seva yenye nyuzi moja. Hebu tufungue vituo viwili: katika moja tutaendesha ./http_server, katika tofauti - kasoro. Baada ya dakika moja, takwimu zifuatazo zitaonyeshwa kwenye terminal ya pili:

$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   493.52us   76.70us  17.31ms   89.57%
    Req/Sec    24.37k     1.81k   29.34k    68.13%
  11657769 requests in 1.00m, 1.60GB read
Requests/sec: 193974.70
Transfer/sec:     27.19MB

Seva yetu yenye nyuzi moja iliweza kuchakata zaidi ya maombi milioni 11 kwa dakika yanayotokana na miunganisho 100. Sio matokeo mabaya, lakini inaweza kuboreshwa?

Seva yenye nyuzi nyingi

Kama ilivyoelezwa hapo juu, kiboreshaji cha I/O kinaweza kuundwa kwa nyuzi tofauti, na hivyo kutumia cores zote za CPU. Wacha tuweke mbinu hii katika vitendo:

Onyesha http_server_multithreaded.c

#include "reactor.h"

static Reactor *reactor;
#pragma omp threadprivate(reactor)

#include "common.h"

int main(void) {
#pragma omp parallel
    {
        SAFE_CALL((reactor = reactor_new()), NULL);
        SAFE_CALL(reactor_register(reactor, new_server(true), EPOLLIN,
                                   on_accept, NULL),
                  -1);
        SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
        SAFE_CALL(reactor_destroy(reactor), -1);
    }
}

Sasa kila thread anamiliki yake kinu:

static Reactor *reactor;
#pragma omp threadprivate(reactor)

Tafadhali kumbuka kuwa hoja ya kukokotoa new_server() neema true. Hii inamaanisha kuwa tunapeana chaguo kwenye tundu la seva SO_REUSEPORTkuitumia katika mazingira yenye nyuzi nyingi. Unaweza kusoma zaidi hapa.

Mbio za pili

Sasa hebu tupime utendaji wa seva yenye nyuzi nyingi:

$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.14ms    2.53ms  40.73ms   89.98%
    Req/Sec    79.98k    18.07k  154.64k    78.65%
  38208400 requests in 1.00m, 5.23GB read
Requests/sec: 635876.41
Transfer/sec:     89.14MB

Idadi ya maombi yaliyochakatwa katika dakika 1 iliongezeka kwa ~ mara 3.28! Lakini tulikuwa tu ~ milioni XNUMX tu pungufu ya nambari ya pande zote, kwa hivyo hebu tujaribu kurekebisha hilo.

Kwanza tuangalie takwimu zinazozalishwa perf:

$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded

 Performance counter stats for './http_server_multithreaded':

     242446,314933      task-clock (msec)         #    4,000 CPUs utilized          
         1 813 074      context-switches          #    0,007 M/sec                  
             4 689      cpu-migrations            #    0,019 K/sec                  
               254      page-faults               #    0,001 K/sec                  
   895 324 830 170      cycles                    #    3,693 GHz                    
   621 378 066 808      instructions              #    0,69  insn per cycle         
   119 926 709 370      branches                  #  494,653 M/sec                  
     3 227 095 669      branch-misses             #    2,69% of all branches        
           808 664      cache-misses                                                

      60,604330670 seconds time elapsed

Kwa kutumia CPU Affinity, mkusanyiko na -march=native, PBL, ongezeko la idadi ya vibao akiba, Ongeza MAX_EVENTS na kutumia EPOLLET haikutoa ongezeko kubwa la utendaji. Lakini nini kinatokea ikiwa unaongeza idadi ya viunganisho vya wakati mmoja?

Takwimu za miunganisho 352 kwa wakati mmoja:

$ wrk -c352 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 352 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.12ms    3.79ms  68.23ms   87.49%
    Req/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 requests in 1.00m, 5.48GB read
Requests/sec: 665789.26
Transfer/sec:     93.34MB

Matokeo yaliyotarajiwa yalipatikana, na kwa hiyo grafu ya kuvutia inayoonyesha utegemezi wa idadi ya maombi yaliyochakatwa katika dakika 1 kwa idadi ya miunganisho:

Kiashirio kamili cha bare-C I/O

Tunaona kwamba baada ya miunganisho ya mia kadhaa, idadi ya maombi yaliyochakatwa kwa seva zote mbili hushuka sana (katika toleo la nyuzi nyingi hii inaonekana zaidi). Je! hii inahusiana na utekelezaji wa rafu ya Linux TCP/IP? Jisikie huru kuandika mawazo yako kuhusu tabia hii ya grafu na uboreshaji kwa chaguo zenye nyuzi nyingi na zenye nyuzi moja kwenye maoni.

Kama alibainisha katika maoni, jaribio hili la utendakazi halionyeshi tabia ya kiboreshaji cha I/O chini ya mizigo halisi, kwa sababu karibu kila mara seva huingiliana na hifadhidata, hutoa magogo, hutumia kriptografia na TLS nk, kama matokeo ambayo mzigo unakuwa sio sare (nguvu). Majaribio pamoja na vipengele vya wahusika wengine yatafanywa katika makala kuhusu prokta ya I/O.

Hasara za kinu cha I/O

Unahitaji kuelewa kuwa kiboreshaji cha I/O sio bila shida zake, ambazo ni:

  • Kutumia kiboreshaji cha I/O katika mazingira yenye nyuzi nyingi ni ngumu zaidi, kwa sababu itabidi usimamie mtiririko kwa mikono.
  • Mazoezi yanaonyesha kuwa katika hali nyingi mzigo sio sare, ambayo inaweza kusababisha ukataji wa nyuzi moja wakati mwingine yuko busy na kazi.
  • Ikiwa kidhibiti tukio kimoja kitazuia mazungumzo, kiteuzi cha mfumo chenyewe pia kitazuia, jambo ambalo linaweza kusababisha hitilafu ambazo ni ngumu kupata.

Hutatua matatizo haya Mwigizaji wa I/O, ambayo mara nyingi huwa na mpangilio ambao husambaza sawasawa mzigo kwenye dimbwi la nyuzi, na pia ina API inayofaa zaidi. Tutazungumza juu yake baadaye, katika nakala yangu nyingine.

Hitimisho

Hapa ndipo safari yetu kutoka kwa nadharia moja kwa moja hadi kwenye bomba la wasifu imefikia mwisho.

Haupaswi kukaa juu ya hili, kwa sababu kuna njia nyingine nyingi za kuvutia za kuandika programu za mtandao na viwango tofauti vya urahisi na kasi. Kuvutia, kwa maoni yangu, viungo vinatolewa hapa chini.

Tutaonana hivi karibuni!

Miradi ya kuvutia

Nisome nini kingine?

Chanzo: mapenzi.com

Kuongeza maoni