Plně funkční I/O reaktor v jazyce C

Plně funkční I/O reaktor v jazyce C

Úvod

I/O reaktor (jednovláknový cyklus událostí) — to je vzor pro psaní vysoce zatíženého softwaru, který se používá v mnoha populárních řešeních:

V tomto článku se podíváme na vnitřnosti I/O reaktoru a princip jeho fungování, napíšeme implementaci v méně než 200 řádcích kódu a přimějeme jednoduchý HTTP server zpracovávat více než 40 milionů požadavků/min.

Úvod

  • Článek je napsán s cílem pomoci pochopit fungování I/O reaktoru a tím si uvědomit rizika spojená s jeho používáním.
  • Pro pochopení článku je nutné mít znalosti základů jazyka C a malou zkušenost s vývojem síťových aplikací.
  • Veškerý kód je napsán v jazyce C přísně podle (pozor: dlouhý PDF) standartu C11 pro Linux a je dostupný na GitHub.

Proč je to potřebné?

S rostoucí popularitou internetu se webovým serverům stalo potřeba zpracovávat velké množství připojení zároveň, proto byly vyzkoušeny dva přístupy: blokující I/O na velkém počtu vláken OS a nebločící I/O v kombinaci se systémem notifikací o událostech, také nazývaným „systémový selektor“ (epoll/kqueue/IOCP/etc).

První přístup zahrnoval vytvoření nového vlákna OS pro každé příchozí připojení. Jeho nevýhodou je špatná škálovatelnost: operační systém musí provádět mnoho přepnutí kontextu a systémových volání. Tyto operace jsou nákladné a mohou vést k nedostatku volné RAM při značném počtu připojení.

Upravená verze určuje fixní počet vláken (vláknový pool), čímž zabrání systému v havarijním ukončení vykonávání, ale zároveň přináší nový problém: pokud v daném okamžiku vlákna poolu blokují dlouhé operace čtení, tak další sokety, které už mohou přijímat data, to nebudou moci provést.

Druhý přístup používá systém notifikací o událostech (systémový selektor), který poskytuje OS. V tomto článku je rozebrán nejčastěji se vyskytující typ systémového selektoru, založený na notifikacích (událostech, upozorněních) o připravenosti k I/O operacím, spíše než na notifikacích o jejich dokončení. Zjednodušený příklad jeho použití lze představit následujícím diagramem:

Plně funkční I/O reaktor v jazyce C

Rozdíl mezi těmito přístupy spočívá v následujícím:

  • Blokující I/O operace pozastavují uživatelský tok dokud, dokud operační systém řádně defragmenuje příchozí IP pakety do proudu bajtů (TCP, příjem dat) nebo se neuvolní dostatek místa ve vnitřních vyrovnávacích pamětích pro následný přenos přes NIC (odesílání dat).
  • Systémový selektor po nějaké době informuje program, že operační systém již defragmenoval IP pakety (TCP, příjem dat) nebo že je dostatek místa ve vnitřních vyrovnávacích pamětích již k dispozici (odesílání dat).

Závěrem, rezervace proudu operačního systému pro každý I/O je zbytečná ztráta výpočetní síly, protože ve skutečnosti, proudy nejsou zaplněny užitečnou prací (odtud pochází termín „programové přerušení“). Systémový selektor řeší tento problém, což umožňuje uživatelskému programu využívat CPU zdroje mnohem efektivněji.

Model I/O reaktoru

I/O reaktor funguje jako mezivrstva mezi systémovým selektorem a uživatelským kódem. Princip jeho činnosti je popsán následujícím blokovým schématem:

Plně funkční I/O reaktor v jazyce C

  • Připomínám, že událost je upozornění, že určitý soket je schopen provést neblokující I/O operaci.
  • Zpracovatel událostí je funkce vyvolaná I/O reaktorem při obdržení události, která poté vykonává neblokující I/O operaci.

Je důležité poznamenat, že I/O reaktor je podle definice jednovláknový, ale nic nebrání tomu, aby byl koncept použit ve vícevložkovém prostředí s 1 vlákno: 1 reaktor, čímž se využijí všechny jádra CPU.

Implementace

