Potpuno funkcionalni I/O reaktor na čistu C

Potpuno funkcionalni I/O reaktor na čistu C

Uvod

I/O reaktor (jednopotni ciklus događaja) — to je obrazac za pisanje visoko opterećenog softvera, korišten u mnogim popularnim rješenjima:

U ovom članku ćemo razmotriti osnove I/O reagenta i princip njegovog rada, napisati implementaciju u manje od 200 redaka koda i natjerati jednostavni HTTP poslužitelj da obradi više od 40 milijuna zahtjeva/min.

Predgovor

  • Članak je napisan s ciljem da pomogne u razumijevanju funkcioniranja I/O reaktora, a time i svesti rizike u njegovoj upotrebi.
  • Za razumijevanje članka potrebno je znanje osnova programskog jezika C i malo iskustva u razvoju mrežnih aplikacija.
  • Sav kod je napisan u programskom jeziku C strogo prema (oprez: dug PDF) standardu C11 za Linux i dostupan je na GitHub.

Zašto je to važno?

S porastom popularnosti Interneta, web poslužiteljima je postalo potrebno obraditi velik broj veze istovremeno, zbog čega su isprobana dva pristupa: blokirajuće I/O s velikim brojem OS niti i neblokirajuće I/O u kombinaciji sa sustavom obavijesti o događajima, koji se također naziva „sustavnim selektorom“ (epoll/kqueue/IOCP/etc).

Prvi pristup podrazumijeva stvaranje nove OS niti za svaku dolaznu vezu. Njegov nedostatak je loša skalabilnost: operativnom sustavu će biti potrebno izvršiti mnogo promjena konteksta i sistemskih poziva. To su skupe operacije i mogu dovesti do nedostatka slobodne RAM-a pri velikom broju veza.

Modificirana verzija izdava fiksni broj niti (pool niti), čime se sprječava da sustav nenamjerno prekine izvršavanje, ali unosi novu problematiku: ako u sadašnjem trenutku pool niti blokiraju dugotrajne operacije čitanja, onda drugi soketi koji već mogu primiti podatke neće moći to učiniti.

Drugi pristup koristi sustav obavijesti o događajima (sustavni selektor) koji pruža OS. U ovom članku obradit ćemo najčešći oblik sustavnog selektora, temeljen na obavijestima (događajima, notifikacijama) o spremnosti za I/O operacije, umjesto na obavijestima o njihovom završetku. P pojednostavljeni primjer njegove primjene može se predstaviti sljedećim blok-diagramom:

Potpuno funkcionalni I/O reaktor na čistu C

Razlika između ovih pristupa leži u sljedećem:

  • Blokirajuće I/O operacije obustavljaju korisnički tok do tada, dok OS ne defragmentira primanje IP paketi u tok bajtova (TCP, primanje podataka) ili ne oslobodi dovoljno prostora u internim baferima zapisa za dalje slanje preko NIC (slanje podataka).
  • Sistem selektor nakon nekog vremena obaveštava program da je OS već defragmentirao IP pakete (TCP, primanje podataka) ili da je dovoljno prostora u internim baferima zapisa već dostupno (slanje podataka).

Ukratko, rezervisanje toka OS za svaku I/O - to je bespotrebno trošenje računske moći, jer zapravo, tokovi nisu zauzeti korisnim radom (otuda termin „programska prekid“). Sistem selektor rešava ovaj problem, omogućavajući korisničkom programu da efikasnije troši CPU resurse.

Model I/O reaktora

I/O reaktor deluje kao sloj između sistemskog selektora i korisničkog koda. Način na koji funkcioniše opisan je sledećom blok dijagramom:

Potpuno funkcionalni I/O reaktor na čistu C

  • Podsećam da je događaj — obaveštenje da je određeni soket u stanju da izvrši neblokirajuću I/O operaciju.
  • Obradnik događaja je funkcija koja se poziva od strane I/O reaktora prilikom prijema događaja, koja zatim izvršava neblokirajuću I/O operaciju.

Važno je napomenuti da je I/O reaktor po definiciji jednonitni, ali ništa ne sprečava korišćenje koncepta u višenednom okruženju u odnosu 1 nit: 1 reaktor, čime se optimizuje korišćenje svih CPU jezgara.

Implementacija

Javni interfejs ćemo staviti u datoteku reactor.h, a implementaciju — u reactor.c. reactor.h sastojiće se od sledećih deklaracija:

Prikazati deklaracije u reactor.h

typedef struct reactor Reactor;

/*
 * Pokazatelj na funkciju koja će biti pozvana od strane I/O reaktora prilikom
 * dolaska događaja iz sistemskog selektora.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Vraća `NULL` u slučaju greške, ne-`NULL` pokazatelj na `Reactor` u
 * suprotnom.
 */
Reactor *reactor_new(void);

/*
 * Oslobađa sistemski selektor, sve registrovane sokete u tom trenutku
 * i sam I/O reaktor.
 *
 * Sledeće funkcije vraćaju -1 u slučaju greške, 0 u slučaju uspeha.
 */
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);

/*
 * Pokreće petlju događaja sa vremenskim odbrojavanjem `timeout`.
 *
 * Ova funkcija će preneti kontrolu pozivnom kodu ako je vreme isteklo
 * ili/ili u slučaju nedostatka registrovanih soketa.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

Struktura I/O reaktora sastoji se od fajl deskriptora selektora epoll i heš tabele GHashTable, koja svaki soket mapira na CallbackData (struktura izdržava događaja i korisničkog argumenta za njega).

Prikazati Reactor i CallbackData

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

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

Obratite pažnju da koristimo mogućnost da radimo sa nepotpunim tipom preko pokazatelja. U reactor.h deklariramo strukturu reactor, a u reactor.c njoj definišemo, time ne dopuštajući korisniku da izmenjuje njene polja. Ovo je jedan od obrazaca sakrivanja podataka, koji se stilski uklapa u semantiku C-a.

Funkcije reactor_register, reactor_deregister i reactor_reregister ažuriraju listu interesantnih soketa i odgovarajuće obradnike događaja u sistemskom selektoru i u heš tabeli.

Prikazati 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 uhvatio događaj sa deskriptorom fd, poziva odgovarajući obradnik događaja, u koji prosleđuje fd, bit masku generisanih događaja i korisnički pokazatelj na void.

Prikazati 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) {
        // Greška
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Vrijeme je isteklo
        case 0:
            result = 0;
            goto cleanup;
        // Uspješna operacija
        default:
            // Pozvati obradnike događaja
            for (int i = 0; i table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

Zaključno, niz poziva funkcija u korisničkom kodu izgleda ovako:

Potpuno funkcionalni I/O reaktor na čistu C

Jednoprocesorski server

Kako bismo testirali I/O reaktor pod visokim opterećenjem, napisat ćemo jednostavni HTTP web server koji odgovara slikom na svaki zahtjev.

Kratki vodič o HTTP protokolu

HTTP — to je protokol aplikacijskog nivoa, koji se većinom koristi za interakciju servera s preglednikom.

HTTP se lako može koristiti preko transportnog protokola TCP, šaljajući i primajući poruke u formatu koji je određen specifikacije.

Format zahtjeva

CRLF
CRLF
CRLF
CRLF CRLF

  • CRLF — to je niz od dva znaka: r i n, koji razdvaja prvu liniju zahtjeva, zaglavlja i podatke.
  • <КОМАНДА> — jedna od CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Preglednik će našem serveru slati komandu GET, što znači „Pošalji mi sadržaj datoteke“.
  • <URI> — Uniformni resurs identifikator. Na primjer, ako je URI = /index.html, klijent traži početnu stranicu web sajta.
  • <ВЕРСИЯ HTTP> — verzija HTTP protokola u formatu HTTP/X.Y. Najčešće korištena verzija danas je HTTP/1.1.
  • <ЗАГОЛОВОК N> — to je par ključ-vrijednost u formatu :, poslan serveru za daljnju analizu.
  • — podaci potrebni serveru za izvršenje operacije. Često je to samo JSON ili bilo koji drugi format.

Format odgovora

CRLF
CRLF
CRLF
CRLF CRLF

  • <КОД СТАТУСА> — to je broj koji predstavlja rezultat operacije. Naš poslužitelj uvijek će vraćati status 200 (uspješna operacija).
  • <ОПИСАНИЕ СТАТУСА> — stringovno predstavljanje koda statusa. Za kod statusa 200 — to je OK.
  • <ЗАГОЛОВОК N> — oznaka istog formata kao u zahtjevu. Vraćat ćemo zaglavlja Content-Length (veličina datoteke) i Content-Type: text/html (tip vraćenih podataka).
  • — podaci koje korisnik traži. U našem slučaju to je put do slike u HTML.

Datoteka http_server.c (jednopotni poslužitelj) uključuje datoteku common.h, koja sadržava 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);

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

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

Funkcija fail() ispisuje proslijeđene argumente u terminal (kao printf()) i završava 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' soketa, koji je stvoren sustavskim pozivima socket(), bind() i listen() i sposoban 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;
}

  • Napomena: soket se prvotno stvara u neblokirajućem načinu s pomoću zastavice SOCK_NONBLOCK, tako da funkcija on_accept() (nastavite čitati) sustavni poziv accept() ne zaustavi izvršavanje niti.
  • Ako reuse_port je true, tada će ova funkcija konfigurirati soket s opcijom SO_REUSEPORT pomoću setsockopt(), kako bi koristila istu portu u višepotnoj sredini (vidi odjeljak 'Višepotni poslužitelj').

Obraditelj događaja on_accept() poziva se nakon što OS generira događaj EPOLLIN, što u ovom slučaju znači da nova veza može biti prihvaćena. on_accept() prihvaća novu vezu, prebacuje je u neblokirajući način i registrira s obraditeljem događaja 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);
}

Obraditelj događaja on_recv() poziva se nakon što OS generira događaj EPOLLIN, što u ovom slučaju znači da je veza registrovana on_accept(), spremna za prijem podataka.

on_recv() čita podatke iz veze sve dok HTTP zahtev ne bude potpuno primljen, zatim registruje handler on_send() za slanje HTTP odgovora. Ako klijent prekinu vezu, socket se deregistruje i zatvara putem close().

Prikaži funkciju on_recv()

static void on_recv(void *arg, int fd, uint32_t events) {
    RequestBuffer *buffer = arg;

    // Prihvatamo ulazne podatke sve dok recv ne vrati 0 ili grešku
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Klijent je prekinu vezu
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read je vratio grešku različitu od greške koja blokira poziv
    // niti
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Primljen je kompletan HTTP zahtev od klijenta. Sada registrujemo handler
    // događaja za slanje podataka
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

Obraditelj događaja on_send() poziva se nakon što OS generira događaj EPOLLOUT, što znači da je veza registrovana on_recv(), spremna za slanje podataka. Ova funkcija šalje HTTP odgovor, koji sadrži HTML sa slikom, klijentu i zatim ponovo menja handler za događaj na on_recv().

Prikaži funkciju on_send()

staticka 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 datoteci http_server.c, u funkciji main() kreiramo I/O reaktora pomoću reactor_new(), kreiramo serverski socket i registrujemo ga, pokrećemo reaktora pomoću reactor_run() tako dugo na jednu minute, 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);
}

Provjeravamo da li sve radi kako treba. Kompajliramo (chmod a+x compile.sh && ./compile.sh u korijenu projekta) i pokrećemo server, otvaramo http://127.0.0.1:18470 u pretraživaču i promatramo ono što smo očekivali:

Potpuno funkcionalni I/O reaktor na čistu C

Merenje performansi

Prikaži specifikacije mog računara

$ 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 jednog servera s jednim procesom. Otvorimo dva terminala: u jednom pokrenimo ./http_server, u drugom — wrk. Nakon minute, u 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š server s jednim procesom mogao je obraditi više od 11 milijuna zahtjeva u minuti iz 100 veza. Dobri rezultati, ali možemo li ih poboljšati?

Višenitni server

Kao što je ranije rečeno, I/O reaktor se može kreirati u odvojenim nitima, čime se koriste svi jezgre CPU-a. Primijenimo 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 ima vlastiti reaktor:

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

Obratite pažnju na to da je argument funkcije new_server() - to znači da dodjeljujemo poslužiteljskom soketu opciju true, kako bismo ga koristili u višenitnom okruženju. Više informacija možete pronaći SO_REUSEPORTDrugi pokušaj ovdje.

Sada ćemo mjeriti 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 minuti porastao je oko 3.28 puta! Ali nedostajalo je samo ~dva milijuna do zaokruženog broja, pokušajmo to ispraviti.

Prvo ćemo pogledati statistiku generiranu

perf perf:

$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses .\/http_server_multithreaded

 Statistika performanse za '.\/http_server_multithreaded':

     242446,314933      task-clock (msec)         #    4,000 CPU-a iskorišteno          
         1 813 074      kontekst-prebacivanja          #    0,007 M\/sec                  
             4 689      migracije CPU-a            #    0,019 K\/sec                  
               254      strani-uspjesi               #    0,001 K\/sec                  
   895 324 830 170      ciklusi                    #    3,693 GHz                    
   621 378 066 808      instrukcije              #    0,69  insn po ciklusu         
   119 926 709 370      grane                  #  494,653 M\/sec                  
     3 227 095 669      promašaji grana             #    2,69% svih grana        
           808 664      promašaji keša                                                

      60,604330670 sekundi vrijeme proteklo

Korištenje CPU afiniteta, kompilacija s -march=native, PGO, povećanje broja pogodaka u keš, povećanje MAX_EVENTS i korištenje EPOLLET nije donijelo značajnog poboljšanja u performansama. No, što će se dogoditi ako povećamo broj istodobnih konekcija?

Statistika s 352 istodobna konekcije:

$ 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"
Izvođenje testa od 1m @ http:\/\/127.0.0.1:18470
  8 niti i 352 konekcije
  Thread Stats   Avg      Stdev     Max   +\/ Stdev
    Latencija     2.12ms    3.79ms  68.23ms   87.49%
    Req\/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 zahtjeva u 1.00m, 5.48GB pročitano
Zahtjevi\/sec: 665789.26
Transfer\/sec:     93.34MB

Željeni rezultat je postignut, a uz to i zanimljiva grafika koja prikazuje ovisnost broja obrađenih zahtjeva u 1 minuti od broja konekcija:

Potpuno funkcionalni I/O reaktor na čistu C

Vidimo da nakon nekoliko stotina konekcija broj obrađenih zahtjeva obaju servera naglo opada (kod višedretvenog varijante to je više uočljivo). Je li to povezano s implementacijom TCP\/IP stoga Linuxa? Slobodno ostavite svoje pretpostavke o ovom ponašanju grafika i optimizacijama višedretvenih i jednedretvenih varijanti u komentarima.

Kako napomenuli u komentarima, ovaj test performansi ne prikazuje ponašanje I\/O reaktora pod stvarnim opterećenjima, jer gotovo uvijek server komunicira s bazom podataka, ispisuje logove, koristi kriptografiju s TLS itd., zbog čega opterećenje postaje nejednako (dinamično). Testovi zajedno sa vanjskim komponentama bit će provedeni u članku o I\/O reaktoru.

Nedostaci I\/O reaktora

Trebate razumjeti da I\/O reaktor nema nedostataka, a to se posebno odnosi na:

  • Korištenje I\/O reaktora u višedretvenom okruženju je nešto složenije, jer će biti potrebno ručno upravljati nitima.
  • Praksa pokazuje da je u većini slučajeva opterećenje neujednačeno, što može dovesti do toga da jedan tok radi dok je drugi zauzet poslom.
  • Ako jedan obrtnik događaja blokira tok, također će se blokirati i sam sistemski selektor, što može dovesti do teško uočljivih grešaka.

Ove probleme rešava I/O reaktor, često imajući planifikator koji ravnomerno raspoređuje opterećenje u grupi tokova, a takođe ima i jednostavnije API. O tome će biti reči kasnije, u mom drugom članku.

Zaključak

Time je naše putovanje od teorije do izlaza profajlera završeno.

Ne treba se ovde zaustavljati, jer postoje mnogi drugi interesantni pristupi za pisanje mrežnog softvera s različitim nivoima praktičnosti i brzine. Interesantne, po mom mišljenju, poveznice su navedene ispod.

Do narednog susreta!

Interesantni projekti

Šta još pročitati?

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster