Popolnoma delujoč V/I reaktor na golem C

Popolnoma delujoč V/I reaktor na golem C

Predstavitev

I/O reaktor (enojni navoj zanka dogodkov) je vzorec za pisanje visoko obremenjene programske opreme, ki se uporablja v številnih priljubljenih rešitvah:

V tem članku si bomo ogledali podrobnosti V/I reaktorja in kako deluje, napisali izvedbo v manj kot 200 vrsticah kode in naredili preprost strežnik HTTP, ki obdela več kot 40 milijonov zahtev/min.

Predgovor

  • Članek je bil napisan za pomoč pri razumevanju delovanja I/O reaktorja in s tem razumevanju tveganj pri njegovi uporabi.
  • Za razumevanje članka je potrebno poznavanje osnov. jezik C in nekaj izkušenj z razvojem omrežnih aplikacij.
  • Vsa koda je napisana v jeziku C strogo v skladu z (pozor: dolg PDF) po standardu C11 za Linux in na voljo na GitHub.

Zakaj to?

Z naraščajočo priljubljenostjo interneta so spletni strežniki začeli obravnavati veliko število povezav hkrati, zato sta bila preizkušena dva pristopa: blokiranje V/I na velikem številu niti OS in neblokiranje V/I v kombinaciji z sistem za obveščanje o dogodkih, imenovan tudi "izbirnik sistema" (epoll/kqueue/IOCP/itd).

Prvi pristop je vključeval ustvarjanje nove niti OS za vsako dohodno povezavo. Njegova pomanjkljivost je slaba razširljivost: operacijski sistem bo moral implementirati veliko prehodi konteksta и sistemske klice. So drage operacije in lahko povzročijo pomanjkanje prostega RAM-a z impresivnim številom povezav.

Spremenjena različica poudarja fiksno število niti (področje niti), s čimer sistemu preprečite prekinitev izvajanja, a hkrati uvedete novo težavo: če je področje niti trenutno blokirano z operacijami dolgega branja, potem druge vtičnice, ki že lahko sprejemajo podatke, ne bodo mogle naredi tako.

Drugi pristop uporablja sistem obveščanja o dogodkih (izbirnik sistema), ki ga zagotavlja OS. Ta članek obravnava najpogostejšo vrsto sistemskega izbirnika, ki temelji na opozorilih (dogodkih, obvestilih) o pripravljenosti na V/I operacije, ne pa na obvestila o njihovem zaključku. Poenostavljen primer njegove uporabe je lahko predstavljen z naslednjim blokovnim diagramom:

Popolnoma delujoč V/I reaktor na golem C

Razlika med temi pristopi je naslednja:

  • Blokiranje V/I operacij prekiniti tok uporabnikov doklerdokler OS ni pravilen defragmentira dohodni IP paketi v tok bajtov (TCP, prejemanje podatkov) ali pa v notranjih zapisovalnih medpomnilnikih ne bo dovolj prostora za nadaljnje pošiljanje prek NIC (pošiljanje podatkov).
  • Izbirnik sistema čez čas obvesti program, da OS že defragmentirani paketi IP (TCP, sprejem podatkov) ali dovolj prostora v notranjih pisalnih medpomnilnikih že na voljo (pošiljanje podatkov).

Če povzamemo, je rezerviranje niti OS za vsak V/I izguba računalniške moči, ker v resnici niti ne opravljajo koristnega dela (od tod izraz "programska prekinitev"). Sistemski izbirnik rešuje to težavo in uporabniškemu programu omogoča veliko bolj ekonomično uporabo virov procesorja.

Model I/O reaktorja

V/I reaktor deluje kot plast med sistemskim izbirnikom in uporabniško kodo. Načelo njegovega delovanja opisuje naslednji blokovni diagram:

Popolnoma delujoč V/I reaktor na golem C

  • Naj vas spomnim, da je dogodek obvestilo, da lahko določena vtičnica izvede neblokirno V/I operacijo.
  • Upravljalnik dogodkov je funkcija, ki jo pokliče V/I reaktor, ko prejme dogodek, ki nato izvede neblokirno V/I operacijo.

Pomembno je omeniti, da je I/O reaktor po definiciji enoniten, vendar nič ne preprečuje uporabe tega koncepta v večnitnem okolju v razmerju 1 nit: 1 reaktor, s čimer se reciklirajo vsa jedra CPE.

Реализация

Javni vmesnik bomo postavili v datoteko reactor.h, in izvajanje - v reactor.c. reactor.h bo sestavljen iz naslednjih objav:

Prikaži deklaracije v 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);

Struktura I/O reaktorja je sestavljena iz deskriptor datoteke selektor epoll и zgoščene tabele GHashTable, ki preslika vsako vtičnico v CallbackData (struktura upravljalnika dogodkov in uporabniški argument zanj).

Prikaži podatke o reaktorju in povratnem klicu

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

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

Upoštevajte, da smo omogočili možnost upravljanja nepopolni tip glede na indeks. IN reactor.h deklariramo strukturo reactor, in v reactor.c definiramo in s tem preprečimo, da bi uporabnik izrecno spreminjal njegova polja. To je eden od vzorcev skrivanje podatkov, ki se jedrnato ujema s semantiko C.

Funkcije reactor_register, reactor_deregister и reactor_reregister posodobite seznam vtičnic, ki vas zanimajo, in ustrezne obdelovalce dogodkov v sistemskem izbirniku in zgoščeni tabeli.

Prikaži funkcije registracije

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

Ko V/I reaktor prestreže dogodek z deskriptorjem fd, pokliče ustrezen upravljalnik dogodkov, h kateremu posreduje fd, bitna maska generirani dogodki in uporabniški kazalec na void.

Prikaži funkcijo 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;
}

Če povzamemo, bo veriga funkcijskih klicev v uporabniški kodi imela naslednjo obliko:

Popolnoma delujoč V/I reaktor na golem C

Strežnik z eno nitjo

Da bi testirali I/O reaktor pod visoko obremenitvijo, bomo napisali preprost spletni strežnik HTTP, ki se na vsako zahtevo odzove s sliko.

Hiter sklic na protokol HTTP

HTTP - to je protokol ravni uporabe, ki se uporablja predvsem za interakcijo med strežnikom in brskalnikom.

HTTP je mogoče preprosto uporabiti transport protokol TCP, pošiljanje in prejemanje sporočil v določenem formatu specifikacija.

Oblika zahteve

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

  • CRLF je zaporedje dveh znakov: r и n, ki ločuje prvo vrstico zahteve, glave in podatke.
  • <КОМАНДА> - eden od CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Brskalnik bo poslal ukaz našemu strežniku GET, kar pomeni "Pošlji mi vsebino datoteke."
  • <URI> - enotni identifikator vira. Na primer, če je URI = /index.html, potem stranka zahteva glavno stran spletnega mesta.
  • <ВЕРСИЯ HTTP> — različica protokola HTTP v formatu HTTP/X.Y. Danes najpogosteje uporabljena različica je HTTP/1.1.
  • <ЗАГОЛОВОК N> je par ključ-vrednost v formatu <КЛЮЧ>: <ЗНАЧЕНИЕ>, poslana strežniku za nadaljnjo analizo.
  • <ДАННЫЕ> — podatki, ki jih zahteva strežnik za izvedbo operacije. Pogosto je preprosto JSON ali katero koli drugo obliko.

Oblika odgovora

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

  • <КОД СТАТУСА> je število, ki predstavlja rezultat operacije. Naš strežnik bo vedno vrnil status 200 (uspešno delovanje).
  • <ОПИСАНИЕ СТАТУСА> — nizovna predstavitev statusne kode. Za statusno kodo 200 je to OK.
  • <ЗАГОЛОВОК N> — glava istega formata kot v zahtevku. Naslove bomo vrnili Content-Length (velikost datoteke) in Content-Type: text/html (vrsta vrnjenega podatka).
  • <ДАННЫЕ> — podatki, ki jih zahteva uporabnik. V našem primeru je to pot do slike v HTML.

datoteka http_server.c (strežnik z eno nitjo) vključuje datoteko common.h, ki vsebuje naslednje prototipe funkcij:

Prikaži prototipe funkcij v common.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);

Opisan je tudi funkcionalni makro SAFE_CALL() in funkcija je definirana fail(). Makro primerja vrednost izraza z napako in, če je pogoj resničen, pokliče funkcijo fail():

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

Funkcija fail() natisne posredovane argumente terminalu (npr printf()) in prekine program s kodo 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);
}

Funkcija new_server() vrne deskriptor datoteke "strežniške" vtičnice, ustvarjene s sistemskimi klici socket(), bind() и listen() in lahko sprejema dohodne povezave v načinu brez blokiranja.

Prikaži funkcijo 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;
}

  • Upoštevajte, da je vtičnica prvotno ustvarjena v načinu brez blokiranja z uporabo zastavice SOCK_NONBLOCKtako da v funkciji on_accept() (preberi več) sistemski klic accept() ni zaustavil izvajanja niti.
  • če reuse_port je enako true, potem bo ta funkcija konfigurirala vtičnico z možnostjo SO_REUSEPORT skozi setsockopt()za uporabo istih vrat v okolju z več nitmi (glejte razdelek »Strežnik z več nitmi«).

Obravnavalec dogodkov on_accept() poklican po tem, ko OS ustvari dogodek EPOLLIN, kar v tem primeru pomeni, da je nova povezava lahko sprejeta. on_accept() sprejme novo povezavo, jo preklopi v način brez blokiranja in se registrira z obdelovalcem dogodkov on_recv() v I/O reaktorju.

Pokaži funkcijo 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);
}

Obravnavalec dogodkov on_recv() poklican po tem, ko OS ustvari dogodek EPOLLIN, kar v tem primeru pomeni, da je povezava registrirana on_accept(), pripravljen na sprejem podatkov.

on_recv() bere podatke iz povezave, dokler ni v celoti sprejeta zahteva HTTP, nato registrira obdelovalca on_send() za pošiljanje odziva HTTP. Če odjemalec prekine povezavo, se vtičnica odjavi in ​​zapre z uporabo close().

Pokaži funkcijo 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);
    }
}

Obravnavalec dogodkov on_send() poklican po tem, ko OS ustvari dogodek EPOLLOUT, kar pomeni, da je povezava registrirana on_recv(), pripravljen za pošiljanje podatkov. Ta funkcija odjemalcu pošlje odziv HTTP, ki vsebuje HTML s sliko, nato pa spremeni obravnavo dogodkov nazaj v on_recv().

Prikaži funkcijo 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);
}

In končno, v datoteki http_server.c, v funkciji main() ustvarimo I/O reaktor z uporabo reactor_new(), ustvarite strežniško vtičnico in jo registrirajte, zaženite reaktor z uporabo reactor_run() natanko eno minuto, nato sprostimo sredstva in zapustimo program.

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

Preverimo, ali vse deluje po pričakovanjih. Prevajanje (chmod a+x compile.sh && ./compile.sh v korenu projekta) in zaženite samonapisani strežnik, odprite http://127.0.0.1:18470 v brskalniku in si oglejte, kaj smo pričakovali:

Popolnoma delujoč V/I reaktor na golem C

Merjenje uspešnosti

Pokaži specifikacije mojega avtomobila

$ 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

Izmerimo zmogljivost enonitnega strežnika. Odprimo dva terminala: v enem bomo zagnali ./http_server, v drugačni - delo. Po minuti se na drugem terminalu prikaže naslednja 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

Naš strežnik z eno nitjo je lahko obdelal več kot 11 milijonov zahtev na minuto, ki izvirajo iz 100 povezav. Ni slab rezultat, a ga je mogoče izboljšati?

Večnitni strežnik

Kot je bilo omenjeno zgoraj, je V/I reaktor mogoče ustvariti v ločenih nitih, s čimer se uporabljajo vsa jedra CPE. Uporabimo ta pristop v praksi:

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

Zdaj pa vsaka nit ima svojega reaktor:

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

Upoštevajte, da argument funkcije new_server() storitve true. To pomeni, da strežniškemu socketu dodelimo možnost SO_REUSEPORTza uporabo v večnitnem okolju. Več podrobnosti si lahko preberete tukaj.

Drugi tek

Zdaj pa izmerimo zmogljivost večnitnega strežnika:

$ 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

Število zahtev, obdelanih v 1 minuti, se je povečalo za ~3.28-krat! Toda do okrogle številke nam je manjkala le ~XNUMX milijona, zato poskusimo to popraviti.

Najprej si oglejmo ustvarjeno statistiko 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

Uporaba CPE Affinity, kompilacija z -march=native, PGO, povečanje števila zadetkov gotovina, porast MAX_EVENTS in uporabo EPOLLET ni prineslo bistvenega povečanja učinkovitosti. Toda kaj se zgodi, če povečate število hkratnih povezav?

Statistika za 352 hkratnih povezav:

$ 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

Dobili smo želeni rezultat in s tem zanimiv graf, ki prikazuje odvisnost števila obdelanih zahtevkov v 1 minuti od števila povezav:

Popolnoma delujoč V/I reaktor na golem C

Vidimo, da po nekaj sto povezavah število obdelanih zahtev za oba strežnika močno upade (v večnitni različici je to bolj opazno). Je to povezano z implementacijo sklada TCP/IP v Linuxu? V komentarje lahko napišete svoje domneve o tem obnašanju grafa in optimizacijah za večnitne in enonitne možnosti.

Kot opozoriti v komentarjih ta preizkus zmogljivosti ne prikazuje obnašanja I/O reaktorja pod resničnimi obremenitvami, ker skoraj vedno strežnik komunicira z bazo podatkov, izpisuje dnevnike, uporablja kriptografijo z TLS itd., zaradi česar postane obremenitev neenakomerna (dinamična). Testi skupaj s komponentami tretjih oseb bodo izvedeni v članku o V/I proaktorju.

Slabosti V/I reaktorja

Morate razumeti, da V/I reaktor ni brez pomanjkljivosti, in sicer:

  • Uporaba V/I reaktorja v večnitnem okolju je nekoliko težja, ker boste morali ročno upravljati tokove.
  • Praksa kaže, da je v večini primerov obremenitev neenakomerna, kar lahko privede do beleženja ene niti, medtem ko je druga zaposlena z delom.
  • Če en obravnavalec dogodkov blokira nit, bo blokiral tudi sistemski izbirnik, kar lahko privede do napak, ki jih je težko najti.

Rešuje te težave V/I proaktor, ki ima pogosto razporejevalnik, ki enakomerno porazdeli obremenitev na skupino niti, ima pa tudi bolj priročen API. O tem bomo govorili kasneje, v mojem drugem članku.

Zaključek

Tu se je naše potovanje od teorije naravnost do profilnega izpuha končalo.

Ne bi se smeli zadrževati na tem, ker obstaja veliko drugih enako zanimivih pristopov k pisanju omrežne programske opreme z različnimi stopnjami priročnosti in hitrosti. Spodaj so po mojem mnenju zanimive povezave.

Se vidiva spet!

Zanimivi projekti

Kaj naj še preberem?

Vir: www.habr.com

Dodaj komentar