Veřejné rozhraní umístíme do souboru reactor.h, a implementaci — do reactor.c. reactor.h bude obsahovat následující deklarace:

Zobrazit deklarace v reactor.h

typedef struct reactor Reactor;

/*
 * Ukazatel na funkci, která bude vyvolána I/O reaktorem při příjmu
 * události od systémového selektoru.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Vrací `NULL` v případě chyby, ne-`NULL` ukazatel na `Reactor` v
 * opačném případě.
 */
Reactor *reactor_new(void);

/*
 * Uvolňuje systémový selektor, všechny registrované sokety v daném okamžiku
 * a samotný I/O reaktor.
 *
 * Následující funkce vrací -1 v případě chyby, 0 v případě úspěchu.
 */
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);

/*
 * Spouští cyklus událostí s časovým limitem `timeout`.
 *
 * Tato funkce předá kontrolu volajícímu kódu, pokud uplynulý čas vypršel
 * nebo/či při absenci registrovaných socketů.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

Struktura I/O reaktoru se skládá z souborového popisovače selektoru epoll a hash tabulky GHashTable, která každý socket mapuje na CallbackData (struktura obsluhující událost a argument pro uživatele).

Zobrazit Reactor a CallbackData

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

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

Všimněte si, že jsme využili možnost manipulace s neúplným typem pomocí ukazatele. V reactor.h vyhlašujeme strukturu reactor, a v reactor.c a definujeme ji, čímž zabraňujeme uživateli explicitně měnit její pole. To je jedno z vzorů zakrývání dat, které se elegantně vejde do semantiky C.

Funkce reactor_register, reactor_deregister a reactor_reregister aktualizují seznam zajímajících se socketů a odpovídajících obsluhovačů událostí v systému selektoru a v hash tabulce.

Zobrazit registrační funkce

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

Po tom, co I/O reaktor zachytí událost s popisovačem fd, vyvolá odpovídající obsluhovač události, do kterého předává fd, bitovou masku vygenerovaných událostí a uživatelský ukazatel na void.

Zobrazit funkci 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) {
        // Chyba
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Čas vypršel
        case 0:
            result = 0;
            goto cleanup;
        // Úspěšná operace
        default:
            // Zavolat zpracovatele událostí
            for (int i = 0; i table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

Souhrnně, řetězec volání funkcí v uživatelském kódu bude mít následující podobu:

Plně funkční I/O reaktor v jazyce C

Jednovláknový server

Abychom otestovali I/O reaktor při vysokém zatížení, napíšeme jednoduchý HTTP webový server, který na jakýkoli dotaz odpoví obrázkem.

Krátká příručka k protokolu HTTP

HTTP — je to protokol aplikace úrovně, především používaný pro komunikaci serveru s prohlížečem.

HTTP lze snadno používat nad transportní protokolu TCP, posílající a přijímající zprávy ve formátu stanoveném specifikací.

Formát požadavku

CRLF
CRLF
CRLF
CRLF CRLF

  • CRLF — je to sekvence dvou znaků: jsou rovny nule. a n, která odděluje první řádek požadavku, hlavičky a data.
  • <КОМАНДА> — jedna z CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Prohlížeč našemu serveru pošle příkaz GET, což znamená „Pošli mi obsah souboru“.
  • <URI> — unifikovaný identifikátor zdroje. Například, pokud URI = /index.html, pak klient žádá o hlavní stránku webu.
  • <ВЕРСИЯ HTTP> — verze protokolu HTTP ve formátu HTTP/X.Y. Nejčastěji používaná verze v současnosti je HTTP/1.1.
  • <ЗАГОЛОВОК N> — to je pár klíč-hodnota ve formátu :, posílané serveru k dalšímu zpracování.
  • — data, která server potřebuje k provedení operace. Často je to jen JSON nebo jakýkoli jiný formát.

Formát odpovědi

CRLF
CRLF
CRLF
CRLF CRLF

  • <КОД СТАТУСА> — je číslo, které představuje výsledek operace. Náš server bude vždy vracet stav 200 (úspěšná operace).
  • <ОПИСАНИЕ СТАТУСА> — řetězcová reprezentace kódu stavu. Pro kód stavu 200 — to je OK.
  • <ЗАГОЛОВОК N> — hlavička ve stejném formátu jako v žádosti. Budeme vracet hlavičky Content-Length (velikost souboru) a Content-Type: text/html (typ vracených dat).
  • — uživatelsky požadovaná data. V našem případě je to cesta k obrázku v HTML.

Soubor http_server.c (jednoduchý server) zahrnuje soubor common.h, který obsahuje následující prototypy funkcí:

Zobrazit prototypy funkcí 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);

Také je zde popsán funkční makro SAFE_CALL() a je definována funkce fail(). Makro porovnává hodnotu výrazu s chybou, a pokud je podmínka splněna, zavolá funkci fail():

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

Funkce fail() vytiskne předané argumenty do terminálu (jako printf()) a ukončuje program s kódem 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);
}

Funkce new_server() vrací popisovač souboru "serverového" soketu, vytvořeného systémovými voláními socket(), bind() a listen() a schopného přijímat příchozí spojení v neblokujícím režimu.

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

je volán poté, co operační systém vygeneruje událost on_accept() EPOLLIN , což v tomto případě znamená, že nové spojení může být přijato.přijímá nové spojení, přepíná ho do neblokujícího režimu a registruje s obsluhou události on_accept() on_recv() v I/O reaktoru. Zobrazit funkci on_accept()

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

je volán poté, co operační systém vygeneruje událost v I/O reaktoru. EPOLLIN , což v tomto případě znamená, že nové spojení může být přijato., v tomto případě to znamená, že spojení, které bylo registrováno on_accept(), je připraveno přijmout data.

v I/O reaktoru. čte data ze spojení, dokud není plně přijata HTTP žádost, poté registruje zpracovatele on_send() pro odeslání HTTP odpovědi. Pokud klient přeruší spojení, soket bude deregistrován a uzavřen pomocí close().

Zobrazit funkci on_recv()

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

    // Přijímáme vstupní data, dokud recv nevrátí 0 nebo chybu
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Klient přerušil spojení
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read vrátil chybu, která není chybou, kvůli které by se volání zablokovalo
    // vlákno
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Byla přijata úplná HTTP žádost od klienta. Nyní registrujeme zpracovatele
    // událostí pro odesílání dat
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

je volán poté, co operační systém vygeneruje událost on_send() EPOLLIN EPOLLOUT, což znamená, že spojení, které bylo registrováno v I/O reaktoru., je připraveno odeslat data. Tato funkce odesílá HTTP odpověď obsahující HTML s obrázkem klientovi a poté znovu změní zpracovatele událostí na v I/O reaktoru..

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

A nakonec, v souboru http_server.c, ve funkci main() vytváříme I/O reaktor pomocí reactor_new(), vytváříme serverový soket a registrujeme ho, spustíme reaktor pomocí reactor_run() přesně na jednu minutu, poté uvolníme prostředky a opustíme program.

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

Zkontrolujeme, že všechno funguje tak, jak má. Kompilujeme (chmod a+x compile.sh && ./compile.sh v kořenovém adresáři projektu) a spustíme náš vlastní server, otevřeme http://127.0.0.1:18470 v prohlížeči a pozorujeme, co jsme očekávali:

Plně funkční I/O reaktor v jazyce C

Měření výkonu

Zobrazit specifikace mého stroje

$ 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

Změříme výkon jednovláknového serveru. Otevřeme dvě terminály: v jednom spustíme ./http_server, v druhém — wrk. Po minutě se ve druhém terminálu zobrazí následující 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

Náš jednovláknový server zvládl zpracovat přes 11 milionů požadavků za minutu vycházejících z 100 připojení. Dobré číslo, ale lze ho zlepšit?

Vícevláknový server

Jak bylo zmíněno výše, I/O reaktor lze vytvářet v samostatných vláknech, čímž se využijí všechny jádra CPU. Aplikujeme tento přístup v praxi:

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

Nyní každé vlákno má svůj vlastní reaktor:

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

Všimněte si, že argumentem funkce new_server() je true. To znamená, že serverovému socketu přiřazujeme možnost prostřednictvím, abychom jej mohli využít v mnohovláknovém prostředí. Podrobnosti si můžete přečíst tady.

Druhá část

Nyní změříme výkon vícevláknového serveru:

$ 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

Počet zpracovaných požadavků za 1 minutu vzrostl přibližně 3.28krát! Ale k zaokrouhlenému číslu chybělo pouze ~dva miliony, pokusíme se to opravit.

Nejprve se podíváme na statistiku, kterou jsme vygenerovali 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 výkonu pro './http_server_multithreaded':

     242446,314933      task-clock (ms)         #    4,000 CPU využito          
         1 813 074      přepnutí kontextu          #    0,007 M/sec                  
             4 689      migrace CPU            #    0,019 K/sec                  
               254      stránkovací chyby               #    0,001 K/sec                  
   895 324 830 170      cykly                    #    3,693 GHz                    
   621 378 066 808      instrukce              #    0,69  insn na cyklus         
   119 926 709 370      větve                  #  494,653 M/sec                  
     3 227 095 669      chyby větvení             #    2,69% všech větví        
           808 664      chyby v cache                                                

      60,604330670 sekund uplynulo

Využití affinity CPU, kompilace s -march=native, PGO, zvýšení počtu zásahů do cache, zvýšení MAX_EVENTS a využívání EPOLLET nepřineslo výrazný nárůst výkonu. Ale co se stane, pokud zvýšíme počet současných spojení?

Statistika při 352 současných spojení:

$ 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"
Běh testu po dobu 1m @ http://127.0.0.1:18470
  8 vláken a 352 spojení
  Statistiky vlákna   Avg      Stdev     Max   +/− Stdev
    Latence     2.12ms    3.79ms  68.23ms   87.49%
    Req/sec    83.78k    12.69k  169.81k    83.59%
  40006142 požadavků za 1.00m, 5.48GB přečteno
Požadavky/sec: 665789.26
Přenos/sec:     93.34MB

Žádaný výsledek byl dosažen, spolu s ním zajímavý graf ukazující závislost počtu zpracovaných požadavků za 1 minutu na počtu spojení:

Plně funkční I/O reaktor v jazyce C

Vidíme, že po několika stovkách spojení počet zpracovaných požadavků u obou serverů výrazně klesá (u vícerozměrné varianty je to výraznější). Je to spojeno s realizací TCP/IP protokolu v Linuxu? Své názory ohledně takového chování grafu a optimalizací vícerozměrných a jednorozměrných variant neváhejte psát do komentářů.

Jak , že nařízení diskriminuje poskytovatele a porušuje V komentářích, tento test výkonu neukazuje chování I/O reactoru při skutečných zátěžích, protože téměř vždy server interaguje s DB, zapisuje logy, používá kryptografii s TLS atd., což způsobuje, že zátěž se stává heterogenní (dynamickou). Testy spolu se třetími komponenty budou provedeny v článku o I/O reactoru.

Nevýhody I/O reactoru

Je třeba si uvědomit, že I/O reactor není bez nedostatků, a to konkrétně:

  • Používání I/O reactoru v vícerozměrné prostředí je poněkud složitější, protože je nutné ručně řídit vlákna.
  • Praxe ukazuje, že v většině případů je zatížení nehomogenní, což může vést k tomu, že jedna vlna bude zatěžovat, zatímco druhá bude zaměstnána prací.
  • Pokud jeden zpracovatel události zablokuje vlákno, pak se také zablokuje samotný systémový selektor, což může vést k těžko odhalitelným chybám.

Tyto problémy řeší I/O reactor, který obvykle má plánovač, který rovnoměrně rozděluje zátěž v poolu vláken, a také má pohodlnější API. O něm bude řeč později, v mém jiném článku.

Závěr

Na tomto našem putování z teorie přímo k profilingovému výstupu jsme skončili.

Není dobré na tom zastavit, existuje mnoho dalších zajímavých přístupů k psaní síťového softwaru s různou úrovní pohodlí a rychlosti. Zajímavé odkazy, jak se domnívám, jsou uvedeny níže.

Na shledanou!

Zajímavé projekty

Co ještě číst?

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster