Fuldt udstyret bare-C I/O-reaktor

Fuldt udstyret bare-C I/O-reaktor

Indledning

I/O reaktor (enkelt gevind begivenhedsløkke) er et mønster til at skrive software med høj belastning, der bruges i mange populære løsninger:

I denne artikel vil vi se på ins og outs af en I/O-reaktor, og hvordan den virker, skrive en implementering på mindre end 200 linjer kode og lave en simpel HTTP-serverproces over 40 millioner anmodninger/min.

Forord

  • Artiklen er skrevet for at hjælpe med at forstå I/O-reaktorens funktion og derfor forstå risiciene ved brug af den.
  • Kendskab til det grundlæggende er nødvendigt for at forstå artiklen. C sprog og en vis erfaring med udvikling af netværksapplikationer.
  • Al kode er skrevet i C-sprog strengt efter (forsigtig: lang PDF) til C11 standard til Linux og tilgængelig på GitHub.

Hvorfor det?

Med internettets voksende popularitet begyndte webservere at skulle håndtere et stort antal forbindelser samtidigt, og derfor blev to tilgange forsøgt: blokering af I/O på et stort antal OS-tråde og ikke-blokerende I/O i kombination med et hændelsesnotifikationssystem, også kaldet "systemvælger" (epoll/kqueue/IOCP/etc).

Den første tilgang involverede at oprette en ny OS-tråd for hver indkommende forbindelse. Dens ulempe er dårlig skalerbarhed: operativsystemet bliver nødt til at implementere mange kontekstovergange и systemopkald. Det er dyre operationer og kan føre til mangel på gratis RAM med et imponerende antal forbindelser.

Den ændrede version fremhæver fast antal tråde (trådpulje), hvilket forhindrer systemet i at gå ned, men introducerer samtidig et nyt problem: hvis en trådpulje i øjeblikket er blokeret af lange læseoperationer, så vil andre sockets, der allerede er i stand til at modtage data, ikke kunne gøre det så.

Den anden tilgang bruger hændelsesnotifikationssystem (systemvælger) leveret af OS. Denne artikel diskuterer den mest almindelige type systemvælger baseret på advarsler (hændelser, meddelelser) om parathed til I/O-operationer snarere end på meddelelser om deres afslutning. Et forenklet eksempel på dets brug kan repræsenteres af følgende blokdiagram:

Fuldt udstyret bare-C I/O-reaktor

Forskellen mellem disse tilgange er som følger:

  • Blokering af I/O-operationer suspendere brugerflow indtilindtil OS er korrekt defragmenterer indgående IP-pakker til byte stream (TCP, modtager data), eller der vil ikke være nok plads til rådighed i de interne skrivebuffere til efterfølgende afsendelse via NIC (sende data).
  • Systemvælger over tid giver programmet besked om, at OS allerede defragmenterede IP-pakker (TCP, datamodtagelse) eller nok plads i interne skrivebuffere allerede tilgængelig (sende data).

For at opsummere, er det spild af computerkraft at reservere en OS-tråd for hver I/O, for i virkeligheden udfører trådene ikke brugbart arbejde (deraf udtrykket "software afbrydelse"). Systemvælgeren løser dette problem, hvilket giver brugerprogrammet mulighed for at bruge CPU-ressourcer meget mere økonomisk.

I/O-reaktormodel

I/O-reaktoren fungerer som et lag mellem systemvælgeren og brugerkoden. Princippet for dets funktion er beskrevet af følgende blokdiagram:

Fuldt udstyret bare-C I/O-reaktor

  • Lad mig minde dig om, at en hændelse er en meddelelse om, at en bestemt socket er i stand til at udføre en ikke-blokerende I/O-operation.
  • En hændelseshåndtering er en funktion, der kaldes af I/O-reaktoren, når en hændelse modtages, som derefter udfører en ikke-blokerende I/O-operation.

Det er vigtigt at bemærke, at I/O-reaktoren per definition er enkelttrådet, men der er intet til hinder for, at konceptet kan bruges i et flertrådet miljø i et forhold på 1 tråd: 1 reaktor, og derved genanvende alle CPU-kerner.

implementering

Vi vil placere den offentlige grænseflade i en fil reactor.h, og implementering - i reactor.c. reactor.h vil bestå af følgende meddelelser:

Vis erklæringer 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 af filbeskrivelse vælger epoll и hash tabeller GHashTable, som kortlægger hver socket til CallbackData (struktur af en hændelseshandler og et brugerargument for det).

Vis reaktor- og tilbagekaldsdata

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

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

Bemærk venligst, at vi har aktiveret muligheden for at håndtere ufuldstændig type ifølge indekset. I reactor.h vi erklærer strukturen reactor, og i reactor.c vi definerer det og forhindrer derved brugeren i eksplicit at ændre sine felter. Dette er et af mønstrene skjule data, som kort og godt passer ind i C-semantik.

