Fullständig bare-C I/O-reaktor

Fullständig bare-C I/O-reaktor

Inledning

I/O-reaktor (enkeltrådig händelseslinga) är ett mönster för att skriva högbelastad programvara, som används i många populära lösningar:

I den här artikeln kommer vi att titta på detaljerna i en I/O-reaktor och hur den fungerar, skriva en implementering på mindre än 200 rader kod och göra en enkel HTTP-serverprocess över 40 miljoner förfrågningar/min.

Förord

  • Artikeln skrevs för att hjälpa till att förstå I/O-reaktorns funktion och därför förstå riskerna med att använda den.
  • Kunskap om grunderna krävs för att förstå artikeln. C språk och viss erfarenhet av utveckling av nätverksapplikationer.
  • All kod är skriven på C-språk strikt enligt (varning: lång PDF) till C11-standard för Linux och tillgänglig på GitHub.

Varför det?

Med Internets växande popularitet började webbservrar behöva hantera ett stort antal anslutningar samtidigt, och därför prövades två tillvägagångssätt: blockering av I/O på ett stort antal OS-trådar och icke-blockerande I/O i kombination med ett händelseaviseringssystem, även kallat "systemväljare" (epoll/kkö/IOCP/etc).

Det första tillvägagångssättet innebar att skapa en ny OS-tråd för varje inkommande anslutning. Dess nackdel är dålig skalbarhet: operativsystemet kommer att behöva implementera många kontextövergångar и systemsamtal. De är dyra operationer och kan leda till brist på ledigt RAM med ett imponerande antal anslutningar.

Den modifierade versionen framhäver fast antal trådar (trådpool), vilket förhindrar systemet från att avbryta exekveringen, men introducerar samtidigt ett nytt problem: om en trådpool för närvarande är blockerad av långa läsoperationer, kommer andra sockets som redan kan ta emot data inte att kunna göra det.

Den andra metoden använder händelseaviseringssystem (systemväljare) från OS. Den här artikeln diskuterar den vanligaste typen av systemväljare, baserad på varningar (händelser, meddelanden) om beredskap för I/O-operationer, snarare än på meddelanden om deras slutförande. Ett förenklat exempel på dess användning kan representeras av följande blockschema:

Fullständig bare-C I/O-reaktor

Skillnaden mellan dessa metoder är följande:

  • Blockerar I/O-operationer uppskjuta användarflöde fram tillstills operativsystemet är korrekt defragmenterar inkommande IP-paket till byteström (TCP, tar emot data) eller så kommer det inte att finnas tillräckligt med utrymme tillgängligt i de interna skrivbuffertarna för efterföljande sändning via NIC (skicka data).
  • Systemväljare över tid meddelar programmet att OS redan defragmenterade IP-paket (TCP, datamottagning) eller tillräckligt med utrymme i interna skrivbuffertar redan tillgänglig (sända data).

För att sammanfatta det, att reservera en OS-tråd för varje I/O är ett slöseri med datorkraft, eftersom trådarna i verkligheten inte gör något användbart arbete (därav termen "programvaruavbrott"). Systemväljaren löser detta problem, vilket gör att användarprogrammet kan använda CPU-resurserna mycket mer ekonomiskt.

I/O-reaktormodell

I/O-reaktorn fungerar som ett lager mellan systemväljaren och användarkoden. Principen för dess funktion beskrivs av följande blockschema:

Fullständig bare-C I/O-reaktor

  • Låt mig påminna dig om att en händelse är ett meddelande om att en viss socket kan utföra en icke-blockerande I/O-operation.
  • En händelsehanterare är en funktion som anropas av I/O-reaktorn när en händelse tas emot, som sedan utför en icke-blockerande I/O-operation.

Det är viktigt att notera att I/O-reaktorn per definition är enkeltrådad, men det finns inget som hindrar konceptet från att användas i en flertrådig miljö i förhållandet 1 tråd: 1 reaktor, och därigenom återvinna alla CPU-kärnor.

genomförande

Vi kommer att placera det offentliga gränssnittet i en fil reactor.h, och implementering - in reactor.c. reactor.h kommer att bestå av följande meddelanden:

Visa deklarationer i reaktor.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);

I/O-reaktorstrukturen består av filbeskrivning väljare epoll и hashtabeller GHashTable, som mappar varje socket till CallbackData (struktur av en händelsehanterare och ett användarargument för det).

Visa Reactor och CallbackData

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

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

Observera att vi har aktiverat möjligheten att hantera ofullständig typ enligt index. I reactor.h vi deklarerar strukturen reactor, och i reactor.c vi definierar det och förhindrar därigenom användaren från att explicit ändra sina fält. Detta är ett av mönstren dölja data, som kortfattat passar in i C-semantik.

