Teljes funkcionalitású csupasz C I/O reaktor

Teljes funkcionalitású csupasz C I/O reaktor

Bevezetés

I/O reaktor (egymenetes eseményhurok) egy minta nagy terhelésű szoftverek írásához, amelyet számos népszerű megoldásban használnak:

Ebben a cikkben megvizsgáljuk az I/O reaktor csínját-bínját és működését, egy implementációt írunk le kevesebb mint 200 kódsorból, és egy egyszerű HTTP-kiszolgáló folyamatot készítünk 40 millió kérés/perc felett.

Előszó

  • A cikk azért készült, hogy segítsen megérteni az I/O reaktor működését, és ezáltal megérteni a használat során felmerülő kockázatokat.
  • A cikk megértéséhez az alapok ismerete szükséges. C nyelv és némi tapasztalat hálózati alkalmazások fejlesztésében.
  • Minden kód C nyelven van írva szigorúan a (Figyelem: hosszú PDF) C11 szabvány szerint Linuxra és a következőn érhető el GitHub.

Miért is?

Az internet népszerűségének növekedésével a webszervereknek egyre nagyobb számú kapcsolatot kellett egyszerre kezelniük, ezért két megközelítést próbáltak ki: az I/O blokkolását nagyszámú operációs rendszer szálon és a nem blokkoló I/O-t a eseményértesítési rendszer, más néven „rendszerválasztó” (epoll/kqueue/IOCP/stb).

Az első megközelítés egy új operációs rendszer-szál létrehozását jelentette minden bejövő kapcsolathoz. Hátránya a rossz skálázhatóság: az operációs rendszernek sokat kell implementálnia kontextus átmenetek и rendszerhívások. Ezek költséges műveletek, és a szabad RAM hiányához vezethetnek lenyűgöző számú kapcsolat mellett.

A módosított változat kiemeli fix számú szál (szálkészlet), ezzel megakadályozva, hogy a rendszer megszakítsa a végrehajtást, ugyanakkor új problémát vet fel: ha egy szálkészletet jelenleg hosszú olvasási műveletek blokkolnak, akkor a többi socket, amely már képes adatokat fogadni, nem fogja tudni tehát csináld meg.

A második megközelítést alkalmazza eseményértesítési rendszer (rendszerválasztó) az operációs rendszer által biztosított. Ez a cikk a rendszerválasztó leggyakoribb típusát tárgyalja, amely az I/O műveletekre való készenlétről szóló riasztásokon (események, értesítések) alapul, nem pedig a értesítések azok befejezéséről. Használatának egyszerűsített példája a következő blokkdiagrammal ábrázolható:

Teljes funkcionalitású csupasz C I/O reaktor

E megközelítések közötti különbség a következő:

  • I/O műveletek blokkolása felfüggeszti felhasználói áramlás amígamíg az operációs rendszer megfelelő nem lesz töredezettségmentesítése beérkező IP-csomagok bájtos adatfolyamhoz (TCP, adatok fogadása), vagy nem lesz elegendő hely a belső írási pufferekben a későbbi küldéshez NIC (adatküldés).
  • Rendszerválasztó túlóra értesíti a programot, hogy az OS már töredezettségmentesített IP-csomagok (TCP, adatfogadás) vagy elegendő hely a belső írási pufferekben már elérhető (adatok küldése).

Összefoglalva, minden I/O számára egy operációs rendszer szál lefoglalása számítási teljesítmény pazarlása, mivel a valóságban a szálak nem végeznek hasznos munkát (ezért a kifejezés "szoftver megszakítás"). A rendszerválasztó megoldja ezt a problémát, lehetővé téve a felhasználói program számára, hogy sokkal gazdaságosabban használja fel a CPU erőforrásait.

I/O reaktor modell

Az I/O reaktor rétegként működik a rendszerválasztó és a felhasználói kód között. Működésének elvét a következő blokkdiagram írja le:

Teljes funkcionalitású csupasz C I/O reaktor

  • Hadd emlékeztesselek arra, hogy az esemény egy értesítés arról, hogy egy bizonyos socket nem blokkoló I/O műveletet tud végrehajtani.
  • Az eseménykezelő egy olyan funkció, amelyet az I/O reaktor hív meg, amikor esemény érkezik, és ezután egy nem blokkoló I/O műveletet hajt végre.

Fontos megjegyezni, hogy az I/O reaktor definíció szerint egyszálú, de semmi akadálya annak, hogy a koncepciót többszálas környezetben 1 szál: 1 reaktor arányban használják, ezáltal az összes CPU magot újrahasznosítsák.

Реализация

A nyilvános felületet egy fájlban helyezzük el reactor.h, és megvalósítás - in reactor.c. reactor.h a következő közleményekből fog állni:

Deklarációk megjelenítése a reaktorban.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);

Az I/O reaktor szerkezete a következőkből áll fájlleíró választó epoll и hash táblázatok GHashTable, amely leképezi az egyes aljzatokat CallbackData (egy eseménykezelő szerkezete és egy felhasználói argumentum hozzá).

Reaktor és visszahívási adatok megjelenítése

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

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

Felhívjuk figyelmét, hogy engedélyeztük a kezelési képességet hiányos típus az index szerint. BAN BEN reactor.h kijelentjük a szerkezetet reactor, és be reactor.c definiáljuk, ezzel megakadályozva, hogy a felhasználó kifejezetten módosítsa a mezőit. Ez az egyik minta adatok elrejtése, ami tömören beleillik a C szemantikába.

függvények reactor_register, reactor_deregister и reactor_reregister frissítse az érdeklődésre számot tartó socketek listáját és a megfelelő eseménykezelőket a rendszerválasztóban és a hash-táblázatban.

Regisztrációs funkciók megjelenítése

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

Miután az I/O reaktor elfogta az eseményt a leíróval fd, meghívja a megfelelő eseménykezelőt, amelyhez továbbad fd, bit maszk generált eseményeket és egy felhasználói mutatót void.

Reactor_run() függvény megjelenítése

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

Összefoglalva, a függvényhívások lánca a felhasználói kódban a következő formában jelenik meg:

Teljes funkcionalitású csupasz C I/O reaktor

Egyszálas szerver

Az I/O reaktor nagy terhelés alatti teszteléséhez egy egyszerű HTTP webszervert fogunk írni, amely minden kérésre képpel válaszol.

Gyors hivatkozás a HTTP protokollra

HTTP - ez a protokoll alkalmazási szint, elsősorban szerver-böngésző interakcióhoz használják.

A HTTP könnyen használható szállítás jegyzőkönyv TCP, üzenetek küldése és fogadása meghatározott formátumban leírás.

Kérési formátum

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

  • CRLF két karakter sorozata: r и n, amely elválasztja a kérés első sorát, a fejléceket és az adatokat.
  • <КОМАНДА> - az egyik CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. A böngésző parancsot küld a szerverünknek GET, azaz "Küldje el a fájl tartalmát."
  • <URI> - egységes erőforrás-azonosító. Például, ha URI = /index.html, akkor az ügyfél lekéri az oldal főoldalát.
  • <ВЕРСИЯ HTTP> — a HTTP protokoll verziója a formátumban HTTP/X.Y. A ma leggyakrabban használt változat az HTTP/1.1.
  • <ЗАГОЛОВОК N> egy kulcs-érték pár a formátumban <КЛЮЧ>: <ЗНАЧЕНИЕ>, elküldjük a szervernek további elemzés céljából.
  • <ДАННЫЕ> — a szerver által a művelet végrehajtásához szükséges adatok. Gyakran egyszerű JSON vagy bármilyen más formátumban.

Válasz formátum

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

  • <КОД СТАТУСА> a művelet eredményét jelző szám. Szerverünk mindig 200-as állapotot ad vissza (sikeres művelet).
  • <ОПИСАНИЕ СТАТУСА> — az állapotkód karakterlánc reprezentációja. A 200-as állapotkód esetében ez OK.
  • <ЗАГОЛОВОК N> — a kérésben szereplő formátumú fejléc. Visszaküldjük a címeket Content-Length (fájl mérete) és Content-Type: text/html (visszaküldési adattípus).
  • <ДАННЫЕ> — a felhasználó által kért adatok. Esetünkben ez az út a képbe HTML.

fájl http_server.c (egyszálú szerver) fájlt tartalmaz common.h, amely a következő függvényprototípusokat tartalmazza:

Közös függvényprototípusok megjelenítése.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);

Leírjuk a funkcionális makrót is SAFE_CALL() és a függvény definiálva van fail(). A makró összehasonlítja a kifejezés értékét a hibával, és ha a feltétel igaz, meghívja a függvényt fail():

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

Funkció fail() kiírja az átadott argumentumokat a terminálnak (pl printf()), és leállítja a programot a kóddal 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);
}

Funkció new_server() a rendszerhívások által létrehozott "szerver" socket fájlleíróját adja vissza socket(), bind() и listen() és képes a bejövő kapcsolatok fogadására nem blokkoló módban.

Új_szerver() függvény megjelenítése

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

  • Vegye figyelembe, hogy a socket kezdetben nem blokkoló módban jön létre a zászló használatával SOCK_NONBLOCKígy a függvényben on_accept() (tovább) rendszerhívás accept() nem állította le a szál végrehajtását.
  • Ha reuse_port egyenlő true, akkor ez a funkció konfigurálja a foglalatot az opcióval SO_REUSEPORT keresztül setsockopt()ugyanazt a portot használja többszálú környezetben (lásd a „Többszálú szerver”).

Eseménykezelő on_accept() azután hívják meg, hogy az operációs rendszer eseményt generált EPOLLIN, ebben az esetben azt jelenti, hogy az új kapcsolat elfogadható. on_accept() elfogad egy új kapcsolatot, nem blokkoló módba kapcsolja és regisztrál egy eseménykezelőben on_recv() I/O reaktorban.

On_accept() függvény megjelenítése

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

Eseménykezelő on_recv() azután hívják meg, hogy az operációs rendszer eseményt generált EPOLLIN, ebben az esetben azt jelenti, hogy a kapcsolat regisztrálva van on_accept(), készen áll az adatok fogadására.

on_recv() beolvassa a kapcsolat adatait a HTTP kérés teljes beérkezéséig, majd regisztrál egy kezelőt on_send() HTTP-válasz küldéséhez. Ha az ügyfél megszakítja a kapcsolatot, a socket regisztrációja törlődik, és a használatával bezárásra kerül close().

Az on_recv() függvény megjelenítése

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

Eseménykezelő on_send() azután hívják meg, hogy az operációs rendszer eseményt generált EPOLLOUT, ami azt jelenti, hogy a kapcsolat regisztrálva van on_recv(), készen áll az adatok küldésére. Ez a függvény HTML-kódot tartalmazó HTTP-választ küld egy képpel az ügyfélnek, majd visszaállítja az eseménykezelőt on_recv().

On_send() függvény megjelenítése

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

És végül a fájlban http_server.c, funkcióban main() segítségével létrehozunk egy I/O reaktort reactor_new(), hozzon létre egy szerver socketet és regisztrálja, indítsa el a reaktort a használatával reactor_run() pontosan egy percig, majd felszabadítjuk az erőforrásokat, és kilépünk a programból.

A http_server.c megjelenítése

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

Ellenőrizzük, hogy minden a várt módon működik-e. Összeállítás (chmod a+x compile.sh && ./compile.sh a projektgyökérben) és indítsa el a saját maga által írt szervert, nyissa meg http://127.0.0.1:18470 a böngészőben, és nézze meg, mire számítottunk:

Teljes funkcionalitású csupasz C I/O reaktor

Teljesítménymérés

Mutasd meg az autóm műszaki adatait

$ 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

Mérjük meg egy egyszálú szerver teljesítményét. Nyissunk két terminált: az egyikben futunk ./http_server, más- wrk. Egy perc múlva a következő statisztikák jelennek meg a második terminálon:

$ 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

Egyszálú szerverünk percenként több mint 11 millió kérést tudott feldolgozni 100 kapcsolatból. Nem rossz eredmény, de lehet javítani?

Többszálú szerver

Ahogy fentebb említettük, az I/O reaktor külön szálakból hozható létre, ezáltal az összes CPU magot felhasználva. Alkalmazzuk ezt a megközelítést a gyakorlatban:

A http_server_multithreaded.c megjelenítése

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

Most minden szál birtokolja a sajátját reaktor:

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

Felhívjuk figyelmét, hogy a függvény argumentuma new_server() szószólói true. Ez azt jelenti, hogy a lehetőséget hozzárendeljük a szerver sockethez SO_REUSEPORThogy többszálú környezetben használjuk. További részleteket olvashat itt.

Második futam

Most mérjük meg egy többszálú szerver teljesítményét:

$ 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

Az 1 perc alatt feldolgozott kérelmek száma ~3.28-szorosára nőtt! De már csak ~XNUMX millió hiányzott a kerek számhoz, úgyhogy próbáljuk meg javítani.

Először nézzük meg a generált statisztikákat 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

CPU Affinity használata, összeállítás a -march=native, PGO, a találatok számának növekedése gyorsítótár, növekedés MAX_EVENTS és használja EPOLLET nem hozott jelentős teljesítménynövekedést. De mi történik, ha növeli az egyidejű kapcsolatok számát?

Statisztikák 352 egyidejű kapcsolathoz:

$ 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

Megkaptuk a kívánt eredményt, és ezzel egy érdekes grafikont, amely az 1 perc alatt feldolgozott kérések számának a kapcsolatok számától való függését mutatja:

Teljes funkcionalitású csupasz C I/O reaktor

Azt látjuk, hogy pár száz csatlakozás után mindkét szerver esetében meredeken csökken a feldolgozott kérések száma (a többszálas verzióban ez jobban észrevehető). Ez kapcsolódik a Linux TCP/IP verem megvalósításához? Nyugodtan írja meg a megjegyzésekben a grafikon viselkedésével és a többszálú és egyszálú opciók optimalizálásával kapcsolatos feltételezéseit.

Mint neves a megjegyzésekben ez a teljesítményteszt nem mutatja meg az I/O reaktor viselkedését valós terhelés alatt, mert szinte mindig a szerver interakcióba lép az adatbázissal, naplókat ad ki, titkosítást használ TLS stb., aminek következtében a terhelés egyenetlenné (dinamikussá) válik. A harmadik féltől származó komponensekkel együtt végzett teszteket az I/O proaktorról szóló cikkben végezzük el.

Az I/O reaktor hátrányai

Meg kell értenie, hogy az I/O reaktornak nincsenek hátrányai, nevezetesen:

  • Az I/O reaktor használata többszálú környezetben valamivel nehezebb, mert manuálisan kell kezelnie az áramlásokat.
  • A gyakorlat azt mutatja, hogy a legtöbb esetben a terhelés nem egyenletes, ami az egyik szál naplózásához vezethet, míg a másik munkával van elfoglalva.
  • Ha az egyik eseménykezelő blokkol egy szálat, akkor maga a rendszerválasztó is blokkol, ami nehezen fellelhető hibákhoz vezethet.

Megoldja ezeket a problémákat I/O proaktor, amely gyakran rendelkezik egy ütemezővel, amely egyenletesen osztja el a terhelést egy szálkészlet között, és rendelkezik egy kényelmesebb API-val is. Erről később, másik cikkemben lesz szó.

Következtetés

Itt ért véget az utunk az elmélettől egyenesen a profiler kipufogójáig.

Nem szabad ezen elidőzni, mert sok más, hasonlóan érdekes megközelítés létezik a hálózati szoftverek írására, különböző kényelmi és sebességű. Érdekes, véleményem szerint a linkek alább találhatók.

A következő alkalomig!

Érdekes projektek

Mit olvassak még?

Forrás: will.com

Hozzászólás