Volledige kaal-C I/O-reaktor

Volledige kaal-C I/O-reaktor

Inleiding

I/O-reaktor (enkeldraad gebeurtenis lus) is 'n patroon vir die skryf van hoëladingsagteware, wat in baie gewilde oplossings gebruik word:

In hierdie artikel sal ons kyk na die ins en outs van 'n I/O-reaktor en hoe dit werk, 'n implementering in minder as 200 reëls kode skryf, en 'n eenvoudige HTTP-bedienerproses van meer as 40 miljoen versoeke/min maak.

voorwoord

  • Die artikel is geskryf om te help om die werking van die I/O-reaktor te verstaan, en dus die risiko's te verstaan ​​wanneer dit gebruik word.
  • Kennis van die basiese beginsels word vereis om die artikel te verstaan. C taal en 'n bietjie ondervinding in netwerktoepassingsontwikkeling.
  • Alle kode is in C-taal geskryf streng volgens (versigtig: lang PDF) na C11 standaard vir Linux en beskikbaar op GitHub.

Hoekom dit doen?

Met die toenemende gewildheid van die internet het webbedieners begin om 'n groot aantal verbindings gelyktydig te hanteer, en daarom is twee benaderings probeer: blokkering van I/O op 'n groot aantal OS-drade en nie-blokkerende I/O in kombinasie met 'n gebeurteniskennisgewingstelsel, ook genoem "stelselkieser" (epoll/kqueue/IOCP/ens).

Die eerste benadering behels die skep van 'n nuwe OS-draad vir elke inkomende verbinding. Die nadeel daarvan is swak skaalbaarheid: die bedryfstelsel sal baie moet implementeer konteks oorgange и stelsel oproepe. Dit is duur bedrywighede en kan lei tot 'n gebrek aan gratis RAM met 'n indrukwekkende aantal verbindings.

Die gewysigde weergawe beklemtoon vaste aantal drade (draadpoel), waardeur die stelsel verhoed om uitvoering te staak, maar terselfdertyd 'n nuwe probleem bekendstel: as 'n draadpoel tans geblokkeer word deur langleesbewerkings, dan sal ander voetstukke wat reeds in staat is om data te ontvang, nie in staat wees om doen so.

Die tweede benadering gebruik gebeurtenis kennisgewing stelsel (stelselkieser) verskaf deur die bedryfstelsel. Hierdie artikel bespreek die mees algemene tipe stelselkieser, gebaseer op waarskuwings (gebeurtenisse, kennisgewings) oor gereedheid vir I/O-bedrywighede, eerder as op kennisgewings oor hul voltooiing. 'n Vereenvoudigde voorbeeld van die gebruik daarvan kan deur die volgende blokdiagram voorgestel word:

Volledige kaal-C I/O-reaktor

Die verskil tussen hierdie benaderings is soos volg:

  • Blokkering van I/O-bewerkings opskort gebruikersvloei tottotdat die bedryfstelsel reg is defragmenteer inkomende IP-pakkies om byte stroom (TCP, data ontvang) of daar sal nie genoeg spasie beskikbaar wees in die interne skryfbuffers vir daaropvolgende versending via NIC (stuur data).
  • Stelsel kieser oortyd stel die program in kennis dat die bedryfstelsel reeds gedefragmenteerde IP-pakkies (TCP, data-ontvangs) of genoeg spasie in interne skryfbuffers reeds beskikbaar (stuur data).

Om dit op te som, die bespreking van 'n OS-draad vir elke I/O is 'n vermorsing van rekenaarkrag, want in werklikheid doen die drade nie bruikbare werk nie (dit is waar die term vandaan kom "sagteware onderbreking"). Die stelselkieser los hierdie probleem op, wat die gebruikerprogram toelaat om SVE-hulpbronne baie meer ekonomies te gebruik.

I/O-reaktormodel

Die I/O-reaktor dien as 'n laag tussen die stelselkieser en die gebruikerskode. Die beginsel van sy werking word beskryf deur die volgende blokdiagram:

Volledige kaal-C I/O-reaktor

  • Laat ek jou daaraan herinner dat 'n gebeurtenis 'n kennisgewing is dat 'n sekere sok 'n nie-blokkerende I/O-bewerking kan uitvoer.
  • 'n Gebeurtenishanteerder is 'n funksie wat deur die I/O-reaktor genoem word wanneer 'n gebeurtenis ontvang word, wat dan 'n nie-blokkerende I/O-operasie uitvoer.

Dit is belangrik om daarop te let dat die I/O-reaktor per definisie enkeldraad is, maar daar is niks wat die konsep keer om in 'n multi-draadomgewing gebruik te word teen 'n verhouding van 1 draad: 1 reaktor, waardeur alle SVE-kerns herwin word nie.

Implementering

Ons sal die publieke koppelvlak in 'n lêer plaas reactor.h, en implementering - in reactor.c. reactor.h sal uit die volgende aankondigings bestaan:

Toon verklarings in reaktor.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);

Die I/O-reaktorstruktuur bestaan ​​uit lêer beskrywer keurder epoll и hash-tabelle GHashTable, wat elke sok na karteer CallbackData (struktuur van 'n gebeurtenishanteerder en 'n gebruikersargument daarvoor).

Wys reaktor- en terugbeldata

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

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

Neem asseblief kennis dat ons die vermoë om te hanteer geaktiveer het onvolledige tipe volgens die indeks. IN reactor.h ons verklaar die struktuur reactoren in reactor.c ons definieer dit, waardeur die gebruiker verhoed word om sy velde uitdruklik te verander. Dit is een van die patrone versteek data, wat bondig in C semantiek pas.

Funksies reactor_register, reactor_deregister и reactor_reregister werk die lys van belangstellende voetstukke en ooreenstemmende gebeurtenishanteerders in die stelselkieser en hash-tabel op.

Wys registrasie funksies

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

Nadat die I/O-reaktor die gebeurtenis met die beskrywer onderskep het fd, roep dit die ooreenstemmende gebeurtenishanteerder, waarna dit deurgaan fd, bietjie masker gegenereerde gebeurtenisse en 'n gebruikerwyser na void.

Wys reactor_run() funksie

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

Om op te som, die ketting van funksie-oproepe in gebruikerskode sal die volgende vorm aanneem:

Volledige kaal-C I/O-reaktor

Enkeldraadbediener

Om die I/O-reaktor onder hoë las te toets, sal ons 'n eenvoudige HTTP-webbediener skryf wat op enige versoek met 'n beeld reageer.

'N Vinnige verwysing na die HTTP-protokol

HTTP - dit is die protokol toepassing vlak, hoofsaaklik gebruik vir bediener-blaaier-interaksie.

HTTP kan maklik oor gebruik word vervoer protokol TCP, stuur en ontvang van boodskappe in 'n gespesifiseerde formaat spesifikasie.

Versoek Formaat

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

  • CRLF is 'n reeks van twee karakters: r и n, skei die eerste reël van die versoek, opskrifte en data.
  • <КОМАНДА> - een van CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Die blaaier sal 'n opdrag na ons bediener stuur GET, wat beteken "Stuur vir my die inhoud van die lêer."
  • <URI> - eenvormige hulpbronidentifiseerder. Byvoorbeeld, as URI = /index.html, dan versoek die kliënt die hoofbladsy van die webwerf.
  • <ВЕРСИЯ HTTP> — weergawe van die HTTP-protokol in die formaat HTTP/X.Y. Die mees gebruikte weergawe vandag is HTTP/1.1.
  • <ЗАГОЛОВОК N> is 'n sleutel-waarde-paar in die formaat <КЛЮЧ>: <ЗНАЧЕНИЕ>, na die bediener gestuur vir verdere ontleding.
  • <ДАННЫЕ> - data wat deur die bediener benodig word om die operasie uit te voer. Dikwels is dit eenvoudig Into of enige ander formaat.

