Plně vybavený bare-C I/O reaktor

Plně vybavený bare-C I/O reaktor

úvod

I/O reaktor (jednovláknové smyčka událostí) je vzor pro psaní vysoce zátěžového softwaru, který se používá v mnoha populárních řešeních:

V tomto článku se podíváme na výhody a nevýhody I/O reaktoru a na to, jak funguje, napíšeme implementaci v méně než 200 řádcích kódu a vytvoříme jednoduchý proces HTTP serveru přes 40 milionů požadavků/min.

předmluva

  • Článek byl napsán tak, aby pomohl porozumět fungování I/O reaktoru, a tudíž porozumět rizikům při jeho používání.
  • Pro pochopení článku je nutná znalost základů. jazyk C a určité zkušenosti s vývojem síťových aplikací.
  • Veškerý kód je napsán v jazyce C přesně podle (upozornění: dlouhé PDF) podle standardu C11 pro Linux a dostupné na GitHub.

Proč to udělal?

S rostoucí popularitou internetu začaly webové servery potřebovat obsluhovat velké množství připojení současně, a proto byly vyzkoušeny dva přístupy: blokování I/O na velkém počtu vláken OS a neblokování I/O v kombinaci s systém oznamování událostí, nazývaný také „výběr systému“ (epoll/kqueue/IOCP/atd).

První přístup zahrnoval vytvoření nového vlákna operačního systému pro každé příchozí připojení. Jeho nevýhodou je špatná škálovatelnost: operační systém jich bude muset implementovat mnoho kontextové přechody и systémová volání. Jsou to drahé operace a mohou vést k nedostatku volné paměti RAM s působivým počtem připojení.

Upravená verze zdůrazňuje pevný počet vláken (pool vláken), čímž se zabrání systému v přerušení provádění, ale zároveň se zavádí nový problém: pokud je fond vláken aktuálně blokován dlouhými operacemi čtení, pak ostatní sokety, které jsou již schopny přijímat data, nebudou moci Učiň tak.

Druhý přístup využívá systém oznamování událostí (selektor systému) poskytovaný OS. Tento článek pojednává o nejběžnějším typu selektoru systému založeného na výstrahách (událostech, upozorněních) o připravenosti na I/O operace, spíše než na oznámení o jejich dokončení. Zjednodušený příklad jeho použití může být znázorněn na následujícím blokovém schématu:

Plně vybavený bare-C I/O reaktor

Rozdíl mezi těmito přístupy je následující:

  • Blokování I/O operací pozastavit uživatelský tok dokuddokud nebude OS správně defragmentuje přicházející IP pakety do byte streamu (TCP, přijímání dat) nebo nebude v interních vyrovnávací paměti pro zápis dostatek místa pro následné odeslání přes NIC (odeslání dat).
  • Volič systému přesčas upozorní program, že OS již defragmentované IP pakety (TCP, příjem dat) nebo dostatek místa v interních vyrovnávací paměti pro zápis již k dispozici (odesílání dat).

Abych to shrnul, rezervování vlákna OS pro každý I/O je plýtváním výpočetního výkonu, protože ve skutečnosti vlákna nedělají užitečnou práci (odtud tento termín pochází "softwarové přerušení"). Selektor systému tento problém řeší a umožňuje uživatelskému programu využívat zdroje CPU mnohem ekonomičtěji.

Model I/O reaktoru

I/O reaktor funguje jako vrstva mezi selektorem systému a uživatelským kódem. Princip jeho činnosti popisuje následující blokové schéma:

Plně vybavený bare-C I/O reaktor

  • Dovolte mi připomenout, že událost je upozornění, že určitý soket je schopen provést neblokující I/O operaci.
  • Obsluha události je funkce volaná I/O reaktorem, když je přijata událost, která pak provede neblokující I/O operaci.

Je důležité poznamenat, že I/O reaktor je z definice jednovláknový, ale nic nebrání tomu, aby byl koncept použit ve vícevláknovém prostředí v poměru 1 vlákno: 1 reaktor, čímž se recyklují všechna jádra CPU.

uskutečnění

Veřejné rozhraní umístíme do souboru reactor.h, a implementace - in reactor.c. reactor.h se bude skládat z následujících oznámení:

Zobrazit deklarace v reaktoru.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 reaktoru se skládá z deskriptor souboru volič epoll и hashovací tabulky GHashTable, který mapuje každou zásuvku CallbackData (struktura obsluhy události a uživatelský argument pro ni).

Zobrazit data reaktoru a zpětného volání

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

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

Vezměte prosím na vědomí, že jsme povolili možnost zpracování neúplný typ podle indexu. V reactor.h deklarujeme strukturu reactorA reactor.c definujeme jej, čímž zabráníme uživateli explicitně měnit jeho pole. Toto je jeden ze vzorů skrytí dat, který stručně zapadá do sémantiky C.

funkce reactor_register, reactor_deregister и reactor_reregister aktualizujte seznam zájmových soketů a odpovídajících obslužných rutin událostí v systémovém selektoru a hashovací 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;
}

Poté, co I/O reaktor zachytil událost pomocí deskriptoru fd, zavolá odpovídající obsluhu události, které předá fd, bitová maska generované události a uživatelský ukazatel void.

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

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

Plně vybavený bare-C I/O reaktor

Jednovláknový server

Abychom otestovali I/O reaktor při vysoké zátěži, napíšeme jednoduchý HTTP webový server, který na jakýkoli požadavek odpoví obrázkem.

Rychlý odkaz na protokol HTTP

HTTP - toto je protokol aplikační úroveň, primárně používaný pro interakci server-prohlížeč.

HTTP lze snadno použít přes doprava protokol TCP, odesílání a přijímání zpráv v určeném formátu Specifikace.

Formát požadavku

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

  • CRLF je posloupnost dvou znaků: r и n, oddělující první řádek požadavku, hlavičky a data.
  • <КОМАНДА> - jeden z CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Prohlížeč odešle příkaz našemu serveru GET, což znamená "Pošlete mi obsah souboru."
  • <URI> - jednotný identifikátor zdroje. Pokud například URI = /index.html, pak klient požaduje hlavní stránku webu.
  • <ВЕРСИЯ HTTP> — verze protokolu HTTP ve formátu HTTP/X.Y. Dnes je nejčastěji používaná verze HTTP/1.1.
  • <ЗАГОЛОВОК N> je pár klíč–hodnota ve formátu <КЛЮЧ>: <ЗНАЧЕНИЕ>, odeslána na server k další analýze.
  • <ДАННЫЕ> — data požadovaná serverem k provedení operace. Často je to jednoduché JSON nebo jakýkoli jiný formát.

Formát odpovědi

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

  • <КОД СТАТУСА> je číslo představující výsledek operace. Náš server vždy vrátí stav 200 (úspěšná operace).
  • <ОПИСАНИЕ СТАТУСА> — řetězcová reprezentace stavového kódu. Pro stavový kód 200 to je OK.
  • <ЗАГОЛОВОК N> — záhlaví stejného formátu jako v požadavku. Tituly vrátíme Content-Length (velikost souboru) a Content-Type: text/html (návratový datový typ).
  • <ДАННЫЕ> — údaje požadované uživatelem. V našem případě je to cesta k obrazu in HTML.

Soubor http_server.c (server s jedním vláknem) obsahuje soubor common.h, který obsahuje následující prototypy funkcí:

Zobrazit prototypy funkcí společně.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);

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

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

Funkce fail() vypíše předané argumenty do terminálu (např printf()) a ukončí program pomocí kódu 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í deskriptor souboru soketu "server" vytvořený systémovými voláními socket(), bind() и listen() a schopný 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;
}

  • Všimněte si, že soket je zpočátku vytvořen v neblokujícím režimu pomocí příznaku SOCK_NONBLOCKtakže ve funkci on_accept() (čtěte více) systémové volání accept() nezastavil provádění vlákna.
  • Jestliže reuse_port rovná true, pak tato funkce nakonfiguruje zásuvku s možností SO_REUSEPORT přes setsockopt()používat stejný port ve vícevláknovém prostředí (viz část „Vícevláknový server“).

Obsluha události on_accept() voláno poté, co operační systém vygeneruje událost EPOLLIN, což v tomto případě znamená, že nové připojení může být přijato. on_accept() přijme nové připojení, přepne jej do neblokovacího režimu a zaregistruje se pomocí obsluhy události on_recv() v I/O reaktoru.

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

Obsluha události on_recv() voláno poté, co operační systém vygeneruje událost EPOLLIN, v tomto případě to znamená, že se připojení zaregistrovalo on_accept(), připravena přijímat data.

on_recv() čte data z připojení, dokud není zcela přijat HTTP požadavek, poté zaregistruje handler on_send() k odeslání odpovědi HTTP. Pokud klient přeruší připojení, je soket odregistrován a uzavřen pomocí close().

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

Obsluha události on_send() voláno poté, co operační systém vygeneruje událost EPOLLOUT, což znamená, že se připojení zaregistrovalo on_recv(), připraven k odeslání dat. Tato funkce odešle HTTP odpověď obsahující HTML s obrázkem klientovi a poté změní obslužnou rutinu události zpět na on_recv().

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(), vytvořte serverový soket a zaregistrujte jej, spusťte reaktor pomocí reactor_run() přesně na jednu minutu a poté uvolníme prostředky a ukončí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, zda vše funguje podle očekávání. Kompilace (chmod a+x compile.sh && ./compile.sh v kořenovém adresáři projektu) a spusťte samostatně psaný server, otevřete http://127.0.0.1:18470 v prohlížeči a uvidíme, co jsme očekávali:

Plně vybavený bare-C I/O reaktor

Měření výkonu

Ukaž moje specifikace auta

$ 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

Pojďme změřit výkon jednovláknového serveru. Otevřeme dva terminály: v jednom spustíme ./http_server, v jiném - vlnit. Po minutě se na 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 byl schopen zpracovat více než 11 milionů požadavků za minutu pocházejících ze 100 připojení. Není to špatný výsledek, ale dá se to zlepšit?

Vícevláknový server

Jak bylo zmíněno výše, I/O reaktor může být vytvořen v samostatných vláknech, čímž se využívají všechna jádra CPU. Uveďme tento přístup do praxe:

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 vlastní svůj vlastní reaktor:

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

Vezměte prosím na vědomí, že argument funkce new_server() obhajuje true. To znamená, že možnost přiřadíme serverovému soketu SO_REUSEPORTpro použití ve vícevláknovém prostředí. Můžete si přečíst více zde.

Druhý běh

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 požadavků zpracovaných za 1 minutu vzrostl ~3.28krát! Ale do kulatého čísla nám chyběly jen ~XNUMX miliony, tak to zkusme napravit.

Nejprve se podívejme na vygenerované statistiky perf:

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

 Performance counter stats for './http_server_multithreaded':

     242446,314933      task-clock (msec)         #    4,000 CPUs utilized          
         1 813 074      context-switches          #    0,007 M/sec                  
             4 689      cpu-migrations            #    0,019 K/sec                  
               254      page-faults               #    0,001 K/sec                  
   895 324 830 170      cycles                    #    3,693 GHz                    
   621 378 066 808      instructions              #    0,69  insn per cycle         
   119 926 709 370      branches                  #  494,653 M/sec                  
     3 227 095 669      branch-misses             #    2,69% of all branches        
           808 664      cache-misses                                                

      60,604330670 seconds time elapsed

Použití CPU Affinity, kompilace s -march=native, PGO, zvýšení počtu zásahů cache, zvýšit MAX_EVENTS a použít EPOLLET nepřineslo výrazné zvýšení výkonu. Ale co se stane, když zvýšíte počet současných připojení?

Statistiky pro 352 současných připojení:

$ 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

Požadovaný výsledek byl získán a s ním i zajímavý graf ukazující závislost počtu zpracovaných požadavků za 1 minutu na počtu spojení:

Plně vybavený bare-C I/O reaktor

Vidíme, že po několika stovkách připojení počet zpracovaných požadavků pro oba servery prudce klesá (u vícevláknové verze je to znatelnější). Souvisí to s implementací zásobníku TCP/IP v Linuxu? Své domněnky o tomto chování grafu a optimalizacích pro vícevláknové a jednovláknové možnosti klidně napište do komentářů.

Jak poznamenal v komentářích tento test výkonu neukazuje chování I/O reaktoru při skutečném zatížení, protože téměř vždy server komunikuje s databází, vydává protokoly, používá kryptografii s TLS atd., v důsledku čehož se zatížení stává nerovnoměrným (dynamickým). Testy spolu s komponentami třetích stran budou provedeny v článku o I/O proaktoru.

Nevýhody I/O reaktoru

Musíte pochopit, že I/O reaktor není bez svých nevýhod, jmenovitě:

  • Použití I/O reaktoru ve vícevláknovém prostředí je poněkud obtížnější, protože budete muset ručně spravovat toky.
  • Praxe ukazuje, že ve většině případů je zatížení nerovnoměrné, což může vést k protokolování jednoho vlákna, zatímco jiné je zaneprázdněno prací.
  • Pokud jedna obsluha události zablokuje vlákno, pak se zablokuje i samotný selektor systému, což může vést k těžko dohledatelným chybám.

Řeší tyto problémy I/O proaktor, který má často plánovač, který rovnoměrně rozděluje zátěž do fondu vláken, a má také pohodlnější API. Budeme o tom mluvit později, v mém jiném článku.

Závěr

Tady naše cesta od teorie přímo k výfuku profileru skončila.

Neměli byste se tím zabývat, protože existuje mnoho dalších stejně zajímavých přístupů k psaní síťového softwaru s různou úrovní pohodlí a rychlosti. Zajímavé, podle mého názoru, odkazy jsou uvedeny níže.

Až do příště!

Zajímavé projekty

Co ještě číst?

Zdroj: www.habr.com

Přidat komentář