Teljes funkcionalitású I/O reaktor tiszta C nyelven.

Teljes funkcionalitású I/O reaktor tiszta C nyelven.

Bevezetés

I/O reaktor (egyszálú eseményciklus) — ez egy mintázat nagy terhelésű szoftverek írására, amelyet számos népszerű megoldásban használnak:

A cikk célja, hogy bemutassa az I/O reaktor működését, megvalósítunk egy implementációt kevesebb mint 200 sorban, és lehetővé tesszük, hogy egy egyszerű HTTP szerver több mint 40 millió kérést dolgozzon fel percenként.

Bevezető

  • A cikket azzal a céllal írtuk, hogy segítsen megérteni az I/O reaktor működését és ezzel együtt tudatosítani a használatával kapcsolatos kockázatokat.
  • A cikk elsajátításához alapfokú tudásra van szükség a C nyelvből és némi hálózati alkalmazásfejlesztési tapasztalatra.
  • Minden kód C nyelven íródott szigorúan a (vigyázat: hosszú PDF) C11 standardnak megfelelően Linuxra, és elérhető a GitHub.

Miért van szükség erre?

Az internet népszerűsödésével a webszervereknek meg kellett oldaniuk a nagyszámú egyidejű kapcsolat kezelését, emiatt két megközelítést próbáltak ki: blokkoló I/O-t sok OS szállítószálon, és nem blokkoló I/O-t eseményértesítési rendszerrel kombinálva, amit "rendszerválasztónak" neveznek (epoll/kqueue/IOCP/etc).

Az első megközelítés új OS szálat hozott létre minden bejövő kapcsolat esetén. Ennek hátránya a rossz skálázhatóság: a operációs rendszernek számos kontextusváltást és rendszerhívástkell végrehajtania. Ezek költséges műveletek, és a kapcsolataink magas számához kellhet elegendő szabad RAM.

A módosított változat rögzített számú szálat (szálhalmaz) képez, ezzel megakadályozza, hogy a rendszer váratlanul leállítsa a végrehajtást, de ezzel újabb problémát is felvet: ha a szálkészlet jelenleg hosszú olvasási műveletekben van, más socketek, amelyek már képesek lennének adatok elfogadására, ezt nem tudják megtenni. A második megközelítés eseményértesítési rendszert

(rendszerválasztó) használ, amit az operációs rendszer biztosít. Ebben a cikkben a leggyakrabban előforduló eseményértesítési rendszert vizsgáljuk, amely az I/O műveletekhez való érés értesítéseken (események, értesítések) alapul, nem pedig az azok befejezéséről szóló értesítéseken. Az ilyen használat egyszerűbb példáját a következő blokkdiagram képviseli: A két megközelítés közötti különbség a következő: A blokkoló I/O műveletekfelfüggesztik

Teljes funkcionalitású I/O reaktor tiszta C nyelven.

a felhasználói szálat.

  • a felhasználói szálat. blokkoló I/O műveletek felhasználói szálat. Ez a támogatási kultúra hiánya, együtt a „törjük össze, hogy szebbé tegyük” elvvel, eltávolítja a fejlesztőket tőlük., amíg az operációs rendszer megfelelően defragmentálja a beérkező IP csomagokat byte-áramokba (TCP, adatátvitel) vagy amíg nem szabadul fel elegendő hely a belső cache-ben a következő NIC (adatküldés).
  • A rendszerlegedő egy kis idő elteltével értesíti a programot arról, hogy az operációs rendszer már defragmentálta az IP csomagokat (TCP, adatátvitel), vagy elegendő hely áll rendelkezésre a belső cache-ben már az adatküldéshez.

Összegzésül, az operációs rendszer áramának fenntartása minden I/O-hoz értelmetlen számítási teljesítménypazarlás, hiszen valójában az áramok nem végeznek hasznos munkát (innen ered a »programzsák«). A rendszerlegedő megoldja ezt a problémát, lehetővé téve a felhasználói program számára, hogy a CPU erőforrásait sokkal hatékonyabban hasznosítsa.

Az I/O reaktor modell

Az I/O reaktor a rendszerlegedő és a felhasználói kód közötti köztes rétegként működik. A működési elvét az alábbi blokkdiagram írja le:

Teljes funkcionalitású I/O reaktor tiszta C nyelven.

  • Emlékeztetem, hogy az esemény egy értesítés arról, hogy egy bizonyos socket készen áll egy nem blokkoló I/O művelet végrehajtására.
  • Az eseménykezelő egy olyan függvény, amelyet az I/O reaktor hív meg esemény megérkezésekor, és amely ezután végrehajt egy nem blokkoló I/O műveletet.

Fontos megjegyezni, hogy az I/O reaktor alapértelmezés szerint egy szálú, de semmi sem akadályozza meg a fogalom használatát többszálú környezetben, 1 szál: 1 reaktor arányban, így kihasználva az összes CPU magot.

Megvalósítás

A nyilvános interfészt az reactor.hfájlba helyezzük, a megvalósítást pedig a reactor.c. reactor.h a következő deklarációkból fog állni:

Deklarációk megjelenítése a reactor.h-ban

typedef struct reactor Reactor;

/*
 * A függvény mutató, amelyet az I/O reaktor hív meg, amikor esemény érkezik
 * a rendszerszelvényből.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * NULL-t ad vissza hibás esetben, nem-NULL mutatót a `Reactor`-ra
 * egyébként.
 */
Reactor *reactor_new(void);

/*
 * Felszabadítja a rendszerszelvényt, minden éppen regisztrált socketet
 * és magát az I/O reaktort.
 *
 * A következő függvények -1-et térnek vissza hibás esetben, 0-t a siker esetén.
 */
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);

/*
 * Elindít egy eseményciklust a `timeout` időtartammal.
 *
 * Ez a függvény átadja a vezérlést a hívó kódnak, ha az allokált idő letelt
 * vagy ha nincsenek regisztrált socketek.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

Az I/O reaktor struktúrája a következőkből áll fájlelemek leírója szelvény epoll és hash-táblázat GHashTable, amely minden socketet párosít a CallbackData (az eseménykezelő és a felhasználói argumentumot tartalmazó struktúra).

Mutasd a Reactor-t és a CallbackData-t

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

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

Kérjük, vegye figyelembe, hogy lehetőséget adtunk nem teljes típusú * mutatókkal való bánásmódra. A * struktúra bejelentésével * a `reactor`-t, definiálva ezzel megakadályozzuk, hogy a felhasználó közvetlenül módosítsa a mezőit. Ez az adatok elrejtésének egyik mintája, * amely szervesen illeszkedik a C nyelv szematájába. nem teljes típus ahol reactor.h a struktúra deklarálva van reactor, míg a reactor.c definiálásával megakadályozza, hogy a felhasználó közvetlenül megváltoztassa a mezőit. Ez egyike az adatok elrejtésének mintáinak, amely * összhangban van a C nyelv szematájával. adatok elrejtése, amely szervesen illeszkedik a C nyelv szematájába.

Funkciók reactor_register, reactor_deregister és reactor_reregister frissítik az érdeklődő socketek listáját és a megfelelő eseménykezelőket a rendszerszelvényben és a hash-táblázatban.

Mutasd be a regisztráló funkciókat

#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 észlelt egy eseményt a deskriptorral, fda megfelelő eseménykezelőt hívja, amely megkapja fd, az események bitmaskját és a felhasználói pointert Mutasd be a reactor_run() függvényt void.

Показать функцию 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) {
        // Hiba
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Idő lejárt
        case 0:
            result = 0;
            goto cleanup;
        // Sikeres művelet
        default:
            // Eseménykezelők hívása
            for (int i = 0; i table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

Összegezve, a felhasználói kódban a függvényhívások lánca a következőképpen fog kinézni:

Teljes funkcionalitású I/O reaktor tiszta C nyelven.

Egyszálú szerver

Az I/O reaktor magas terhelés alatt történő teszteléséhez egyszerű HTTP webszervert írunk, amely bármilyen kérésre képet válaszol.

Rövid áttekintés az HTTP protokollról

-kérések) hatalmas terheléssel — 6 millió esemény másodpercenként. Minden keresztül halad a — ez egy protokoll alkalmazási szinten, amely elsősorban a szerver és a böngésző közötti interakcióra használatos.

Az HTTP-t könnyen lehet használni szállítási protocol TCP, üzeneteket küldve és fogadva a megadott formátumban a specifikáció szerint.

Kérési formátum

CRLF
CRLF
CRLF
CRLF CRLF

  • CRLF — ez a két karakterből álló sorozat: r és n, amely elválasztja a kérdés első sorát, a fejlécet és az adatokat.
  • <КОМАНДА> — az egyik CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST http://10.132.1.202:8181/restconf/config/network-topology:network-topology/topology/topology-netconf/node/jun01/yang-ext:mount/junos-conf-root:configuration/junos-conf-protocols:protocols, PUT, TRACE.A böngésző a szerverünknek küldeni fogja a parancsot GET, ami azt jelenti, hogy "Kérlek küldd el a fájl tartalmát".
  • <URI> — egységes erőforrás azonosító. Például, ha az URI = /index.html, akkor az ügyfél a webhely főoldalát kéri.
  • <ВЕРСИЯ HTTP> — a HTTP protokoll verziója a következő formátumban HTTP/X.Y.A leggyakrabban használt verzió napjainkban — HTTP/1.1.
  • — ez egy kulcs-érték pár a következő formátumban :, amelyet a szervernek küldenek további elemzés céljából.
  • — az adat, amelyre a szervernek szüksége van a művelet végrehajtásához. Gyakran ez egyszerűen JSON vagy bármely más formátum.

Válasz formátuma

CRLF
CRLF
CRLF
CRLF CRLF

  • <КОД СТАТУСА> — ez a szám az operáció eredményét jelenti. A szerverünk mindig 200-as státuszt fog visszaadni (sikeres művelet).
  • <ОПИСАНИЕ СТАТУСА> — a státuszkód szöveges reprezentációja. A 200-as státuszkód esetén ez OK.
  • — a kéréshez hasonló formátumú fejléc. A következő fejlécet fogjuk visszaadni: Content-Length (a fájl mérete) és Content-Type: text/html (visszaadott adatok típusa).
  • — a felhasználó által kért adatok. Ebben az esetben ez a kép elérési útja a HTML.

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

Függvény prototípusok megjelenítése a common.h fájlban

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять новое соединение.
 */
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);