Antwoordformaat

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

  • <КОД СТАТУСА> is 'n getal wat die resultaat van die bewerking verteenwoordig. Ons bediener sal altyd status 200 (suksesvolle operasie) gee.
  • <ОПИСАНИЕ СТАТУСА> — stringvoorstelling van die statuskode. Vir statuskode 200 is dit OK.
  • <ЗАГОЛОВОК N> — kopskrif van dieselfde formaat as in die versoek. Ons sal die titels teruggee Content-Length (lêergrootte) en Content-Type: text/html (terug datatipe).
  • <ДАННЫЕ> - data wat deur die gebruiker aangevra is. In ons geval is dit die pad na die beeld in HTML.

lêer http_server.c (enkeldraadbediener) sluit lêer in common.h, wat die volgende funksie prototipes bevat:

Wys funksie prototipes in gemeen.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);

Die funksionele makro word ook beskryf SAFE_CALL() en die funksie word gedefinieer fail(). Die makro vergelyk die waarde van die uitdrukking met die fout, en as die voorwaarde waar is, roep die funksie fail():

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

Funksie fail() druk die geslaagde argumente na die terminale (soos printf()) en beëindig die program met die kode 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);
}

Funksie new_server() gee die lêerbeskrywing terug van die "bediener"-sok wat deur stelseloproepe geskep is socket(), bind() и listen() en in staat is om inkomende verbindings in 'n nie-blokkerende modus te aanvaar.

Wys new_server() funksie

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

  • Let daarop dat die sok aanvanklik in nie-blokkerende modus geskep word deur die vlag te gebruik SOCK_NONBLOCKsodat in die funksie on_accept() (lees meer) stelseloproep accept() het nie die draaduitvoering gestop nie.
  • As reuse_port is gelyk aan true, dan sal hierdie funksie die sok konfigureer met die opsie SO_REUSEPORT deur setsockopt()om dieselfde poort in 'n multi-threaded omgewing te gebruik (sien afdeling "Multi-threaded server").

Gebeurtenis hanteerder on_accept() genoem nadat die bedryfstelsel 'n gebeurtenis genereer EPOLLIN, wat in hierdie geval beteken dat die nuwe verbinding aanvaar kan word. on_accept() aanvaar 'n nuwe verbinding, skakel dit oor na nie-blokkerende modus en registreer by 'n gebeurtenishanteerder on_recv() in 'n I/O-reaktor.

Wys on_accept() funksie

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

Gebeurtenis hanteerder on_recv() genoem nadat die bedryfstelsel 'n gebeurtenis genereer EPOLLIN, in hierdie geval wat beteken dat die verbinding geregistreer is on_accept(), gereed om data te ontvang.

on_recv() lees data vanaf die verbinding totdat die HTTP-versoek heeltemal ontvang is, dan registreer dit 'n hanteerder on_send() om 'n HTTP-antwoord te stuur. As die kliënt die verbinding verbreek, word die sok gederegistreer en gesluit met behulp van close().

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

Gebeurtenis hanteerder on_send() genoem nadat die bedryfstelsel 'n gebeurtenis genereer EPOLLOUT, wat beteken dat die verbinding geregistreer is on_recv(), gereed om data te stuur. Hierdie funksie stuur 'n HTTP-antwoord wat HTML bevat met 'n prent na die kliënt en verander dan die gebeurtenishanteerder terug na on_recv().

Wys on_send() funksie

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

En uiteindelik, in die lêer http_server.c, in funksie main() ons skep 'n I/O-reaktor met behulp van reactor_new(), skep 'n bedienersok en registreer dit, begin die reaktor met behulp van reactor_run() vir presies een minuut, en dan stel ons hulpbronne vry en verlaat die program.

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

Kom ons kyk of alles werk soos verwag. Samestelling (chmod a+x compile.sh && ./compile.sh in die projekwortel) en begin die selfgeskrewe bediener, maak oop http://127.0.0.1:18470 in die blaaier en kyk wat ons verwag het:

Volledige kaal-C I/O-reaktor

Prestasiemeting

Wys my motorspesifikasies

$ 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