Funktioner reactor_register, reactor_deregister и reactor_reregister opdatere listen over sockets af interesse og tilsvarende hændelseshandlere i systemvælgeren og hashtabellen.

Vis 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 at I/O-reaktoren har opsnappet hændelsen med deskriptoren fd, kalder den den tilsvarende hændelseshandler, som den går videre til fd, bit maske genererede hændelser og en brugerpegepind til void.

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

For at opsummere vil kæden af ​​funktionskald i brugerkode have følgende form:

Fuldt udstyret bare-C I/O-reaktor

Enkelt gevind server

For at teste I/O-reaktoren under høj belastning, vil vi skrive en simpel HTTP-webserver, der svarer på enhver anmodning med et billede.

En hurtig reference til HTTP-protokollen

HTTP - dette er protokollen anvendelsesniveau, primært brugt til server-browser-interaktion.

HTTP kan nemt bruges over transportere protokol TCP, afsendelse og modtagelse af beskeder i et specificeret format specifikation.

Anmodningsformat

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

  • CRLF er en sekvens af to tegn: r и n, adskiller den første linje af anmodningen, overskrifter og data.
  • <КОМАНДА> - en af CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Browseren sender en kommando til vores server GET, hvilket betyder "Send mig indholdet af filen."
  • <URI>ensartet ressourceidentifikator. For eksempel, hvis URI = /index.html, så anmoder klienten om webstedets hovedside.
  • <ВЕРСИЯ HTTP> — version af HTTP-protokollen i formatet HTTP/X.Y. Den mest brugte version i dag er HTTP/1.1.
  • <ЗАГОЛОВОК N> er et nøgleværdi-par i formatet <КЛЮЧ>: <ЗНАЧЕНИЕ>, sendt til serveren for yderligere analyse.
  • <ДАННЫЕ> — data, der kræves af serveren for at udføre handlingen. Ofte er det enkelt JSON eller et hvilket som helst andet format.

Svarformat

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

  • <КОД СТАТУСА> er et tal, der repræsenterer resultatet af operationen. Vores server vil altid returnere status 200 (vellykket drift).
  • <ОПИСАНИЕ СТАТУСА> — strengrepræsentation af statuskoden. For statuskode 200 er dette OK.
  • <ЗАГОЛОВОК N> — overskrift i samme format som i anmodningen. Vi vil returnere titlerne Content-Length (filstørrelse) og Content-Type: text/html (returdatatype).
  • <ДАННЫЕ> — data anmodet af brugeren. I vores tilfælde er dette vejen til billedet ind HTML.

fil http_server.c (single threaded server) inkluderer fil common.h, som indeholder følgende funktionsprototyper:

Vis funktionsprototyper i fællesskab.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);

Den funktionelle makro er også beskrevet SAFE_CALL() og funktionen er defineret fail(). Makroen sammenligner værdien af ​​udtrykket med fejlen, og hvis betingelsen er sand, kalder den funktionen fail():

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

Funktion fail() udskriver de overførte argumenter til terminalen (som printf()) og afslutter 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() returnerer filbeskrivelsen af ​​"server"-socket oprettet af systemkald socket(), bind() и listen() og i stand til at acceptere indgående forbindelser i en ikke-blokerende tilstand.

Vis new_server() funktion

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

  • Bemærk, at socket oprindeligt oprettes i ikke-blokerende tilstand ved hjælp af flaget SOCK_NONBLOCKså det i funktionen on_accept() (læs mere) systemkald accept() stoppede ikke trådudførelsen.
  • Hvis reuse_port er lig med true, så konfigurerer denne funktion stikket med muligheden SO_REUSEPORT igennem setsockopt()at bruge den samme port i et multi-threaded miljø (se afsnittet "Multi-threaded server").

Event Handler on_accept() kaldet efter OS genererer en hændelse EPOLLIN, hvilket i dette tilfælde betyder, at den nye forbindelse kan accepteres. on_accept() accepterer en ny forbindelse, skifter den til ikke-blokerende tilstand og registrerer sig hos en hændelseshandler on_recv() i en I/O-reaktor.

Vis on_accept() funktion

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

Event Handler on_recv() kaldet efter OS genererer en hændelse EPOLLIN, i dette tilfælde betyder, at forbindelsen er registreret on_accept(), klar til at modtage data.

on_recv() læser data fra forbindelsen indtil HTTP-anmodningen er fuldstændig modtaget, så registrerer den en behandler on_send() at sende et HTTP-svar. Hvis klienten afbryder forbindelsen, afmeldes stikket og lukkes vha close().

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

