Täisfunktsionaalne tühi-C I/O reaktor

Täisfunktsionaalne tühi-C I/O reaktor

Sissejuhatus

I/O reaktor (ühe keermega sündmuse silmus) on suure koormusega tarkvara kirjutamise muster, mida kasutatakse paljudes populaarsetes lahendustes:

Selles artiklis vaatleme I/O reaktori läbi ja lõhki ning selle toimimist, kirjutame juurutusse vähem kui 200 koodireaga ja teeme lihtsa HTTP-serveri protsessi üle 40 miljoni päringu minutis.

Eessõna

  • Artikkel on kirjutatud selleks, et aidata mõista I/O reaktori toimimist ja seega mõista selle kasutamisel tekkivaid riske.
  • Artikli mõistmiseks on vaja põhiteadmisi. C keel ja mõningane kogemus võrgurakenduste arendamisel.
  • Kogu kood on kirjutatud C-keeles rangelt vastavalt (Ettevaatust: pikk PDF) standardile C11 Linuxi jaoks ja saadaval GitHub.

Miks seda teha?

Interneti populaarsuse kasvuga hakkasid veebiserverid korraga käsitlema suurt hulka ühendusi ja seetõttu prooviti kahte lähenemisviisi: I/O blokeerimine suurel hulgal OS-i lõimedel ja mitteblokeeriv I/O kombinatsioonis sündmuste teavitussüsteem, mida nimetatakse ka "süsteemi valijaks" (epoll/kqueue/IOCP/jne).

Esimene lähenemisviis hõlmas uue OS-i lõime loomist iga sissetuleva ühenduse jaoks. Selle puuduseks on halb skaleeritavus: operatsioonisüsteem peab rakendama paljusid konteksti üleminekud и süsteemikõned. Need on kallid toimingud ja võivad põhjustada vaba RAM-i puudumise ja muljetavaldava arvu ühenduste.

Muudetud versioon tõstab esile fikseeritud arv niidid (lõimekogum), takistades sellega süsteemi täitmist katkestamast, kuid samal ajal tekitades uue probleemi: kui lõimede kogum on praegu pikkade lugemistoimingute tõttu blokeeritud, siis teised pesad, mis juba on võimelised andmeid vastu võtma, ei saa seda teha. tee nii.

Teine lähenemisviis kasutab sündmuste teavitussüsteem (süsteemi valija), mida pakub OS. Selles artiklis käsitletakse kõige levinumat süsteemivalija tüüpi, mis põhinevad hoiatustel (sündmused, märguanded) I/O toimingute valmisoleku kohta, mitte teateid nende lõpetamise kohta. Selle kasutamise lihtsustatud näidet saab esitada järgmise plokkskeemi abil:

Täisfunktsionaalne tühi-C I/O reaktor

Nende lähenemisviiside erinevus on järgmine:

  • I/O toimingute blokeerimine riputama kasutajavoog kunikuni OS on õige defragmendid sissetulevad IP-paketid baitide voogu (TCP, andmete vastuvõtmine) vastasel juhul ei ole sisemistes kirjutuspuhvrites piisavalt ruumi järgnevaks saatmiseks NIC (andmete saatmine).
  • Süsteemi valija üle aja teatab programmile, et OS juba defragmenteeritud IP-paketid (TCP, andmete vastuvõtt) või piisavalt ruumi sisemistes kirjutuspuhvrites juba saadaval (andmete saatmine).

Kokkuvõtteks võib öelda, et iga sisendi/väljundi jaoks OS-i lõime reserveerimine on arvutusvõimsuse raiskamine, sest tegelikkuses ei tee lõimed kasulikku tööd (sellest ka termin "tarkvara katkestus"). Süsteemi valija lahendab selle probleemi, võimaldades kasutajaprogrammil kasutada protsessori ressursse palju säästlikumalt.

I/O reaktori mudel

I/O reaktor toimib kihina süsteemivalija ja kasutajakoodi vahel. Selle tööpõhimõtet kirjeldab järgmine plokkskeem:

Täisfunktsionaalne tühi-C I/O reaktor

  • Tuletan meelde, et sündmus on teade, et teatud pesa on võimeline sooritama mitteblokeeriva I/O toimingu.
  • Sündmuste töötleja on funktsioon, mille kutsub välja I/O reaktor sündmuse vastuvõtmisel, mis seejärel teostab mitteblokeeriva I/O toimingu.

Oluline on märkida, et I/O reaktor on definitsiooni järgi ühe keermega, kuid miski ei takista seda kontseptsiooni kasutamast mitme keermega keskkonnas vahekorras 1 lõime: 1 reaktor, mis taaskasutab kõik protsessori südamikud.

Реализация

Asetame avaliku liidese faili reactor.h, ja rakendamine - sisse reactor.c. reactor.h koosneb järgmistest teadaannetest:

Näita deklaratsioone reaktoris.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);

I/O reaktori struktuur koosneb faili deskriptor valija epoll и räsi tabelid GHashTable, mis kaardistab iga pesa CallbackData (sündmuse töötleja struktuur ja kasutaja argument selle jaoks).

Näita reaktorit ja tagasihelistamisandmeid

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

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

Pange tähele, et oleme lubanud käsitsemise võimaluse mittetäielik tüüp indeksi järgi. IN reactor.h deklareerime struktuuri reactorja sisse reactor.c me määratleme selle, takistades sellega kasutajal oma välju selgesõnaliselt muutmast. See on üks mustritest andmete peitmine, mis sobib kokkuvõtlikult C-semantikasse.

Funktsioonid reactor_register, reactor_deregister и reactor_reregister värskendage huvipakkuvate pistikupesade loendit ja vastavaid sündmuste käitlejaid süsteemivalijas ja räsitabelis.

Kuva registreerimisfunktsioonid

#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;
}

Pärast seda, kui I/O reaktor on sündmuse deskriptoriga kinni püüdnud fd, kutsub see vastava sündmuste käitleja, millele see edasi läheb fd, natuke mask loodud sündmused ja kasutaja osuti void.

Näita funktsiooni 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;
}

Kokkuvõtteks võib öelda, et kasutajakoodi funktsioonikutsete ahel on järgmisel kujul:

Täisfunktsionaalne tühi-C I/O reaktor

Ühe keermega server

I/O reaktori suure koormuse all testimiseks kirjutame lihtsa HTTP veebiserveri, mis vastab igale päringule pildiga.

Kiire viide HTTP-protokollile

HTTP - see on protokoll rakenduse tase, mida kasutatakse peamiselt serveri ja brauseri suhtluseks.

HTTP-d saab hõlpsasti kasutada transport protokoll TCP, sõnumite saatmine ja vastuvõtmine määratud vormingus spetsifikatsioon.

Taotluse vorming

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

  • CRLF on kahe märgi jada: r и n, eraldades päringu esimese rea, päised ja andmed.
  • <КОМАНДА> - üks neist CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Brauser saadab meie serverile käsu GET, mis tähendab "Saada mulle faili sisu".
  • <URI> - ühtne ressursi identifikaator. Näiteks kui URI = /index.html, siis taotleb klient saidi avalehte.
  • <ВЕРСИЯ HTTP> — HTTP-protokolli versioon vormingus HTTP/X.Y. Tänapäeval on kõige sagedamini kasutatav versioon HTTP/1.1.
  • <ЗАГОЛОВОК N> on vormingus võtme-väärtuse paar <КЛЮЧ>: <ЗНАЧЕНИЕ>, saadetakse serverisse edasiseks analüüsiks.
  • <ДАННЫЕ> — andmed, mida server toimingu sooritamiseks vajab. Sageli on see lihtne JSON või mis tahes muus vormingus.

Vastuse vorming

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

  • <КОД СТАТУСА> on arv, mis tähistab operatsiooni tulemust. Meie server tagastab alati oleku 200 (edukas toiming).
  • <ОПИСАНИЕ СТАТУСА> — olekukoodi stringesitus. Olekukoodi 200 puhul on see OK.
  • <ЗАГОЛОВОК N> — päringuga samas vormingus päis. Tagastame pealkirjad Content-Length (faili suurus) ja Content-Type: text/html (tagastatav andmetüüp).
  • <ДАННЫЕ> — kasutaja küsitud andmed. Meie puhul on see tee pildini HTML.

fail http_server.c (ühe keermega server) sisaldab faili common.h, mis sisaldab järgmisi funktsioonide prototüüpe:

Näita funktsioonide prototüüpe ühiselt.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);

Samuti kirjeldatakse funktsionaalset makrot SAFE_CALL() ja funktsioon on määratletud fail(). Makro võrdleb avaldise väärtust veaga ja kui tingimus on tõene, kutsub funktsiooni välja fail():

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

Funktsioon fail() prindib terminali edastatud argumendid (nt printf()) ja lõpetab programmi koodiga 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);
}

Funktsioon new_server() tagastab süsteemikutsete loodud "serveri" pesa failideskriptori socket(), bind() и listen() ja on võimeline vastu võtma sissetulevaid ühendusi mitteblokeerivas režiimis.

Näita funktsiooni new_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;
}

  • Pange tähele, et pesa luuakse algselt mitteblokeerivas režiimis lipu abil SOCK_NONBLOCKnii et funktsioonis on_accept() (loe edasi) süsteemikõne accept() ei peatanud lõime täitmist.
  • kui reuse_port on true, siis konfigureerib see funktsioon pistikupesa valikuga SO_REUSEPORT läbi setsockopt()sama pordi kasutamiseks mitme lõimega keskkonnas (vt jaotist "Mitme lõimega server").

Sündmuste haldaja on_accept() kutsutakse pärast OS-i sündmuse genereerimist EPOLLIN, mis antud juhul tähendab, et uue ühendusega saab nõustuda. on_accept() võtab vastu uue ühenduse, lülitab selle mitteblokeerivasse režiimi ja registreerub sündmuste käitlejas on_recv() I/O reaktoris.

Näita funktsiooni on_accept().

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);
}

Sündmuste haldaja on_recv() kutsutakse pärast OS-i sündmuse genereerimist EPOLLIN, mis antud juhul tähendab, et ühendus on registreeritud on_accept(), valmis andmete vastuvõtmiseks.

on_recv() loeb ühendusest andmeid kuni HTTP päringu täieliku vastuvõtmiseni, seejärel registreerib töötleja on_send() HTTP vastuse saatmiseks. Kui klient katkestab ühenduse, kustutatakse pistikupesa registreerimine ja suletakse kasutades close().

Näita funktsiooni on_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);
    }
}

Sündmuste haldaja on_send() kutsutakse pärast OS-i sündmuse genereerimist EPOLLOUT, mis tähendab, et ühendus on registreeritud on_recv(), valmis andmete saatmiseks. See funktsioon saadab kliendile HTTP-vastuse, mis sisaldab HTML-i koos pildiga, ja muudab seejärel sündmuste töötleja uuesti on_recv().

Näita funktsiooni on_send().

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);
}

Ja lõpuks failis http_server.c, funktsioonis main() loome I/O reaktori kasutades reactor_new(), looge serveri pesa ja registreerige see, käivitage reaktor kasutades reactor_run() täpselt ühe minuti ja siis vabastame ressursid ja väljume programmist.

Kuva 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);
}

Kontrollime, kas kõik toimib ootuspäraselt. Koostamine (chmod a+x compile.sh && ./compile.sh projekti juures) ja käivitage ise kirjutatud server, avage http://127.0.0.1:18470 brauseris ja vaadake, mida ootasime:

Täisfunktsionaalne tühi-C I/O reaktor

Jõudluse mõõtmine

Näita minu auto tehnilisi andmeid

$ 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

Mõõdame ühe lõimega serveri jõudlust. Avame kaks terminali: ühes hakkame jooksma ./http_server, teises - wrk. Minuti pärast kuvatakse teises terminalis järgmine statistika:

$ 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

Meie ühe lõimega server suutis töödelda üle 11 miljoni päringu minutis, mis pärinesid 100 ühendusest. Pole paha tulemus, aga kas seda saab parandada?

Mitme lõimega server

Nagu eespool mainitud, saab I/O reaktori luua eraldi lõimedes, kasutades seeläbi kõiki protsessori südamikke. Rakendame seda lähenemisviisi praktikas:

Kuva 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);
    }
}

Nüüd iga lõime omab oma reaktor:

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

Pange tähele, et funktsiooni argument new_server() toetab true. See tähendab, et määrame valiku serveri pesale SO_REUSEPORTkasutada seda mitme lõimega keskkonnas. Täpsemalt saate lugeda siin.

Teine jooks

Mõõdame nüüd mitme lõimega serveri jõudlust:

$ 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

1 minutiga töödeldud päringute arv kasvas ~3.28 korda! Kuid ümmargusest numbrist jäi meil puudu vaid ~XNUMX miljonit, nii et proovime seda parandada.

Kõigepealt vaatame koostatud statistikat täiuslik:

$ 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

CPU Affinity kasutamine, koostamine koos -march=native, PGO, tabamuste arvu kasv vahemälu, suurendama MAX_EVENTS ja kasutada EPOLLET ei toonud jõudlust oluliselt juurde. Aga mis juhtub, kui suurendate samaaegsete ühenduste arvu?

Statistika 352 samaaegse ühenduse kohta:

$ 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

Soovitud tulemus saadi ja koos sellega huvitav graafik, mis näitab 1 minuti jooksul töödeldud päringute arvu sõltuvust ühenduste arvust:

Täisfunktsionaalne tühi-C I/O reaktor

Näeme, et pärast paarisada ühendust langeb mõlema serveri töödeldud päringute arv järsult (mitme lõimega versioonis on see märgatavam). Kas see on seotud Linuxi TCP/IP-virna juurutamisega? Kirjutage kommentaaridesse oma eeldused graafiku sellise käitumise ja mitmelõimeliste ja ühelõimeliste valikute optimeerimise kohta.

Kui märkis kommentaarides see jõudluskatse ei näita I/O reaktori käitumist reaalsete koormuste korral, sest peaaegu alati suhtleb server andmebaasiga, väljastab logisid, kasutab krüptograafiat TLS jne, mille tulemusena muutub koormus ebaühtlaseks (dünaamiliseks). Testid koos kolmanda osapoole komponentidega viiakse läbi I/O proaktorit käsitlevas artiklis.

I/O reaktori puudused

Peate mõistma, et I/O reaktoril pole puudusi, nimelt:

  • I/O reaktori kasutamine mitme keermega keskkonnas on mõnevõrra keerulisem, kuna peate vooge käsitsi haldama.
  • Praktika näitab, et enamikul juhtudel on koormus ebaühtlane, mis võib kaasa tuua ühe lõime logisemise, samal ajal kui teine ​​on tööga hõivatud.
  • Kui üks sündmuste töötleja blokeerib lõime, blokeerib ka süsteemivalija ise, mis võib põhjustada raskesti leitavaid vigu.

Lahendab need probleemid I/O proaktor, millel on sageli ajakava, mis jaotab koormuse ühtlaselt lõimede kogumile, ja millel on ka mugavam API. Me räägime sellest hiljem, minu teises artiklis.

Järeldus

Siin on meie teekond teooriast otse profileerija heitgaasini jõudnud lõpule.

Te ei tohiks sellel pikemalt peatuda, sest võrgutarkvara kirjutamiseks on palju teisi võrdselt huvitavaid erineva mugavuse ja kiirusega lähenemisviise. Minu arvates on huvitavad lingid allpool.

Järgmise korrani!

Huvitavad projektid

Mida veel lugeda?

Allikas: www.habr.com

Lisa kommentaar