Reactor I/O bare-C cu funcții complete

Reactor I/O bare-C cu funcții complete

Introducere

reactor I/O (filet simplu bucla de eveniment) este un model pentru scrierea de software cu încărcare mare, utilizat în multe soluții populare:

În acest articol, vom analiza dezavantajele unui reactor I/O și cum funcționează, vom scrie o implementare în mai puțin de 200 de linii de cod și vom face un proces simplu de server HTTP de peste 40 de milioane de solicitări/min.

Prefață

  • Articolul a fost scris pentru a ajuta la înțelegerea funcționării reactorului I/O și, prin urmare, pentru a înțelege riscurile atunci când îl utilizați.
  • Înțelegerea elementelor de bază este necesară pentru a înțelege articolul. limbajul C și ceva experiență în dezvoltarea de aplicații de rețea.
  • Tot codul este scris în limbaj C strict conform (Atenție: PDF lung) la standardul C11 pentru Linux și disponibil pe GitHub.

De ce o fac?

Odată cu popularitatea în creștere a internetului, serverele web au început să aibă nevoie să gestioneze un număr mare de conexiuni simultan și, prin urmare, s-au încercat două abordări: blocarea I/O pe un număr mare de fire de operare și I/O neblocante în combinație cu un sistem de notificare a evenimentelor, numit și „selector de sistem” (epoll/kcoadă/IOCP/etc).

Prima abordare a implicat crearea unui nou thread de sistem de operare pentru fiecare conexiune de intrare. Dezavantajul său este scalabilitatea slabă: sistemul de operare va trebui să implementeze multe tranziții de context и apeluri de sistem. Sunt operațiuni costisitoare și pot duce la lipsa RAM liberă cu un număr impresionant de conexiuni.

Versiunea modificată evidențiază număr fix de fire (pool de fire), prevenind astfel blocarea sistemului, dar în același timp introducând o nouă problemă: dacă un pool de fire este blocat în prezent de operațiuni de citire lungă, atunci alte socket-uri care sunt deja capabile să primească date nu vor putea face asa de.

A doua abordare folosește sistem de notificare a evenimentelor (selector de sistem) oferit de sistemul de operare. Acest articol discută cel mai comun tip de selector de sistem, bazat pe alerte (evenimente, notificări) despre pregătirea pentru operațiuni I/O, mai degrabă decât pe notificări despre finalizarea acestora. Un exemplu simplificat de utilizare poate fi reprezentat de următoarea diagramă bloc:

Reactor I/O bare-C cu funcții complete

Diferența dintre aceste abordări este următoarea:

  • Blocarea operațiunilor I/O suspenda fluxul utilizatorului pana candpână când sistemul de operare este corect defragmente sosit pachete IP la fluxul de octeți (TCP, primirea datelor) sau nu va fi suficient spațiu disponibil în tampoanele interne de scriere pentru trimiterea ulterioară prin NIC (trimiterea datelor).
  • Selector de sistem peste orar anunță programul că sistemul de operare deja pachete IP defragmentate (TCP, recepție de date) sau spațiu suficient în bufferele interne de scriere deja disponibile (trimiterea datelor).

Pentru a rezuma, rezervarea unui fir OS pentru fiecare I/O este o risipă de putere de calcul, deoarece, în realitate, firele de execuție nu fac o muncă utilă (de unde și termenul „întrerupere software”). Selectorul de sistem rezolvă această problemă, permițând programului utilizatorului să utilizeze resursele CPU mult mai economic.

Model de reactor I/O

Reactorul I/O acţionează ca un strat între selectorul de sistem şi codul utilizatorului. Principiul funcționării sale este descris de următoarea diagramă bloc:

Reactor I/O bare-C cu funcții complete

  • Permiteți-mi să vă reamintesc că un eveniment este o notificare că un anumit socket este capabil să efectueze o operație I/O neblocante.
  • Un handler de evenimente este o funcție numită de reactorul I/O atunci când este primit un eveniment, care apoi efectuează o operație I/O neblocante.

Este important de reținut că reactorul I/O este, prin definiție, cu un singur fir, dar nimic nu împiedică utilizarea conceptului într-un mediu cu mai multe fire într-un raport de 1 fir: 1 reactor, reciclând astfel toate nucleele CPU.

punerea în aplicare

Vom plasa interfața publică într-un fișier reactor.h, iar implementarea - în reactor.c. reactor.h va consta în următoarele anunţuri:

Afișați declarațiile în 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);

Structura reactorului I/O este formată din descriptor de fișier selector epoll и tabele de hash GHashTable, care mapează fiecare soclu CallbackData (structura unui handler de evenimente și un argument de utilizator pentru acesta).

Afișați Reactor și CallbackData

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

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

Vă rugăm să rețineți că am activat capacitatea de a gestiona tip incomplet conform indicelui. ÎN reactor.h declarăm structura reactor, și în reactor.c îl definim, împiedicând astfel utilizatorul să-și schimbe în mod explicit câmpurile. Acesta este unul dintre modele ascunderea datelor, care se încadrează succint în semantica C.

Funcții reactor_register, reactor_deregister и reactor_reregister actualizați lista de socket-uri de interes și de gestionare a evenimentelor corespunzătoare din selectorul de sistem și tabelul hash.

Afișați funcțiile de înregistrare

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

După ce reactorul I/O a interceptat evenimentul cu descriptorul fd, apelează handlerul de evenimente corespunzător, la care trece fd, mască de biți evenimente generate și un indicator de utilizator către void.

Afișează funcția 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;
}

Pentru a rezuma, lanțul de apeluri de funcții din codul utilizatorului va lua următoarea formă:

Reactor I/O bare-C cu funcții complete

Server cu un singur thread

Pentru a testa reactorul I/O sub sarcină mare, vom scrie un server web HTTP simplu care răspunde oricărei solicitări cu o imagine.

O referire rapidă la protocolul HTTP

HTTP - acesta este protocolul nivelul de aplicare, folosit în principal pentru interacțiunea server-browser.

HTTP poate fi utilizat cu ușurință transport protocol TCP, trimiterea și primirea mesajelor într-un format specificat specificație.

Format de solicitare

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

  • CRLF este o secvență de două caractere: r и n, separând prima linie a cererii, antete și date.
  • <КОМАНДА> - unul dintre CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Browserul va trimite o comandă serverului nostru GET, adică „Trimite-mi conținutul fișierului”.
  • <URI> - identificator uniform de resursă. De exemplu, dacă URI = /index.html, apoi clientul solicită pagina principală a site-ului.
  • <ВЕРСИЯ HTTP> — versiunea protocolului HTTP în format HTTP/X.Y. Cea mai des folosită versiune astăzi este HTTP/1.1.
  • <ЗАГОЛОВОК N> este o pereche cheie-valoare în format <КЛЮЧ>: <ЗНАЧЕНИЕ>, trimis la server pentru analize suplimentare.
  • <ДАННЫЕ> — datele necesare serverului pentru efectuarea operațiunii. Adesea este simplu JSON sau orice alt format.

Format de răspuns

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

  • <КОД СТАТУСА> este un număr care reprezintă rezultatul operației. Serverul nostru va returna întotdeauna starea 200 (operație reușită).
  • <ОПИСАНИЕ СТАТУСА> — reprezentarea în șir a codului de stare. Pentru codul de stare 200 acesta este OK.
  • <ЗАГОЛОВОК N> — antet cu același format ca și în cerere. Vom returna titlurile Content-Length (dimensiunea fișierului) și Content-Type: text/html (tip de date returnate).
  • <ДАННЫЕ> — datele solicitate de utilizator. În cazul nostru, aceasta este calea către imaginea în HTML.

fișier http_server.c (server cu un singur thread) include fișier common.h, care conține următoarele prototipuri de funcție:

Afișați prototipuri de funcție în comun.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);

Este descrisă și macro-ul funcțional SAFE_CALL() iar funcția este definită fail(). Macro-ul compară valoarea expresiei cu eroarea și, dacă condiția este adevărată, apelează funcția fail():

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

Funcție fail() imprimă argumentele transmise la terminal (cum ar fi printf()) și încheie programul cu codul 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);
}

Funcție new_server() returnează descriptorul de fișier al socket-ului „server” creat de apelurile de sistem socket(), bind() и listen() și capabil să accepte conexiuni de intrare într-un mod neblocant.

Afișează funcția 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;
}

  • Rețineți că socket-ul este creat inițial în modul neblocant folosind steag SOCK_NONBLOCKastfel încât în ​​funcţie on_accept() (citește mai mult) apel de sistem accept() nu a oprit execuția firului.
  • Dacă reuse_port este egal cu true, atunci această funcție va configura soclul cu opțiunea SO_REUSEPORT prin setsockopt()pentru a utiliza același port într-un mediu cu mai multe fire (consultați secțiunea „Server cu mai multe fire”).

Organizatorul evenimentului on_accept() apelat după ce sistemul de operare generează un eveniment EPOLLIN, în acest caz însemnând că noua conexiune poate fi acceptată. on_accept() acceptă o nouă conexiune, o comută în modul fără blocare și se înregistrează cu un handler de evenimente on_recv() într-un reactor I/O.

Afișează funcția 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);
}

Organizatorul evenimentului on_recv() apelat după ce sistemul de operare generează un eveniment EPOLLIN, în acest caz însemnând că legătura înregistrată on_accept(), gata să primească date.

on_recv() citește datele din conexiune până când cererea HTTP este primită complet, apoi înregistrează un handler on_send() pentru a trimite un răspuns HTTP. Dacă clientul întrerupe conexiunea, socket-ul este anulat și închis folosind close().

Afișați funcția 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);
    }
}

Organizatorul evenimentului on_send() apelat după ce sistemul de operare generează un eveniment EPOLLOUT, adică conexiunea înregistrată on_recv(), gata să trimită date. Această funcție trimite clientului un răspuns HTTP care conține HTML cu o imagine și apoi schimbă din nou handlerul de evenimente on_recv().

Afișează funcția 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);
}

Și în sfârșit, în dosar http_server.c, în funcțiune main() creăm un reactor I/O folosind reactor_new(), creați un socket de server și înregistrați-l, porniți reactorul folosind reactor_run() pentru exact un minut, apoi eliberăm resurse și ieșim din program.

Afișați 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);
}

Să verificăm dacă totul funcționează conform așteptărilor. Compilarea (chmod a+x compile.sh && ./compile.sh în rădăcina proiectului) și lansați serverul auto-scris, deschideți http://127.0.0.1:18470 în browser și vedem la ce ne așteptam:

Reactor I/O bare-C cu funcții complete

Măsurarea performanței

Arată specificațiile mașinii mele

$ 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

Să măsurăm performanța unui server cu un singur thread. Să deschidem două terminale: într-unul vom rula ./http_server, într-un alt - lucrare. După un minut, următoarele statistici vor fi afișate în al doilea terminal:

$ 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

Serverul nostru cu un singur thread a fost capabil să proceseze peste 11 milioane de solicitări pe minut provenite din 100 de conexiuni. Nu este un rezultat rău, dar poate fi îmbunătățit?

Server cu mai multe fire

După cum sa menționat mai sus, reactorul I/O poate fi creat în fire separate, utilizând astfel toate nucleele CPU. Să punem în practică această abordare:

Afișați 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);
    }
}

Acum fiecare fir deține al lui reactor:

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

Vă rugăm să rețineți că argumentul funcției new_server() avocați true. Aceasta înseamnă că atribuim opțiunea socket-ului serverului SO_REUSEPORTpentru a-l folosi într-un mediu cu mai multe fire. Puteți citi mai multe detalii aici.

A doua cursă

Acum să măsurăm performanța unui server cu mai multe fire:

$ 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

Numărul de solicitări procesate în 1 minut a crescut de ~3.28 ori! Dar am fost cu doar ~XNUMX milioane mai puțin de numărul rotund, așa că hai să încercăm să remediam asta.

Mai întâi să ne uităm la statisticile generate perfect:

$ 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

Folosind CPU afinitate, compilatie cu -march=native, PG, o creștere a numărului de accesări ascunzătoare, crește MAX_EVENTS si foloseste EPOLLET nu a dat o creștere semnificativă a performanței. Dar ce se întâmplă dacă creșteți numărul de conexiuni simultane?

Statistici pentru 352 de conexiuni simultane:

$ 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

S-a obținut rezultatul dorit și, odată cu acesta, un grafic interesant care arată dependența numărului de cereri procesate în 1 minut de numărul de conexiuni:

Reactor I/O bare-C cu funcții complete

Vedem că după câteva sute de conexiuni, numărul de solicitări procesate pentru ambele servere scade brusc (în versiunea multi-threaded acest lucru este mai vizibil). Este aceasta legată de implementarea stivei Linux TCP/IP? Simțiți-vă liber să scrieți ipotezele dvs. despre acest comportament al graficului și optimizările pentru opțiunile cu mai multe fire și cu un singur fir în comentarii.

Ca remarcat în comentarii, acest test de performanță nu arată comportamentul reactorului I/O sub încărcări reale, deoarece aproape întotdeauna serverul interacționează cu baza de date, scoate jurnalele, folosește criptografie cu TLS etc., în urma cărora sarcina devine neuniformă (dinamică). Testele împreună cu componente terțe vor fi efectuate în articolul despre proactor I/O.

Dezavantajele reactorului I/O

Trebuie să înțelegeți că reactorul I/O nu este lipsit de dezavantaje, și anume:

  • Utilizarea unui reactor I/O într-un mediu cu mai multe fire este oarecum mai dificilă, deoarece va trebui să gestionați manual fluxurile.
  • Practica arată că, în majoritatea cazurilor, sarcina este neuniformă, ceea ce poate duce la înregistrarea unui fir în timp ce altul este ocupat cu lucru.
  • Dacă un handler de evenimente blochează un fir, selectorul de sistem însuși se va bloca și el, ceea ce poate duce la erori greu de găsit.

Rezolvă aceste probleme Proactor I/O, care are adesea un planificator care distribuie uniform sarcina unui grup de fire și are, de asemenea, un API mai convenabil. Vom vorbi despre asta mai târziu, în celălalt articol al meu.

Concluzie

Aici s-a încheiat călătoria noastră de la teorie direct la evacuarea profilelor.

Nu ar trebui să stați asupra acestui lucru, deoarece există multe alte abordări la fel de interesante pentru a scrie software de rețea cu diferite niveluri de confort și viteză. Interesante, după părerea mea, link-urile sunt prezentate mai jos.

Până data viitoare!

Proiecte interesante

Ce altceva ar trebui să citesc?

Sursa: www.habr.com

Adauga un comentariu