Event Handler on_send() kaldet efter OS genererer en hændelse EPOLLOUT, hvilket betyder, at forbindelsen er registreret on_recv(), klar til at sende data. Denne funktion sender et HTTP-svar indeholdende HTML med et billede til klienten og ændrer derefter hændelseshandleren tilbage til on_recv().

Vis on_send() funktion

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

Og til sidst i filen http_server.c, i funktion main() vi skaber en I/O-reaktor vha reactor_new(), opret en server socket og registrer den, start reaktoren vha reactor_run() i præcis et minut, og så frigiver vi ressourcer og afslutter programmet.

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

Lad os tjekke, at alt fungerer som forventet. Kompilere (chmod a+x compile.sh && ./compile.sh i projektroden) og start den selvskrevne server, åbn http://127.0.0.1:18470 i browseren og se, hvad vi forventede:

Fuldt udstyret bare-C I/O-reaktor

Præstationsmåling

Vis mine 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

Lad os måle ydeevnen af ​​en enkelt-tråds server. Lad os åbne to terminaler: i den ene kører vi ./http_server, i en anden - wrk. Efter et minut vil følgende statistik blive vist i den anden terminal:

$ 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

Vores enkelttrådede server var i stand til at behandle over 11 millioner anmodninger i minuttet, der stammede fra 100 forbindelser. Ikke et dårligt resultat, men kan det forbedres?

Multithreaded server

Som nævnt ovenfor kan I/O-reaktoren oprettes i separate tråde, hvorved alle CPU-kerner udnyttes. Lad os omsætte denne tilgang i praksis:

Vis 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 hver tråd ejer sit eget reaktor:

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

Bemærk venligst, at funktionsargumentet new_server() handlinger true. Det betyder, at vi tildeler muligheden til serversocket SO_REUSEPORTat bruge det i et multi-threaded miljø. Du kan læse flere detaljer her.

Andet løb

Lad os nu måle ydeevnen af ​​en multi-threaded 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

Antallet af anmodninger behandlet på 1 minut steg med ~3.28 gange! Men vi manglede kun ~XNUMX millioner af det runde tal, så lad os prøve at rette op på det.

Lad os først se på den genererede statistik 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

Brug af CPU Affinity, kompilering med -march=native, PGO, en stigning i antallet af hits cache, øge MAX_EVENTS og bruge EPOLLET gav ikke en væsentlig stigning i ydeevnen. Men hvad sker der, hvis man øger antallet af samtidige forbindelser?

Statistik for 352 samtidige forbindelser:

$ 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 ønskede resultat blev opnået, og med det en interessant graf, der viser afhængigheden af ​​antallet af behandlede anmodninger på 1 minut af antallet af forbindelser:

Fuldt udstyret bare-C I/O-reaktor

Vi ser, at efter et par hundrede forbindelser falder antallet af behandlede anmodninger for begge servere kraftigt (i den flertrådede version er dette mere mærkbart). Er dette relateret til Linux TCP/IP-stakimplementeringen? Skriv gerne dine antagelser om denne opførsel af grafen og optimeringer for multi-threaded og single-threaded muligheder i kommentarerne.

Som noteret i kommentarerne viser denne ydeevnetest ikke I/O-reaktorens opførsel under reelle belastninger, fordi serveren næsten altid interagerer med databasen, udsender logfiler, bruger kryptografi med TLS osv., hvorved belastningen bliver uensartet (dynamisk). Tests sammen med tredjepartskomponenter vil blive udført i artiklen om I/O-proaktoren.

Ulemper ved I/O-reaktor

Du skal forstå, at I/O-reaktoren ikke er uden sine ulemper, nemlig:

  • At bruge en I/O-reaktor i et multi-threaded miljø er noget vanskeligere, fordi du bliver nødt til manuelt at styre strømmene.
  • Praksis viser, at belastningen i de fleste tilfælde er uensartet, hvilket kan føre til, at en tråd logger, mens en anden har travlt med arbejde.
  • Hvis en hændelseshandler blokerer en tråd, så vil selve systemvælgeren også blokere, hvilket kan føre til svære at finde fejl.

Løser disse problemer I/O proaktor, som ofte har en planlægger, der jævnt fordeler belastningen til en pool af tråde, og som også har en mere bekvem API. Vi vil tale om det senere, i min anden artikel.

Konklusion

Det er her, vores rejse fra teori lige ind i profilerudstødningen er slut.

Du bør ikke dvæle ved dette, for der er mange andre lige så interessante tilgange til at skrive netværkssoftware med forskellige niveauer af bekvemmelighed og hastighed. Interessante, efter min mening, er links givet nedenfor.

Indtil næste gang!

Interessante projekter

Hvad skal man ellers læse?

Kilde: www.habr.com

Tilføj en kommentar