Bare-C I/O reaktor s punim značajkama

Bare-C I/O reaktor s punim značajkama

Uvod

I/O reaktor (s jednim navojem petlja događaja) je obrazac za pisanje visokoopterećenog softvera, koji se koristi u mnogim popularnim rješenjima:

U ovom ćemo članku pogledati sitnice i nedostatke I/O reaktora i kako funkcionira, napisati implementaciju u manje od 200 redaka koda i napraviti jednostavan HTTP poslužitelj koji obrađuje preko 40 milijuna zahtjeva/min.

predgovor

  • Članak je napisan kako bi pomogao razumjeti funkcioniranje I/O reaktora, a time i razumjeti rizike pri njegovoj uporabi.
  • Za razumijevanje članka potrebno je poznavanje osnova. C jezik i nešto iskustva u razvoju mrežnih aplikacija.
  • Sav kod je napisan u C jeziku strogo prema (oprez: dugačak PDF) prema standardu C11 za Linux i dostupan na GitHub.

Zašto to učiniti?

S rastućom popularnošću Interneta, web poslužitelji su počeli istovremeno upravljati velikim brojem veza, pa su se isprobala dva pristupa: blokiranje I/O na velikom broju OS niti i neblokirajući I/O u kombinaciji s sustav obavješćivanja o događajima, koji se naziva i "selektor sustava" (epoll/kqueue/IOCP/itd).

Prvi pristup uključivao je stvaranje nove OS niti za svaku dolaznu vezu. Nedostatak mu je slaba skalabilnost: operativni sustav morat će implementirati mnoge prijelazi konteksta и sistemske pozive. To su skupe operacije i mogu dovesti do nedostatka slobodnog RAM-a s impresivnim brojem veza.

Izmijenjena verzija ističe fiksni broj niti (skup niti), čime se sprječava sustav da prekine izvršenje, ali u isto vrijeme predstavlja novi problem: ako je skup niti trenutno blokiran dugim operacijama čitanja, tada druge utičnice koje već mogu primati podatke neće moći učini tako.

Drugi pristup koristi sustav obavješćivanja o događajima (selektor sustava) koji pruža OS. Ovaj članak govori o najčešćem tipu selektora sustava, koji se temelji na upozorenjima (događajima, obavijestima) o spremnosti za I/O operacije, a ne na obavijesti o njihovom završetku. Pojednostavljeni primjer njegove upotrebe može se prikazati sljedećim blok dijagramom:

Bare-C I/O reaktor s punim značajkama

Razlika između ovih pristupa je sljedeća:

  • Blokiranje I/O operacija obustaviti protok korisnika dodok OS ne bude ispravan defragmentira dolazni IP paketi u tok bajtova (TCP, primanje podataka) ili neće biti dovoljno dostupnog prostora u internim međuspremnicima za pisanje za naknadno slanje putem NIC (slanje podataka).
  • Birač sustava tijekom vremena obavještava program da OS već defragmentirani IP paketi (TCP, prijem podataka) ili dovoljno prostora u internim međuspremnicima za pisanje već dostupno (slanje podataka).

Ukratko, rezerviranje OS niti za svaki I/O je gubitak računalne snage, jer u stvarnosti niti ne obavljaju koristan posao (otuda i izraz "softverski prekid"). Selektor sustava rješava ovaj problem, dopuštajući korisničkom programu da mnogo ekonomičnije koristi CPU resurse.

Model I/O reaktora

I/O reaktor djeluje kao sloj između selektora sustava i korisničkog koda. Princip njegovog rada opisan je sljedećim blok dijagramom:

Bare-C I/O reaktor s punim značajkama

  • Dopustite mi da vas podsjetim da je događaj obavijest da određena utičnica može izvesti neblokirajuću I/O operaciju.
  • Rukovatelj događajem je funkcija koju poziva I/O reaktor kada se primi događaj, a koja zatim izvodi neblokirajuću I/O operaciju.

Važno je napomenuti da je I/O reaktor po definiciji jednonitni, ali ništa ne sprječava da se koncept koristi u višenitnom okruženju u omjeru 1 nit: 1 reaktor, čime se recikliraju sve CPU jezgre.

Provedba

Smjestit ćemo javno sučelje u datoteku reactor.h, a provedba - u reactor.c. reactor.h sastojat će se od sljedećih najava:

Prikaži deklaracije u 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 reaktora sastoji se od deskriptor datoteke selektor epoll и hash tablice GHashTable, koji preslikava svaku utičnicu u CallbackData (struktura rukovatelja događajem i korisnički argument za njega).

Prikaži reaktor i povratne podatke

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

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

Imajte na umu da smo omogućili mogućnost rukovanja nepotpuni tip prema indeksu. U reactor.h deklariramo strukturu reactor, i u reactor.c mi ga definiramo, čime sprječavamo korisnika da eksplicitno mijenja njegova polja. Ovo je jedan od obrazaca skrivanje podataka, koji se sažeto uklapa u C semantiku.

Funkcije reactor_register, reactor_deregister и reactor_reregister ažurirajte popis utičnica od interesa i odgovarajuće rukovatelje događajima u selektoru sustava i tablici raspršivanja.

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

Nakon što je I/O reaktor presreo događaj s deskriptorom fd, poziva odgovarajući rukovatelj događajima, kojemu prolazi fd, bitna maska generirani događaji i korisnički pokazivač na void.

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

Ukratko, lanac poziva funkcija u korisničkom kodu poprimit će sljedeći oblik:

Bare-C I/O reaktor s punim značajkama

Poslužitelj s jednom niti

Kako bismo testirali I/O reaktor pod velikim opterećenjem, napisat ćemo jednostavan HTTP web poslužitelj koji na svaki zahtjev odgovara slikom.

Brzi pregled HTTP protokola

HTTP - ovo je protokol razina primjene, prvenstveno se koristi za interakciju između poslužitelja i preglednika.

HTTP se može jednostavno koristiti preko prijevoz protokol TCP, slanje i primanje poruka u navedenom formatu specifikacija.

Format zahtjeva

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

  • CRLF je niz od dva znaka: r и n, odvajajući prvi redak zahtjeva, zaglavlja i podatke.
  • <КОМАНДА> - jedan od CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Preglednik će poslati naredbu našem poslužitelju GET, što znači "Pošalji mi sadržaj datoteke."
  • <URI> - jedinstveni identifikator izvora. Na primjer, ako je URI = /index.html, tada klijent zahtijeva glavnu stranicu stranice.
  • <ВЕРСИЯ HTTP> — verzija HTTP protokola u formatu HTTP/X.Y. Danas se najčešće koristi verzija HTTP/1.1.
  • <ЗАГОЛОВОК N> je par ključ-vrijednost u formatu <КЛЮЧ>: <ЗНАЧЕНИЕ>, poslan poslužitelju na daljnju analizu.
  • <ДАННЫЕ> — podaci potrebni poslužitelju za izvođenje operacije. Često je jednostavno JSON ili bilo koji drugi format.

Format odgovora

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

  • <КОД СТАТУСА> je broj koji predstavlja rezultat operacije. Naš poslužitelj će uvijek vratiti status 200 (uspješna operacija).
  • <ОПИСАНИЕ СТАТУСА> — nizovni prikaz statusnog koda. Za statusni kod 200 ovo je OK.
  • <ЗАГОЛОВОК N> — zaglavlje istog formata kao u zahtjevu. Vratit ćemo naslove Content-Length (veličina datoteke) i Content-Type: text/html (vrsta povratnog podatka).
  • <ДАННЫЕ> — podaci koje korisnik traži. U našem slučaju, ovo je put do slike HTML.

datoteka http_server.c (poslužitelj s jednom niti) uključuje datoteku common.h, koji sadrži sljedeće prototipove funkcija:

Prikaži zajedničke prototipove funkcija.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);

Također je opisan funkcionalni makro SAFE_CALL() a funkcija je definirana fail(). Makronaredba uspoređuje vrijednost izraza s pogreškom i ako je uvjet istinit, poziva funkciju fail():

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

Funkcija fail() ispisuje proslijeđene argumente terminalu (kao printf()) i prekida program s kodom 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() vraća deskriptor datoteke "poslužiteljskog" utičnice kreiranog sistemskim pozivima socket(), bind() и listen() i sposoban prihvatiti dolazne veze u načinu rada bez blokiranja.

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

  • Imajte na umu da se utičnica inicijalno stvara u neblokirajućem načinu rada pomoću zastavice SOCK_NONBLOCKtako da u funkciji on_accept() (pročitaj više) sistemski poziv accept() nije zaustavio izvođenje niti.
  • Ako reuse_port jednako je true, tada će ova funkcija konfigurirati utičnicu s opcijom SO_REUSEPORT preko setsockopt()za korištenje istog porta u višenitnom okruženju (pogledajte odjeljak “Višenitni poslužitelj”).

Rukovatelj događajima on_accept() poziva nakon što OS generira događaj EPOLLIN, što u ovom slučaju znači da se nova veza može prihvatiti. on_accept() prihvaća novu vezu, prebacuje je u način rada bez blokiranja i registrira se u rukovatelju događajima on_recv() u I/O reaktoru.

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

Rukovatelj događajima on_recv() poziva nakon što OS generira događaj EPOLLIN, u ovom slučaju znači da je veza registrirana on_accept(), spreman za primanje podataka.

on_recv() čita podatke s veze sve dok se HTTP zahtjev u potpunosti ne primi, zatim registrira rukovatelja on_send() za slanje HTTP odgovora. Ako klijent prekine vezu, utičnica se deregistrira i zatvara korištenjem close().

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

Rukovatelj događajima on_send() poziva nakon što OS generira događaj EPOLLOUT, što znači da je veza registrirana on_recv(), spreman za slanje podataka. Ova funkcija šalje HTTP odgovor koji sadrži HTML sa slikom klijentu i zatim mijenja rukovatelj događajima natrag na on_recv().

Prikaži funkciju 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 na kraju, u kartoteci http_server.c, u funkciji main() stvaramo I/O reaktor pomoću reactor_new(), kreirajte poslužiteljsku utičnicu i registrirajte je, pokrenite reaktor pomoću reactor_run() točno jednu minutu, a zatim oslobađamo resurse i izlazimo iz programa.

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

Provjerimo radi li sve prema očekivanjima. Sastavljanje (chmod a+x compile.sh && ./compile.sh u korijenu projekta) i pokrenite poslužitelj koji ste sami napisali, otvorite http://127.0.0.1:18470 u pregledniku i vidjeti što smo očekivali:

Bare-C I/O reaktor s punim značajkama

Mjerenje učinkovitosti

Prikaži specifikacije mog automobila

$ 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

Izmjerimo performanse jednonitnog poslužitelja. Otvorimo dva terminala: u jednom ćemo raditi ./http_server, u drugom - rad. Nakon jedne minute, sljedeća statistika bit će prikazana na drugom terminalu:

$ 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š poslužitelj s jednom niti uspio je obraditi više od 11 milijuna zahtjeva u minuti koji potječu iz 100 veza. Nije loš rezultat, ali može li se poboljšati?

Višenitni poslužitelj

Kao što je gore spomenuto, I/O reaktor može se stvoriti u zasebnim nitima, pri čemu se koriste sve CPU jezgre. Provedimo ovaj pristup u 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);
    }
}

Sada svaka nit posjeduje svoje reaktor:

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

Imajte na umu da argument funkcije new_server() djela true. To znači da dodijelimo opciju serverskom utičnici SO_REUSEPORTkoristiti ga u višenitnom okruženju. Možete pročitati više ovdje.

Druga vožnja

Izmjerimo sada performanse poslužitelja s više niti:

$ 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

Broj zahtjeva obrađenih u 1 minuti porastao je za ~3.28 puta! Ali nedostajalo nam je samo ~XNUMX milijuna do okruglog broja, pa pokušajmo to popraviti.

Prvo pogledajmo generirane statistike za izvedbu:

$ 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

Korištenje CPU Affinity, kompilacija sa -march=native, PGO, povećanje broja pogodaka unovčiti, povećati MAX_EVENTS i koristiti EPOLLET nije dalo značajno povećanje performansi. Ali što se događa ako povećate broj istodobnih veza?

Statistika za 352 istodobne veze:

$ 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

Dobiven je željeni rezultat, a time i zanimljiv graf koji prikazuje ovisnost broja obrađenih zahtjeva u 1 minuti o broju veza:

Bare-C I/O reaktor s punim značajkama

Vidimo da nakon par stotina konekcija broj obrađenih zahtjeva za oba poslužitelja naglo pada (u multi-thread verziji to je primjetnije). Je li to povezano s implementacijom Linux TCP/IP stacka? U komentarima slobodno napišite svoje pretpostavke o ovakvom ponašanju grafikona i optimizacijama za višenitne i jednonitne opcije.

Kao obilježiti u komentarima, ovaj test performansi ne pokazuje ponašanje I/O reaktora pod stvarnim opterećenjima, jer gotovo uvijek poslužitelj komunicira s bazom podataka, ispisuje zapise, koristi kriptografiju s TLS itd. uslijed čega opterećenje postaje nejednoliko (dinamičko). Testovi zajedno s komponentama trećih strana bit će provedeni u članku o I/O proactoru.

Nedostaci I/O reaktora

Morate razumjeti da I/O reaktor nije bez svojih nedostataka, naime:

  • Korištenje I/O reaktora u multi-threaded okruženju je nešto teže, jer morat ćete ručno upravljati tijekovima.
  • Praksa pokazuje da je u većini slučajeva opterećenje neujednačeno, što može dovesti do zapisivanja jedne niti dok je druga zauzeta poslom.
  • Ako jedan rukovatelj događajem blokira nit, tada će i sam selektor sustava također blokirati, što može dovesti do grešaka koje je teško pronaći.

Rješava ove probleme I/O proaktor, koji često ima planer koji ravnomjerno raspoređuje opterećenje na skup niti, a također ima praktičniji API. O tome ćemo kasnije, u mom drugom članku.

Zaključak

Ovdje je naš put od teorije ravno do profilnog ispuha došao kraju.

Ne biste se trebali zadržavati na ovome jer postoje mnogi drugi jednako zanimljivi pristupi pisanju mrežnog softvera s različitim razinama pogodnosti i brzine. Zanimljivi, po mom mišljenju, linkovi su navedeni ispod.

Do sljedećeg puta!

Zanimljivi projekti

Što još trebam pročitati?

Izvor: www.habr.com

Dodajte komentar