Továbbá le van írva a funkcionális makró SAFE_CALL() és a fail()függvény definiálva van. A makró az kifejezés értékét összehasonlítja a hibával, és ha a feltétel teljesült, meghívja a fail():

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

Funkció fail() végrehajtja a megadott argumentumokat a terminálra (mint printf()-re,) és leállítja a programot a 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() visszaadja a "szerver" socket fájl leíróját, amelyet a rendszerhívások hoznak létre socket(), bind() és listen() és képes fogadni a bejövő kapcsolódásokat nem blokkoló módban.

new_server() 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 SOCK_NONBLOCKflag segítségével, hogy a on_accept() (olvasson tovább) rendszerhívás accept() ne állítsa le a szál végrehajtását.
  • Ha reuse_port egyenlő igaz, akkor a függvény beállítja a socketet a SO_REUSEPORT opcióval setsockopt(), hogy ugyanazt a portot használhassa a több szálú környezetben (lásd a "Többszálú szerver" szakaszt).

Eseménykezelő on_accept() akkor hívódik meg, amikor az OS eseményt generál EPOLLIN, ez esetben azt jelenti, hogy új kapcsolatot lehet fogadni. on_accept() új kapcsolatot fogad, átkapcsolja nem blokkoló módba és regisztrálja az eseménykezelőhöz on_recv() az 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() akkor hívódik meg, amikor az OS eseményt generál EPOLLIN, ami azt jelenti, hogy a regisztrált kapcsolat on_accept(), készen áll az adatok fogadására.

on_recv() olvasza az adatokat a kapcsolaton, amíg a HTTP kérés teljesen el nem készül, majd regisztrálja a feldolgozó on_send() a HTTP válasz küldésére. Ha az ügyfél megszakítja a kapcsolatot, a socket törlésre kerül és lezárásra kerül a close().

Funkció megjelenítése on_recv()

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

    // Fogadjuk a bejövő adatokat, amíg a recv 0-t vagy hibát nem ad vissza
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Az ügyfél megszakította a kapcsolatot
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // a read hiba történt, ami nem a blokkoló hiba
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // A teljes HTTP kérés megérkezett az ügyféltől. Most regisztráljuk az eseménykezelőt
    // az adatok küldésére
    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() akkor hívódik meg, amikor az OS eseményt generál EPOLLOUT, ami azt jelenti, hogy a regisztrált kapcsolat on_recv(), készen áll az adatok küldésére. Ez a függvény HTTP választ küld az ügyfélnek, ami HTML-t tartalmaz képpel, majd ismét megváltoztatja az eseménykezelőt on_recv().

Funkció megjelenítése on_send()

statikus üres on_send(void *arg, int fd, uint32_t események) {\n    const char *tartalom = "<img "
 "src="https://habrastorage.org/webt/oh/wl/23/"
                          "ohwl23va3b-dioerobq_mbx4xaw.jpeg">";\n    char válasz[1024];\n    sprintf(válasz,\n            "HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "\n            "text/html" DOUBLE_CRLF "%s",\n            strlen(tartalom), tartalom);\n\n    SAFE_CALL(send(fd, válasz, strlen(válasz), 0), -1);\n    SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);\n}

Végül a fájlban http_server.c, a funkcióban main() a I/O reaktort a következő módon hozunk létre: reactor_new(), létrehozzuk a szerver socketet és regisztráljuk, elindítjuk a reaktort a következő módon: reactor_run() egészen egy percig, majd felszabadítjuk az erőforrásokat és kilépünk a programból.

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 jól működik. Fordítsuk le (chmod a+x compile.sh && ./compile.sh a projekt gyökerében) és indítsuk el az elkészített szervert, nyissuk meg http://127.0.0.1:18470 a böngészőben, és nézzük meg, amit vártunk:

Teljes funkcionalitású I/O reaktor tiszta C nyelven.

Teljesítménymérés

Mutasd a számítógépem jellemzőit

$ 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 az egyszálú szerver teljesítményét. Nyissunk meg két terminált: az egyiken indítsuk el ./http_server, a másikon pedig wrk. Egy perc múlva a második terminálban az alábbi statisztika fog megjelenni:

$ 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 több mint 11 millió kérést tudott kezelni egy perc alatt, 100 kapcsolatból. Nem rossz eredmény, de lehet ezt javítani?

Többszálú szerver

Ahogy korábban említettük, az I/O reaktort külön szálakban hozhatjuk létre, ezzel maximálisan kihasználva a CPU összes magját. Alkalmazzuk ezt a gyakorlatban:

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

Mostantól minden szál saját reaktort birtokol:

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

Figyeljen arra, hogy a függvény argumentumaként new_server() növelni kívánja igaz. Ez azt jelenti, hogy a szerver sockethez beállítjuk az opciót SO_REUSEPORT, hogy használhassuk azt több szálú környezetben. Részleteket olvashat itt.

Második kör

Most mérjük meg a 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 egy perc alatt feldolgozott kérések száma ~3.28-szorosára nőtt! De a kerek számhoz még ~két millió hiányzott, próbáljuk meg javítani.

Először nézzük meg az elkészített statisztikát perf:

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

Performancia számláló statisztikák a '.\/http_server_multithreaded'-hez:

     242446,314933      task-clock (msec)         #    4,000 CPU hasznosítás          
         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% az összes branch-ből        
           808 664      cache-misses                                                

      60,604330670 másodperc eltelt idő

CPU affinitás használata, lefordítás -march=native, PGO, a találatok számának növelése a cache, növelés MAX_EVENTS és a használat EPOLLET nem hozott jelentős teljesítménynövekedést. De mi történik, ha növeljük a párhuzamos kapcsolatok számát?

Statisztika 352 párhuzamos kapcsolattal:

$ 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"
Elindult 1 perces teszt @ http://127.0.0.1:18470
  8 szál és 352 kapcsolat
  Szál Statisztika   Átlag      Stdev     Max   +/ - Stdev
    Késlekedés     2.12ms    3.79ms  68.23ms   87.49%
    Req/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 kérés 1.00m, 5.48GB olvasva
Kérés/másodperc: 665789.26
Átvitel/másodperc:     93.34MB

A kívánt eredmény megvan, és vele egy érdekes grafikon, amely szemlélteti az egy perc alatt feldolgozott kérések számának függését a kapcsolatok számától:

Teljes funkcionalitású I/O reaktor tiszta C nyelven.

Látjuk, hogy néhány száz kapcsolat után a feldolgozott kérések száma mindkét server esetében jelentősen csökken (a több szálon futó verziónál ez észlelhetőbb). Ez összefüggésben állhat a Linux TCP/IP stack megvalósításával? Kérjük, ossza meg véleményét a grafikon ilyen viselkedéséről és a több szálas illetve egyszálas változatok optimalizálásáról a megjegyzésekben.

Mint megjegyezték, a megjegyzésekben, ez a teljesítményteszt nem mutatja az I/O reaktor viselkedését a valós terheléseken, mivel a server szinte mindig interakcióba lép az adatbázissal, naplókat ír ki, kriptográfiát használ TLS stb., emiatt a terhelés heterogén (dinamikus) lesz. A teszteket harmadik féltől származó komponensekkel együtt a következő I/O reaktor cikkben fogjuk elvégezni.

I/O reaktor hátrányai

Meg kell értenünk, hogy az I/O reaktor nem mentes a hátrányaitól, mégpedig:

  • Az I/O reaktor használata több szálas környezetben kissé bonyolultabb, mivel kézzel kell kezelni a szálakat.
  • A gyakorlat azt mutatja, hogy a legtöbb esetben a terhelés nem egyenletes, ami azt eredményezheti, hogy az egyik szál folyamatosan dolgozik, míg a másik a munka miatt le van terhelve.
  • Ha egy eseménykezelő blokkolja a szálat, akkor a rendszer kiválasztója is blokkolva lesz, ami nehezen észlelhető hibákhoz vezethet.

Ezeket a problémákat megoldja I/O reaktor, amely gyakran tartalmaz ütemezőt, amely egyenletesen elosztja a terhelést a szálak pooljában, és emellett egy kényelmesebb API-val rendelkezik. Erről később beszélek, egy másik cikkemben.

Zárás

Ezzel a gyakorlatunk elérkezett a professzionális profilozo kimenethez.

Nem érdemes itt megállni, hiszen számos más, szintén érdekes megközelítés létezik a hálózati szoftverek írására, különböző kényelmi és sebességi szintekkel. Az alábbiakban érdekesnek tartott hivatkozásokat találsz.

Viszontlátásra!

Érdekes projektek

Mit olvassak még?

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster