Potpuno opremljeni bare-C I/O reaktor

Potpuno opremljeni bare-C I/O reaktor

Uvod

I/O reaktor (jednonavojni petlja događaja) je obrazac za pisanje softvera visokog opterećenja, koji se koristi u mnogim popularnim rješenjima:

U ovom članku ćemo pogledati sve detalje I/O reaktora i kako on funkcionira, napisati implementaciju u manje od 200 linija koda i napraviti jednostavan HTTP serverski proces preko 40 miliona zahtjeva/min.

Predgovor

  • Članak je napisan kako bi pomogao u razumijevanju funkcionisanja I/O reaktora, a samim tim i razumijevanju rizika pri njegovom korištenju.
  • Za razumijevanje članka potrebno je poznavanje osnova. C jezik i određeno iskustvo u razvoju mrežnih aplikacija.
  • Sav kod je napisan u jeziku C striktno prema (oprez: dugačak PDF) prema standardu C11 za Linux i dostupno na GitHub.

Zašto je to neophodno?

Sa rastućom popularnošću Interneta, web serveri su počeli imati potrebu da istovremeno rukovode velikim brojem konekcija, te su stoga pokušana dva pristupa: blokiranje I/O na velikom broju OS niti i neblokiranje I/O u kombinaciji sa sistem za obavještavanje o događajima, koji se također naziva "selektor sistema" (epoll/kqueue/IOCP/etc).

Prvi pristup uključivao je kreiranje nove OS niti za svaku dolaznu vezu. Njegov nedostatak je loša skalabilnost: operativni sistem će morati implementirati mnoge tranzicije konteksta и sistemski pozivi. One su skupe operacije i mogu dovesti do nedostatka slobodne RAM memorije sa impresivnim brojem konekcija.

Izmijenjena verzija ističe fiksni broj niti (skup niti), čime se sprečava da sistem prekine izvršenje, ali u isto vreme uvodi novi problem: ako je skup niti trenutno blokiran dugim operacijama čitanja, onda drugi utičnici koji već mogu da primaju podatke neće moći da učiniti.

Drugi pristup koristi sistem obaveštavanja o događajima (selektor sistema) koji obezbeđuje OS. Ovaj članak govori o najčešćem tipu selektora sistema, zasnovanom na upozorenjima (događaji, obaveštenja) o spremnosti za I/O operacije, a ne na obavještenja o njihovom završetku. Pojednostavljeni primjer njegove upotrebe može se predstaviti sljedećim blok dijagramom:

Potpuno opremljeni bare-C I/O reaktor

Razlika između ovih pristupa je sljedeća:

  • Blokiranje I/O operacija suspendovati protok korisnika dodok OS ne bude ispravan defragmentira dolazni IP paketi u tok bajtova (TCP, primanje podataka) ili neće biti dovoljno slobodnog prostora u internim baferima za upisivanje za naknadno slanje putem NIŠTA (slanje podataka).
  • Sistemski birač prekovremeno obavještava program da OS već defragmentirani IP paketi (TCP, prijem podataka) ili dovoljno prostora u internim baferima za pisanje već dostupno (slanje podataka).

Da sumiramo, rezervisanje OS niti za svaki I/O je gubitak računarske snage, jer u stvarnosti, niti ne obavljaju koristan posao (otuda i termin "softverski prekid"). Sistemski birač rješava ovaj problem, dozvoljavajući korisničkom programu da koristi CPU resurse mnogo ekonomičnije.

I/O model reaktora

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

Potpuno opremljeni bare-C I/O reaktor

  • Dozvolite mi da vas podsjetim da je događaj obavijest da je određena utičnica u stanju da izvrši neblokirajuću I/O operaciju.
  • Rukovalac događajem je funkcija koju poziva I/O reaktor kada se primi događaj, koji 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.

Реализация

Javni interfejs ćemo postaviti u fajl reactor.h, a implementacija - 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 table GHashTable, koji mapira svaku utičnicu na CallbackData (struktura rukovaoca događaja i korisnički argument za to).

Prikaži reaktor i povratne podatke

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

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

Napominjemo da smo omogućili mogućnost rukovanja nepotpuni tip prema indeksu. IN reactor.h proglašavamo strukturu reactorand in reactor.c definišemo ga, čime sprečavamo korisnika da eksplicitno menja svoja polja. Ovo je jedan od obrazaca skrivanje podataka, što se sažeto uklapa u C semantiku.

Funkcije reactor_register, reactor_deregister и reactor_reregister ažurirati listu utičnica od interesa i odgovarajućih rukovalaca događajima u selektoru sistema i hash 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;
}

Nakon što je I/O reaktor presreo događaj deskriptorom fd, poziva odgovarajući obrađivač događaja na koji prolazi fd, bit 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;
}

Da rezimiramo, lanac poziva funkcija u korisničkom kodu imat će sljedeći oblik:

Potpuno opremljeni bare-C I/O reaktor

Server sa jednim navojem

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

Brza referenca na HTTP protokol

HTTP - ovo je protokol nivo aplikacije, prvenstveno se koristi za interakciju između servera i pretraživača.

HTTP se može lako koristiti preko transport protokol TCP, slanje i primanje poruka u specificiranom 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 red zahtjeva, zaglavlja i podatke.
  • <КОМАНДА> - jedan od CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Pretraživač će poslati naredbu našem serveru GET, što znači "Pošalji mi sadržaj datoteke."
  • <URI> - uniformni identifikator resursa. Na primjer, ako je URI = /index.html, tada klijent traži 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 <КЛЮЧ>: <ЗНАЧЕНИЕ>, poslat na server na dalju analizu.
  • <ДАННЫЕ> — podatke koje server traži da izvrši operaciju. Č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š server će uvijek vratiti status 200 (uspješan rad).
  • <ОПИСАНИЕ СТАТУСА> — string prikaz statusnog koda. Za statusni kod 200 ovo je OK.
  • <ЗАГОЛОВОК N> — zaglavlje istog formata kao u zahtjevu. Vratićemo naslove Content-Length (veličina datoteke) i Content-Type: text/html (povratni tip podataka).
  • <ДАННЫЕ> — podatke koje korisnik traži. U našem slučaju, ovo je put do slike u HTML.

fajl http_server.c (single threaded server) uključuje datoteku common.h, koji sadrži sljedeće prototipove funkcija:

Prikaži prototipove funkcija u 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 i funkcionalni makro SAFE_CALL() i funkcija je definirana fail(). Makro uspoređuje vrijednost izraza s greš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 završava program 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 "server" socketa kreiranog sistemskim pozivima socket(), bind() и listen() i može prihvatiti dolazne veze u neblokirajućem načinu.

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 je socket inicijalno kreiran u neblokirajućem načinu pomoću zastavice SOCK_NONBLOCKtako da u funkciji on_accept() (pročitajte više) sistemski poziv accept() nije zaustavio izvršavanje niti.
  • ako reuse_port je jednako true, tada će ova funkcija konfigurirati utičnicu s opcijom SO_REUSEPORT kroz setsockopt()za korištenje istog porta u okruženju s više niti (pogledajte odjeljak „Poslužitelj s više niti“).

Događaj Handler on_accept() poziva se nakon što OS generiše događaj EPOLLIN, u ovom slučaju znači da se nova veza može prihvatiti. on_accept() prihvata novu vezu, prebacuje je u neblokirajući način rada i registruje se kod rukovaoca događaja on_recv() u I/O reaktoru.

Prikaži on_accept() funkciju

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

Događaj Handler on_recv() poziva se nakon što OS generiše događaj EPOLLIN, u ovom slučaju znači da je veza registrirana on_accept(), spreman za primanje podataka.

on_recv() čita podatke iz veze sve dok HTTP zahtjev nije u potpunosti primljen, a zatim registruje rukovaoca on_send() da pošaljete HTTP odgovor. Ako klijent prekine vezu, utičnica se odjavljuje i zatvara pomoću 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);
    }
}

Događaj Handler on_send() poziva se nakon što OS generiše događaj EPOLLOUT, što znači da je veza registrovana on_recv(), spreman za slanje podataka. Ova funkcija klijentu šalje HTTP odgovor koji sadrži HTML sa slikom, a zatim vraća rukovaocu događaja on_recv().

Prikaži on_send() funkciju

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 konačno, u fajlu http_server.c, u funkciji main() kreiramo I/O reaktor koristeći reactor_new(), kreirajte serversku utičnicu i registrirajte je, pokrenite reaktor koristeći reactor_run() tačno jedan minut, 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 da li sve radi kako se očekuje. Prevođenje (chmod a+x compile.sh && ./compile.sh u korijenu projekta) i pokrenite samopisni server, otvorite http://127.0.0.1:18470 u pretraživaču i pogledajte šta smo očekivali:

Potpuno opremljeni bare-C I/O reaktor

Merenje performansi

Pokaž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

Hajde da izmerimo performanse jednonitnog servera. Otvorimo dva terminala: u jednom ćemo pokrenuti ./http_server, u drugačijem - wrk. Nakon minute, na drugom terminalu će se prikazati sljedeća 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š jednonitni server je bio u stanju da obradi preko 11 miliona zahteva u minuti koji potiču iz 100 konekcija. Nije loš rezultat, ali može li se poboljšati?

Višenitni server

Kao što je gore spomenuto, I/O reaktor se može kreirati u odvojenim nitima, koristeći na taj način sve CPU jezgre. Hajde da ovaj pristup primenimo 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 poseduje svoje reaktor:

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

Imajte na umu da argument funkcije new_server() advokati true. To znači da dodjeljujemo opciju serverskoj utičnici SO_REUSEPORTda ga koristite u okruženju sa više niti. Možete pročitati više detalja ovdje.

Druga vožnja

Sada izmjerimo performanse višenitnog servera:

$ 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 obrađenih zahtjeva u 1 minuti porastao je za ~3.28 puta! Ali nedostajalo nam je samo ~XNUMX miliona do okruglog broja, pa hajde da to popravimo.

Prvo pogledajmo generisanu statistiku parf:

$ 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 keš, povećati MAX_EVENTS i koristiti EPOLLET nije dalo značajno povećanje performansi. Ali šta se dešava ako povećate broj istovremenih veza?

Statistika za 352 istovremene 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

Dobijen je željeni rezultat, a sa njim i zanimljiv grafikon koji pokazuje ovisnost broja obrađenih zahtjeva u 1 minuti od broja veza:

Potpuno opremljeni bare-C I/O reaktor

Vidimo da nakon par stotina konekcija, broj obrađenih zahtjeva za oba servera naglo opada (u verziji s više niti to je primjetnije). Da li je ovo povezano sa implementacijom Linux TCP/IP steka? Slobodno napišite svoje pretpostavke o ovakvom ponašanju grafa i optimizacijama za višenitne i jednonitne opcije u komentarima.

Kako zabeleženo u komentarima, ovaj test performansi ne pokazuje ponašanje I/O reaktora pod stvarnim opterećenjima, jer skoro uvijek server komunicira sa bazom podataka, izlazi dnevnike, koristi kriptografiju sa TLS itd., zbog čega opterećenje postaje neujednačeno (dinamičko). Testovi zajedno sa komponentama trećih strana biće sprovedeni u članku o I/O proaktoru.

Nedostaci I/O reaktora

Morate shvatiti da I/O reaktor nije bez svojih nedostataka, a to su:

  • Korištenje I/O reaktora u višenitnom okruženju je nešto teže, jer morat ćete ručno upravljati tokovima.
  • Praksa pokazuje da je u većini slučajeva opterećenje neujednačeno, što može dovesti do evidentiranja jedne niti dok je druga zauzeta poslom.
  • Ako jedan rukovalac događaja blokira nit, tada će i sam sistemski birač blokirati, što može dovesti do teško dostupnih grešaka.

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

zaključak

Ovo je mjesto gdje je naš put od teorije direktno do izduvnog sistema profilera došao do kraja.

Ne biste se trebali zadržavati na tome, jer postoji mnogo drugih jednako zanimljivih pristupa pisanju mrežnog softvera s različitim razinama pogodnosti i brzine. Zanimljivo, po mom mišljenju, linkovi su dati ispod.

Do novyh vstreč!

Zanimljivi projekti

Šta još da čitam?

izvor: www.habr.com

Dodajte komentar