funktioner reactor_register, reactor_deregister и reactor_reregister uppdatera listan över sockets av intresse och motsvarande händelsehanterare i systemväljaren och hashtabellen.

Visa registreringsfunktioner

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

Efter att I/O-reaktorn har avlyssnat händelsen med deskriptorn fd, anropar den motsvarande händelsehanterare, till vilken den skickas fd, bitmask genererade händelser och en användarpekare till void.

Visa reactor_run() funktion

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

För att sammanfatta, kommer kedjan av funktionsanrop i användarkoden att ha följande form:

Fullständig bare-C I/O-reaktor

Enkeltrådad server

För att testa I/O-reaktorn under hög belastning kommer vi att skriva en enkel HTTP-webbserver som svarar på varje begäran med en bild.

En snabb referens till HTTP-protokollet

HTTP - det här är protokollet applikationsnivå, används främst för interaktion mellan server och webbläsare.

HTTP kan enkelt användas över transport protokoll TCP, skicka och ta emot meddelanden i ett angivet format Specifikation.

Begärformat

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

  • CRLF är en sekvens av två tecken: r и n, separerar den första raden i begäran, rubriker och data.
  • <КОМАНДА> - en av CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Webbläsaren skickar ett kommando till vår server GET, vilket betyder "Skicka mig innehållet i filen."
  • <URI> - enhetlig resursidentifierare. Till exempel, om URI = /index.html, sedan begär klienten webbplatsens huvudsida.
  • <ВЕРСИЯ HTTP> — version av HTTP-protokollet i formatet HTTP/X.Y. Den vanligaste versionen idag är HTTP/1.1.
  • <ЗАГОЛОВОК N> är ett nyckel-värdepar i formatet <КЛЮЧ>: <ЗНАЧЕНИЕ>, skickas till servern för vidare analys.
  • <ДАННЫЕ> — data som krävs av servern för att utföra operationen. Ofta är det enkelt JSON eller något annat format.

Svarsformat

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

  • <КОД СТАТУСА> är ett tal som representerar resultatet av operationen. Vår server kommer alltid att returnera status 200 (lyckad operation).
  • <ОПИСАНИЕ СТАТУСА> — Strängrepresentation av statuskoden. För statuskod 200 är detta OK.
  • <ЗАГОЛОВОК N> — Rubrik i samma format som i begäran. Vi kommer att returnera titlarna Content-Length (filstorlek) och Content-Type: text/html (returdatatyp).
  • <ДАННЫЕ> — uppgifter som begärs av användaren. I vårt fall är detta vägen till bilden in html.

fil http_server.c (entrådad server) inkluderar fil common.h, som innehåller följande funktionsprototyper:

Visa funktionsprototyper gemensamt.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);

Det funktionella makrot beskrivs också SAFE_CALL() och funktionen är definierad fail(). Makrot jämför uttryckets värde med felet och anropar funktionen om villkoret är sant fail():

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

Funktion fail() skriver ut de skickade argumenten till terminalen (som printf()) och avslutar programmet med koden 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);
}

Funktion new_server() returnerar filbeskrivningen för "server"-socket skapad av systemanrop socket(), bind() и listen() och kan acceptera inkommande anslutningar i ett icke-blockerande läge.

Visa new_server()-funktionen

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

  • Observera att socket initialt skapas i icke-blockerande läge med hjälp av flaggan SOCK_NONBLOCKså att i funktionen on_accept() (läs mer) systemanrop accept() stoppade inte körningen av tråden.
  • Om reuse_port lika true, då kommer den här funktionen att konfigurera uttaget med alternativet SO_REUSEPORT genom setsockopt()för att använda samma port i en flertrådig miljö (se avsnittet "Multi-threaded server").

Händelsehanterare on_accept() anropas efter att operativsystemet genererar en händelse EPOLLIN, i detta fall innebär det att den nya anslutningen kan accepteras. on_accept() accepterar en ny anslutning, växlar den till icke-blockerande läge och registrerar sig hos en händelsehanterare on_recv() i en I/O-reaktor.

Visa on_accept()-funktionen

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

Händelsehanterare on_recv() anropas efter att operativsystemet genererar en händelse EPOLLIN, i detta fall innebär det att anslutningen registrerats on_accept(), redo att ta emot data.

on_recv() läser data från anslutningen tills HTTP-begäran tas emot helt, sedan registrerar den en hanterare on_send() för att skicka ett HTTP-svar. Om klienten bryter anslutningen avregistreras uttaget och stängs med hjälp av close().

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

Händelsehanterare on_send() anropas efter att operativsystemet genererar en händelse EPOLLOUT, vilket betyder att anslutningen registreras on_recv(), redo att skicka data. Denna funktion skickar ett HTTP-svar som innehåller HTML med en bild till klienten och ändrar sedan händelsehanteraren tillbaka till on_recv().

Visa on_send()-funktionen

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

Och slutligen i filen http_server.c, i funktion main() vi skapar en I/O-reaktor med hjälp av reactor_new(), skapa en serversocket och registrera den, starta reaktorn med hjälp av reactor_run() i exakt en minut, och sedan släpper vi resurser och avslutar programmet.

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

Låt oss kontrollera att allt fungerar som förväntat. Sammanställa (chmod a+x compile.sh && ./compile.sh i projektroten) och starta den självskrivna servern, öppna http://127.0.0.1:18470 i webbläsaren och se vad vi förväntade oss:

Fullständig bare-C I/O-reaktor

Prestationsmätning

Visa mina bilspecifikationer

$ 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

Låt oss mäta prestandan för en enkeltrådig server. Låt oss öppna två terminaler: i den ena kör vi ./http_server, i en annan - wrk. Efter en minut kommer följande statistik att visas i den andra terminalen:

$ 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

Vår enkeltrådade server kunde behandla över 11 miljoner förfrågningar per minut från 100 anslutningar. Inget dåligt resultat, men kan det förbättras?

Flertrådad server

Som nämnts ovan kan I/O-reaktorn skapas i separata trådar och därigenom utnyttja alla CPU-kärnor. Låt oss omsätta detta tillvägagångssätt i praktiken:

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

Nu varje tråd äger sin egen reaktor:

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

Observera att funktionsargumentet new_server() talar true. Detta innebär att vi tilldelar alternativet till serversocket SO_REUSEPORTatt använda den i en flertrådig miljö. Du kan läsa mer detaljer här.

Andra körningen

Låt oss nu mäta prestandan för en flertrådig server:

$ 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

Antalet förfrågningar som behandlades på 1 minut ökade med ~3.28 gånger! Men vi hade bara ~XNUMX miljoner brist på det runda siffran, så låt oss försöka fixa det.

Låt oss först titta på statistiken som genereras perfekt:

$ 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

Använder CPU Affinity, sammanställning med -march=native, PGO, en ökning av antalet träffar cache, öka MAX_EVENTS och använda EPOLLET gav ingen nämnvärd prestationshöjning. Men vad händer om man ökar antalet samtidiga anslutningar?

Statistik för 352 samtidiga anslutningar:

$ 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

Det önskade resultatet erhölls, och med det en intressant graf som visar beroendet av antalet bearbetade förfrågningar på 1 minut på antalet anslutningar:

Fullständig bare-C I/O-reaktor

Vi ser att efter ett par hundra anslutningar sjunker antalet behandlade förfrågningar för båda servrarna kraftigt (i den flertrådiga versionen märks detta mer). Är detta relaterat till Linux TCP/IP-stackimplementeringen? Skriv gärna dina antaganden om detta beteende hos grafen och optimeringar för flertrådiga och enkeltrådade alternativ i kommentarerna.

hur noterade i kommentarerna visar detta prestandatest inte I/O-reaktorns beteende under verklig belastning, eftersom servern nästan alltid interagerar med databasen, matar ut loggar, använder kryptografi med TLS etc., som ett resultat av vilket belastningen blir ojämn (dynamisk). Tester tillsammans med tredjepartskomponenter kommer att utföras i artikeln om I/O-proaktorn.

Nackdelar med I/O-reaktor

Du måste förstå att I/O-reaktorn inte är utan sina nackdelar, nämligen:

  • Att använda en I/O-reaktor i en flertrådig miljö är något svårare, eftersom du måste hantera flödena manuellt.
  • Praxis visar att belastningen i de flesta fall är ojämn, vilket kan leda till att en tråd loggas medan en annan är upptagen med arbete.
  • Om en händelsehanterare blockerar en tråd kommer även systemväljaren själv att blockera, vilket kan leda till svåra att hitta buggar.

Löser dessa problem I/O proaktor, som ofta har en schemaläggare som jämnt fördelar belastningen till en pool av trådar, och som även har ett bekvämare API. Vi kommer att prata om det senare, i min andra artikel.

Slutsats

Det är här vår resa från teorin rakt in i profileravgaserna har tagit slut.

Du bör inte dröja vid detta, eftersom det finns många andra lika intressanta metoder för att skriva nätverksprogramvara med olika nivåer av bekvämlighet och hastighet. Intressant, enligt mig, finns länkar nedan.

Vi ses igen!

Intressanta projekt

Vad mer ska jag läsa?

Källa: will.com

Lägg en kommentar