Full-lögun bare-C I/O reactor

Full-lögun bare-C I/O reactor

Inngangur

I/O reactor (einþráður viðburðarlykkja) er mynstur til að skrifa háhlaðan hugbúnað, notað í mörgum vinsælum lausnum:

Í þessari grein munum við skoða inn- og útfærslur á I/O reactor og hvernig hann virkar, skrifa útfærslu í minna en 200 línum af kóða og gera einfalt HTTP netþjónsferli yfir 40 milljónir beiðna/mín.

Formáli

  • Greinin var skrifuð til að hjálpa til við að skilja virkni I/O reactorsins og skilja því áhættuna við notkun hans.
  • Þekking á grunnatriðum er nauðsynleg til að skilja greinina. C tungumál og nokkur reynsla í þróun netforrita.
  • Allur kóði er skrifaður á C tungumáli stranglega samkvæmt (varúð: langur PDF) að C11 staðli fyrir Linux og fáanlegt á GitHub.

Af hverju er þetta nauðsynlegt?

Með auknum vinsældum internetsins fóru vefþjónar að þurfa að sinna miklum fjölda tenginga samtímis og því voru tvær leiðir prófaðar: að loka fyrir I/O á fjölda stýrikerfisþráða og I/O sem ekki hindrar í samsetningu með viðburðatilkynningarkerfi, einnig kallað „kerfisval“ (epoll/kqueue/IOCP/etc).

Fyrsta aðferðin fól í sér að búa til nýjan stýrikerfisþráð fyrir hverja komandi tengingu. Ókostur þess er lélegur sveigjanleiki: stýrikerfið verður að innleiða marga samhengisbreytingar и kerfissímtöl. Þetta eru dýrar aðgerðir og geta leitt til skorts á ókeypis vinnsluminni með glæsilegum fjölda tenginga.

Hin breytta útgáfa hápunktur fastur fjöldi þráða (þráðahópur), sem kemur í veg fyrir að kerfið hætti við framkvæmd, en kynnir á sama tíma nýtt vandamál: ef þráðasafn er lokað með löngum lestri, þá munu aðrar innstungur sem þegar geta tekið á móti gögnum ekki geta gerðu það.

Önnur aðferðin notar tilkynningakerfi fyrir atburði (kerfisval) sem stýrikerfið gefur. Þessi grein fjallar um algengustu gerð kerfisvals, byggt á tilkynningum (atburðum, tilkynningum) um viðbúnað fyrir I/O aðgerðir, frekar en á tilkynningar um að þeim sé lokið. Einfaldað dæmi um notkun þess er hægt að tákna með eftirfarandi blokkarmynd:

Full-lögun bare-C I/O reactor

Munurinn á þessum aðferðum er sem hér segir:

  • Lokun á I/O aðgerðir fresta notendaflæði þar tilþar til stýrikerfið er rétt defragments komandi IP pakkar að bæta straumi (TCP, tekur við gögnum) eða það verður ekki nóg pláss tiltækt í innri skrifbiðmunum fyrir síðari sendingu í gegnum NIC (senda gögn).
  • Kerfisval með tímanum lætur forritið vita að stýrikerfið þegar sundraða IP-pakka (TCP, gagnamóttaka) eða nóg pláss í innri skrifbiðmunum þegar í boði (send gögn).

Til að draga þetta saman, þá er það sóun á tölvuorku að panta stýrikerfisþráð fyrir hvert I/O, vegna þess að í raun eru þræðir ekki að vinna gagnlega vinnu (þetta er þaðan sem hugtakið kemur frá "hugbúnaðar truflun"). Kerfisvalið leysir þetta vandamál og gerir notendaforritinu kleift að nota örgjörvaauðlindir mun hagkvæmari.

I/O reactor líkan

I/O reactor virkar sem lag á milli kerfisvalsins og notendakóðans. Meginreglan um starfsemi þess er lýst með eftirfarandi blokkarmynd:

Full-lögun bare-C I/O reactor

  • Leyfðu mér að minna þig á að atburður er tilkynning um að ákveðin fals sé fær um að framkvæma I/O aðgerð sem ekki hindrar.
  • Atburðastjórnun er aðgerð sem I/O reactor kallar á þegar atburður er móttekinn, sem framkvæmir síðan I/O aðgerð sem ekki hindrar.

Það er mikilvægt að hafa í huga að I/O reactor er samkvæmt skilgreiningu einþráður, en það er ekkert sem kemur í veg fyrir að hugmyndin sé notuð í fjölþráðu umhverfi í hlutfallinu 1 þráður: 1 reactor og endurvinnir þar með alla CPU kjarna.

Framkvæmd

Við munum setja almenna viðmótið í skrá reactor.h, og framkvæmd - í reactor.c. reactor.h mun samanstanda af eftirfarandi tilkynningum:

Sýna yfirlýsingar í reactor.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 reactor uppbyggingin samanstendur af skráarlýsing veljara epoll и kjötkássa töflur GHashTable, sem kortleggur hverja innstungu að CallbackData (uppbygging atburðastjórnunar og notendarök fyrir það).

Sýna Reactor og Callback Data

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

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

Vinsamlegast athugaðu að við höfum virkjað getu til að meðhöndla ófullkomin gerð samkvæmt vísitölunni. IN reactor.h við lýsum yfir uppbyggingunni reactor, og inn reactor.c við skilgreinum það og komum þannig í veg fyrir að notandinn breyti beinlínis sviðum sínum. Þetta er eitt af mynstrum fela gögn, sem passar í stuttu máli inn í C merkingarfræði.

Aðgerðir reactor_register, reactor_deregister и reactor_reregister uppfærðu listann yfir áhugaverða falsa og samsvarandi atburðastjórnun í kerfisvalinu og kjötkássatöflunni.

Sýna skráningaraðgerðir

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

Eftir að I/O reactor hefur stöðvað atburðinn með lýsingunni fd, kallar það samsvarandi atburðastjórnun, sem það fer til fd, bita gríma myndaðir atburðir og notandi bendir á void.

Sýna reactor_run() aðgerð

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

Til að draga saman, mun keðja aðgerðakalla í notendakóða hafa eftirfarandi form:

Full-lögun bare-C I/O reactor

Einþráður netþjónn

Til þess að prófa I/O reactor undir miklu álagi munum við skrifa einfaldan HTTP vefþjón sem svarar öllum beiðnum með mynd.

Fljótleg tilvísun í HTTP samskiptareglur

HTTP - þetta er siðareglur umsóknarstig, fyrst og fremst notað fyrir samskipti miðlara og vafra.

Auðvelt er að nota HTTP yfir flutninga bókun TCP, senda og taka á móti skilaboðum á tilteknu sniði forskrift.

Snið beiðni

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

  • CRLF er röð tveggja stafa: r и n, aðskilja fyrstu línu beiðninnar, hausa og gögn.
  • <КОМАНДА> - einn af CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Vafrinn mun senda skipun á netþjóninn okkar GET, sem þýðir "Sendu mér innihald skrárinnar."
  • <URI> - samræmdu auðkenni auðlinda. Til dæmis, ef URI = /index.html, þá biður viðskiptavinurinn um aðalsíðu síðunnar.
  • <ВЕРСИЯ HTTP> — útgáfa af HTTP samskiptareglum á sniði HTTP/X.Y. Algengasta útgáfan í dag er HTTP/1.1.
  • <ЗАГОЛОВОК N> er lykilgildi par í sniðinu <КЛЮЧ>: <ЗНАЧЕНИЕ>, send á netþjóninn til frekari greiningar.
  • <ДАННЫЕ> — gögn sem miðlarinn þarf til að framkvæma aðgerðina. Oft er það einfalt JSON eða einhverju öðru sniði.

Svarsnið

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

  • <КОД СТАТУСА> er tala sem táknar niðurstöðu aðgerðarinnar. Miðlarinn okkar mun alltaf skila stöðu 200 (vel heppnuð aðgerð).
  • <ОПИСАНИЕ СТАТУСА> — strengjaframsetning á stöðukóða. Fyrir stöðukóða 200 er þetta OK.
  • <ЗАГОЛОВОК N> — haus með sama sniði og í beiðninni. Við munum skila titlunum Content-Length (skráarstærð) og Content-Type: text/html (tegund skilagagna).
  • <ДАННЫЕ> — gögn sem notandinn óskar eftir. Í okkar tilviki er þetta leiðin að myndinni HTML.

skrá http_server.c (einn þráður netþjónn) inniheldur skrá common.h, sem inniheldur eftirfarandi frumgerðir aðgerða:

Sýna aðgerðir frumgerðir sameiginlega.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);

Hagnýtur fjölvi er einnig lýst SAFE_CALL() og fallið er skilgreint fail(). Fjölvi ber saman gildi tjáningarinnar við villuna og ef skilyrðið er satt kallar það fallið fail():

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

Virka fail() prentar framlögð rök í flugstöðina (eins og printf()) og lýkur forritinu með kóðanum 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);
}

Virka new_server() skilar skráarlýsingu "miðlara" tengisins sem búin er til með kerfissímtölum socket(), bind() и listen() og fær um að taka við komandi tengingum í ólokandi ham.

Sýna new_server() aðgerð

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

  • Athugaðu að falsið er upphaflega búið til í ólokandi ham með því að nota fánann SOCK_NONBLOCKþannig að í fallinu on_accept() (lesa meira) kerfiskall accept() stöðvaði ekki framkvæmd þráðarins.
  • Ef reuse_port jafngildir true, þá mun þessi aðgerð stilla falsinn með valkostinum SO_REUSEPORT í gegnum setsockopt()til að nota sömu tengið í fjölþráðu umhverfi (sjá kaflann „Margþráður miðlari“).

Viðburðastjóri on_accept() kallað eftir að stýrikerfið býr til atburð EPOLLIN, í þessu tilviki sem þýðir að hægt er að samþykkja nýja tenginguna. on_accept() tekur við nýrri tengingu, skiptir yfir í ólokandi stillingu og skráir sig hjá atburðastjórnun on_recv() í I/O reactor.

Sýna on_accept() aðgerð

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

Viðburðastjóri on_recv() kallað eftir að stýrikerfið býr til atburð EPOLLIN, í þessu tilviki sem þýðir að tengingin skráð on_accept(), tilbúinn til að taka á móti gögnum.

on_recv() les gögn úr tengingunni þar til HTTP beiðni er alveg móttekin, þá skráir það meðhöndlun on_send() til að senda HTTP svar. Ef viðskiptavinurinn slítur tenginguna er falsið afskráð og lokað með því að nota close().

Sýna aðgerð 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);
    }
}

Viðburðastjóri on_send() kallað eftir að stýrikerfið býr til atburð EPOLLOUT, sem þýðir að tengingin skráð on_recv(), tilbúinn til að senda gögn. Þessi aðgerð sendir HTTP-svar sem inniheldur HTML með mynd til viðskiptavinarins og breytir síðan atburðastjórnuninni aftur í on_recv().

Sýna on_send() aðgerð

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

Og að lokum, í skránni http_server.c, í virkni main() við búum til I/O reactor með því að nota reactor_new(), búðu til netþjóninnstungu og skráðu hana, ræstu reactor með því að nota reactor_run() í nákvæmlega eina mínútu, og þá losum við tilföng og hættum forritinu.

Sýna 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);
}

Við skulum athuga hvort allt virki eins og búist var við. Samsetning (chmod a+x compile.sh && ./compile.sh í verkefnisrótinni) og ræstu sjálfskrifaða netþjóninn, opnaðu http://127.0.0.1:18470 í vafranum og sjáðu hvað við áttum von á:

Full-lögun bare-C I/O reactor

Árangursmæling

Sýndu upplýsingar um bílinn minn

$ 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

Við skulum mæla frammistöðu eins-þráðs netþjóns. Við skulum opna tvær skautanna: í einni munum við keyra ./http_server, í öðru - vinna. Eftir eina mínútu birtist eftirfarandi tölfræði í annarri flugstöðinni:

$ 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

Einþráða netþjónninn okkar var fær um að vinna úr yfir 11 milljón beiðnum á mínútu sem komu frá 100 tengingum. Ekki slæm niðurstaða en er hægt að bæta hana?

Fjölþráður þjónn

Eins og getið er hér að ofan er hægt að búa til I/O reactor í aðskildum þráðum og nýta þannig alla CPU kjarna. Við skulum koma þessari nálgun í framkvæmd:

Sýna 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ú er hver þráður á sitt eigið reactor:

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

Vinsamlegast athugið að fall rök new_server() talsmenn true. Þetta þýðir að við úthlutum valmöguleikanum á netþjónsinnstunguna SO_REUSEPORTað nota það í fjölþráðu umhverfi. Þú getur lesið frekari upplýsingar hér.

Annað hlaup

Nú skulum við mæla árangur fjölþráða netþjóns:

$ 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

Fjöldi beiðna sem afgreiddar voru á 1 mínútu jókst um ~3.28 sinnum! En okkur vantaði aðeins ~XNUMX milljónir upp í hringtöluna, svo við skulum reyna að laga það.

Fyrst skulum við líta á tölfræðina sem myndast fullkominn:

$ 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

Að nota CPU Affinity, samantekt með -march=native, PGO, aukning á fjölda heimsókna skyndiminni, auka MAX_EVENTS og nota EPOLLET gaf ekki verulega aukningu á frammistöðu. En hvað gerist ef þú fjölgar samtímis tengingum?

Tölfræði fyrir 352 samtímis tengingar:

$ 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

Æskileg niðurstaða fékkst og þar með áhugavert línurit sem sýnir hversu háð fjölda afgreiddra beiðna á 1 mínútu er háð fjölda tenginga:

Full-lögun bare-C I/O reactor

Við sjáum að eftir nokkur hundruð tengingar lækkar fjöldi afgreiddra beiðna fyrir báða netþjóna verulega (í fjölþráðu útgáfunni er þetta meira áberandi). Er þetta tengt Linux TCP/IP stafla útfærslunni? Ekki hika við að skrifa forsendur þínar um þessa hegðun grafsins og hagræðingar fyrir fjölþráða og einþráða valkosti í athugasemdunum.

Как tekið fram í athugasemdunum sýnir þetta frammistöðupróf ekki hegðun I/O reactors við raunverulegt álag, því næstum alltaf hefur þjónninn samskipti við gagnagrunninn, gefur út annála, notar dulmál með TLS o.s.frv., þar af leiðandi verður álagið ójafnt (dýnamískt). Prófanir ásamt íhlutum þriðja aðila verða gerðar í greininni um I/O proactor.

Ókostir I/O reactors

Þú þarft að skilja að I/O reactor er ekki án galla, þ.e.

  • Nokkuð erfiðara er að nota I/O reactor í fjölþráðu umhverfi vegna þess þú verður að stjórna flæðinu handvirkt.
  • Æfingin sýnir að í flestum tilfellum er álagið ójafnt, sem getur leitt til þess að einn þráður skráist á meðan annar er upptekinn við vinnu.
  • Ef einn atburðastjórnun lokar þráð, mun kerfisvalið sjálfur einnig loka, sem getur leitt til villu sem erfitt er að finna.

Leysir þessi vandamál I/O proactor, sem oft er með tímaáætlun sem dreifir álaginu jafnt í hóp þráða og hefur einnig þægilegra API. Við munum tala um það síðar, í annarri grein minni.

Ályktun

Þetta er þar sem ferð okkar frá kenningunni beint inn í útblástur prófílsins hefur lokið.

Þú ættir ekki að dvelja við þetta, því það eru margar aðrar jafn áhugaverðar aðferðir við að skrifa nethugbúnað með mismunandi þægindum og hraða. Athyglisvert, að mínu mati, eru hlekkir hér að neðan.

Þar til næst!

Áhugaverð verkefni

Hvað annað ætti ég að lesa?

Heimild: www.habr.com

Bæta við athugasemd