Kom ons meet die werkverrigting van 'n enkeldraadbediener. Kom ons maak twee terminale oop: in een sal ons hardloop ./http_server, in 'n ander - wrk. Na 'n minuut sal die volgende statistieke in die tweede terminaal vertoon word:

$ 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

Ons enkeldraadbediener kon meer as 11 miljoen versoeke per minuut verwerk wat van 100 verbindings afkomstig is. Nie 'n slegte resultaat nie, maar kan dit verbeter word?

Multithreaded bediener

Soos hierbo genoem, kan die I/O-reaktor in aparte drade geskep word, waardeur alle SVE-kerne gebruik word. Kom ons pas hierdie benadering in die praktyk:

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

Nou elke draad besit sy eie reaktor:

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

Neem asseblief kennis dat die funksie argument new_server() advokate true. Dit beteken dat ons die opsie aan die bedienersok toewys SO_REUSEPORTom dit in 'n multi-draad omgewing te gebruik. Jy kan meer besonderhede lees hier.

Tweede lopie

Kom ons meet nou die werkverrigting van 'n multi-threaded bediener:

$ 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

Die aantal versoeke wat in 1 minuut verwerk is, het met ~3.28 keer toegeneem! Maar ons was net ~XNUMX miljoen kort van die ronde getal, so kom ons probeer dit regmaak.

Kom ons kyk eers na die statistieke wat gegenereer word 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

Gebruik CPU Affinity, samestelling met -march=native, PGO, 'n toename in die aantal treffers kas, Verhoog MAX_EVENTS en gebruik EPOLLET het nie 'n beduidende toename in prestasie gegee nie. Maar wat gebeur as jy die aantal gelyktydige verbindings verhoog?

Statistiek vir 352 gelyktydige verbindings:

$ 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

Die gewenste resultaat is verkry, en daarmee saam 'n interessante grafiek wat die afhanklikheid van die aantal verwerkte versoeke in 1 minuut op die aantal verbindings toon:

Volledige kaal-C I/O-reaktor

Ons sien dat na 'n paar honderd verbindings, die aantal verwerkte versoeke vir beide bedieners skerp daal (in die multi-threaded weergawe is dit meer opvallend). Is dit verwant aan die Linux TCP/IP stapel implementering? Skryf gerus jou aannames oor hierdie gedrag van die grafiek en optimaliserings vir multi-draad en enkel-draad opsies in die kommentaar.

As opgemerk in die kommentaar wys hierdie prestasietoets nie die gedrag van die I/O-reaktor onder werklike vragte nie, want byna altyd interaksie met die bediener met die databasis, voer logs uit, gebruik kriptografie met TLS ens., as gevolg waarvan die las nie-uniform (dinamies) word. Toetse saam met derdeparty-komponente sal in die artikel oor die I/O-proaktor uitgevoer word.

Nadele van I/O-reaktor

U moet verstaan ​​dat die I/O-reaktor nie sonder sy nadele is nie, naamlik:

  • Die gebruik van 'n I/O-reaktor in 'n multi-draad omgewing is ietwat moeiliker, want jy sal die vloei handmatig moet bestuur.
  • Praktyk toon dat die vrag in die meeste gevalle nie eenvormig is nie, wat daartoe kan lei dat een draad aanteken terwyl 'n ander besig is met werk.
  • As een gebeurtenishanteerder 'n draad blokkeer, sal die stelselkieser self ook blokkeer, wat kan lei tot moeilik om te vind foute.

Los hierdie probleme op I/O proaktor, wat dikwels 'n skeduleerder het wat die las eweredig na 'n poel drade versprei, en ook 'n geriefliker API het. Ons sal later daaroor praat, in my ander artikel.

Gevolgtrekking

Dit is hier waar ons reis van teorie reguit tot in die profiler-uitlaat tot 'n einde gekom het.

U moet nie hieroor stilstaan ​​nie, want daar is baie ander ewe interessante benaderings om netwerksagteware met verskillende vlakke van gerief en spoed te skryf. Interessant, na my mening, word skakels hieronder gegee.

Tot volgende keer!

Interessante projekte

Wat moet ek nog lees?

Bron: will.com

Voeg 'n opmerking