Volledig functionele I/O-reactor op kale C

Volledig functionele I/O-reactor op kale C

Introductie

I/O-reactor (enkele schroefdraad gebeurtenis lus) is een patroon voor het schrijven van software met hoge belasting, die in veel populaire oplossingen wordt gebruikt:

In dit artikel bekijken we de ins en outs van een I/O-reactor en hoe deze werkt, schrijven we een implementatie in minder dan 200 regels code en maken we een eenvoudig HTTP-serverproces van meer dan 40 miljoen verzoeken/min.

Voorwoord

  • Het artikel is geschreven om de werking van de I/O-reactor beter te begrijpen en daarmee de risico's bij het gebruik ervan te begrijpen.
  • Het begrijpen van de basisprincipes is vereist om het artikel te begrijpen. C-taal en enige ervaring met de ontwikkeling van netwerkapplicaties.
  • Alle code is strikt in de C-taal geschreven volgens (let op: lange pdf) volgens C11-standaard voor Linux en beschikbaar op GitHub.

Waarom doen?

Met de groeiende populariteit van internet begonnen webservers een groot aantal verbindingen tegelijkertijd te verwerken, en daarom werden twee benaderingen geprobeerd: het blokkeren van I/O op een groot aantal OS-threads en niet-blokkerende I/O in combinatie met een gebeurtenismeldingssysteem, ook wel “systeemkiezer” genoemd (epol/in de rij staan/IOCP/enz).

De eerste aanpak bestond uit het creëren van een nieuwe OS-thread voor elke inkomende verbinding. Het nadeel is een slechte schaalbaarheid: het besturingssysteem zal er veel moeten implementeren contextovergangen и systeemoproepen. Het zijn dure handelingen en kunnen leiden tot een gebrek aan vrij RAM-geheugen met een indrukwekkend aantal verbindingen.

De gewijzigde versie benadrukt vast aantal draden (threadpool), waardoor wordt voorkomen dat het systeem crasht, maar tegelijkertijd een nieuw probleem wordt geïntroduceerd: als een threadpool momenteel wordt geblokkeerd door lange leesbewerkingen, zullen andere sockets die al gegevens kunnen ontvangen dit niet kunnen doen Dus.

De tweede benadering maakt gebruik van meldingssysteem voor evenementen (systeemkiezer) geleverd door het besturingssysteem. In dit artikel wordt het meest voorkomende type systeemkiezer besproken, gebaseerd op waarschuwingen (gebeurtenissen, meldingen) over de gereedheid voor I/O-bewerkingen, in plaats van op meldingen over de voltooiing ervan. Een vereenvoudigd voorbeeld van het gebruik ervan kan worden weergegeven door het volgende blokdiagram:

Volledig functionele I/O-reactor op kale C

Het verschil tussen deze benaderingen is als volgt:

  • I/O-bewerkingen blokkeren opschorten gebruikersstroom tottotdat het besturingssysteem correct is defragmenteert inkomend IP-pakketten naar bytestream (TCP, gegevens ontvangen) of er zal niet voldoende ruimte beschikbaar zijn in de interne schrijfbuffers voor het daaropvolgende verzenden via NIC (gegevens verzenden).
  • Systeemkiezer na een tijdje meldt het programma dat het besturingssysteem reeds gedefragmenteerde IP-pakketten (TCP, gegevensontvangst) of voldoende ruimte in interne schrijfbuffers reeds beschikbaar (gegevens verzenden).

Kortom: het reserveren van een OS-thread voor elke I/O is een verspilling van rekenkracht, omdat de threads in werkelijkheid geen nuttig werk doen (hier komt de term vandaan). "software-onderbreking"). De systeemselector lost dit probleem op, waardoor het gebruikersprogramma CPU-bronnen veel zuiniger kan gebruiken.

I/O-reactormodel

De I/O-reactor fungeert als laag tussen de systeemkiezer en de gebruikerscode. Het principe van de werking ervan wordt beschreven door het volgende blokdiagram:

Volledig functionele I/O-reactor op kale C

  • Ik wil u eraan herinneren dat een gebeurtenis een melding is dat een bepaalde socket een niet-blokkerende I/O-bewerking kan uitvoeren.
  • Een gebeurtenishandler is een functie die door de I/O-reactor wordt aangeroepen wanneer een gebeurtenis wordt ontvangen en die vervolgens een niet-blokkerende I/O-bewerking uitvoert.

Het is belangrijk op te merken dat de I/O-reactor per definitie single-threaded is, maar niets weerhoudt het concept ervan om te worden gebruikt in een multi-threaded omgeving met een verhouding van 1 thread: 1 reactor, waardoor alle CPU-kernen worden gerecycled.

uitvoering

We plaatsen de publieke interface in een bestand reactor.h, en implementatie - in reactor.c. reactor.h zal bestaan ​​uit de volgende aankondigingen:

Toon declaraties in reactor.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);

De I/O-reactorstructuur bestaat uit bestandsbeschrijving keuzeschakelaar epol и hash-tabellen GHashTable, waarmee elke socket wordt toegewezen CallbackData (structuur van een gebeurtenishandler en een gebruikersargument ervoor).

Toon Reactor- en CallbackData

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

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

Houd er rekening mee dat we de mogelijkheid tot afhandeling hebben ingeschakeld onvolledige soort volgens de index. IN reactor.h wij verklaren de structuur reactoren in reactor.c we definiëren het, waardoor wordt voorkomen dat de gebruiker de velden expliciet wijzigt. Dit is een van de patronen gegevens verbergen, wat beknopt past in de C-semantiek.

functies reactor_register, reactor_deregister и reactor_reregister werk de lijst met interessante sockets en bijbehorende gebeurtenishandlers in de systeemkiezer en hashtabel bij.

Toon registratiefuncties

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

Nadat de I/O-reactor de gebeurtenis met de descriptor heeft onderschept fd, roept het de corresponderende gebeurtenishandler aan, waarnaar het doorgaat fd, beetje masker gegenereerde gebeurtenissen en een gebruikersaanwijzer naar void.

Toon de reactor_run()-functie

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

Samenvattend zal de keten van functieaanroepen in gebruikerscode de volgende vorm aannemen:

Volledig functionele I/O-reactor op kale C

Single-threaded-server

Om de I/O-reactor onder hoge belasting te testen, zullen we een eenvoudige HTTP-webserver schrijven die op elk verzoek reageert met een afbeelding.

Een korte verwijzing naar het HTTP-protocol

HTTP - dit is het protocol toepassingsniveau, voornamelijk gebruikt voor server-browserinteractie.

HTTP kan eenvoudig worden gebruikt vervoer protocol TCP, het verzenden en ontvangen van berichten in een opgegeven formaat specificatie.

Aanvraagformaat

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

  • CRLF is een reeks van twee karakters: r и n, waarbij de eerste regel van het verzoek, headers en gegevens worden gescheiden.
  • <КОМАНДА> - een van de CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. De browser stuurt een commando naar onze server GET, wat betekent "Stuur mij de inhoud van het bestand."
  • <URI> - uniforme bronidentificatie. Als URI bijvoorbeeld = /index.html, dan vraagt ​​de klant de hoofdpagina van de site op.
  • <ВЕРСИЯ HTTP> — versie van het HTTP-protocol in het formaat HTTP/X.Y. De meest gebruikte versie van vandaag is HTTP/1.1.
  • <ЗАГОЛОВОК N> is een sleutelwaardepaar in de indeling <КЛЮЧ>: <ЗНАЧЕНИЕ>, verzonden naar de server voor verdere analyse.
  • <ДАННЫЕ> — gegevens die de server nodig heeft om de bewerking uit te voeren. Vaak is het eenvoudig JSON of een ander formaat.

Antwoordformaat

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

  • <КОД СТАТУСА> is een getal dat het resultaat van de bewerking vertegenwoordigt. Onze server retourneert altijd de status 200 (succesvolle bewerking).
  • <ОПИСАНИЕ СТАТУСА> — tekenreeksweergave van de statuscode. Voor statuscode 200 is dit OK.
  • <ЗАГОЛОВОК N> — header van hetzelfde formaat als in het verzoek. We zullen de titels retourneren Content-Length (bestandsgrootte) en Content-Type: text/html (gegevenstype retourneren).
  • <ДАННЫЕ> — gegevens opgevraagd door de gebruiker. In ons geval is dit het pad naar de afbeelding in HTML.

file http_server.c (server met enkele thread) bevat bestand common.h, dat de volgende functieprototypes bevat:

Toon functieprototypes gemeenschappelijk.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);

Ook wordt de functionele macro beschreven SAFE_CALL() en de functie is gedefinieerd fail(). De macro vergelijkt de waarde van de expressie met de fout en roept de functie aan als de voorwaarde waar is fail():

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

Functie fail() drukt de doorgegeven argumenten af ​​naar de terminal (zoals printf()) en beëindigt het programma met de code 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);
}

Functie new_server() retourneert de bestandsdescriptor van de "server"-socket die is gemaakt door systeemaanroepen socket(), bind() и listen() en in staat om inkomende verbindingen te accepteren in een niet-blokkerende modus.

Toon new_server() functie

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

  • Houd er rekening mee dat de socket aanvankelijk in niet-blokkerende modus wordt gemaakt met behulp van de vlag SOCK_NONBLOCKdus dat in de functie on_accept() (lees meer) systeemoproep accept() heeft de uitvoering van de thread niet gestopt.
  • als reuse_port is gelijk aan true, dan zal deze functie de socket configureren met de optie SO_REUSEPORT door middel van setsockopt()om dezelfde poort te gebruiken in een multi-threaded omgeving (zie sectie “Multi-threaded server”).

Gebeurtenisbehandelaar on_accept() aangeroepen nadat het besturingssysteem een ​​gebeurtenis heeft gegenereerd EPOLLIN, wat in dit geval betekent dat de nieuwe verbinding kan worden geaccepteerd. on_accept() accepteert een nieuwe verbinding, schakelt deze over naar de niet-blokkerende modus en registreert zich bij een gebeurtenishandler on_recv() in een I/O-reactor.

Toon on_accept() functie

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

Gebeurtenisbehandelaar on_recv() aangeroepen nadat het besturingssysteem een ​​gebeurtenis heeft gegenereerd EPOLLIN, in dit geval betekent dit dat de verbinding is geregistreerd on_accept(), klaar om gegevens te ontvangen.

on_recv() leest gegevens van de verbinding totdat het HTTP-verzoek volledig is ontvangen en registreert vervolgens een handler on_send() om een ​​HTTP-antwoord te verzenden. Als de client de verbinding verbreekt, wordt de socket afgemeld en gesloten met behulp van close().

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

Gebeurtenisbehandelaar on_send() aangeroepen nadat het besturingssysteem een ​​gebeurtenis heeft gegenereerd EPOLLOUT, wat betekent dat de verbinding is geregistreerd on_recv(), klaar om gegevens te verzenden. Deze functie verzendt een HTTP-antwoord met HTML met een afbeelding naar de client en wijzigt vervolgens de gebeurtenishandler terug naar on_recv().

Toon on_send() functie

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

En ten slotte in het bestand http_server.c, in functie main() we creëren een I/O-reactor met behulp van reactor_new(), maak een server socket en registreer deze, start de reactor met behulp van reactor_run() gedurende precies één minuut, en dan geven we bronnen vrij en verlaten we het programma.

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

Laten we controleren of alles werkt zoals verwacht. Compileren (chmod a+x compile.sh && ./compile.sh in de projectroot) en start de zelfgeschreven server, open http://127.0.0.1:18470 in de browser en kijk wat we verwachtten:

Volledig functionele I/O-reactor op kale C

Prestatiemeting

Toon mijn autospecificaties

$ 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

Laten we de prestaties van een single-threaded server meten. Laten we twee terminals openen: in één zullen we rennen ./http_server, op een andere - werk. Na een minuut worden de volgende statistieken weergegeven in de tweede 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

Onze single-threaded server kon ruim 11 miljoen verzoeken per minuut verwerken, afkomstig van 100 verbindingen. Geen slecht resultaat, maar kan het verbeterd worden?

Multithreaded-server

Zoals hierboven vermeld, kan de I/O-reactor in afzonderlijke threads worden gemaakt, waardoor alle CPU-kernen worden gebruikt. Laten we deze aanpak in de praktijk brengen:

Toon 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 elk draadje bezit de zijne reactor:

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

Houd er rekening mee dat het functieargument new_server() acts true. Dit betekent dat we de optie toewijzen aan de server socket SO_REUSEPORTom het te gebruiken in een omgeving met meerdere threads. U kunt meer details lezen hier.

Tweede run

Laten we nu de prestaties van een multi-threaded server meten:

$ 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

Het aantal verwerkte verzoeken in 1 minuut is met ~3.28 keer toegenomen! Maar we kwamen slechts ~XNUMX miljoen tekort aan het ronde getal, dus laten we proberen dat op te lossen.

Laten we eerst eens kijken naar de gegenereerde statistieken 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-affiniteit gebruiken, compilatie met -march=native, PGO, een toename van het aantal hits cache, toename MAX_EVENTS en gebruiken EPOLLET leverde geen noemenswaardige prestatieverbetering op. Maar wat gebeurt er als je het aantal gelijktijdige verbindingen vergroot?

Statistieken voor 352 gelijktijdige verbindingen:

$ 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

Het gewenste resultaat werd verkregen, en daarmee een interessante grafiek die de afhankelijkheid van het aantal verwerkte verzoeken in 1 minuut laat zien van het aantal verbindingen:

Volledig functionele I/O-reactor op kale C

We zien dat na een paar honderd verbindingen het aantal verwerkte verzoeken voor beide servers sterk daalt (in de multi-threaded versie is dit meer merkbaar). Heeft dit te maken met de implementatie van de Linux TCP/IP-stack? Voel je vrij om je aannames over dit gedrag van de grafiek en optimalisaties voor multi-threaded en single-threaded opties in de commentaren te schrijven.

Als dat is genoteerd in de commentaren toont deze prestatietest niet het gedrag van de I/O-reactor onder echte belasting, omdat de server bijna altijd communiceert met de database, logs uitvoert, cryptografie gebruikt met TLS enz., waardoor de belasting niet-uniform (dynamisch) wordt. Tests samen met componenten van derden worden uitgevoerd in het artikel over de I/O-proactor.

Nadelen van I/O-reactor

Je moet begrijpen dat de I/O-reactor niet zonder nadelen is, namelijk:

  • Het gebruik van een I/O-reactor in een omgeving met meerdere threads is iets moeilijker, omdat u zult de stromen handmatig moeten beheren.
  • De praktijk leert dat de belasting in de meeste gevallen niet-uniform is, wat ertoe kan leiden dat de ene thread vastloopt terwijl de andere bezig is met werken.
  • Als één gebeurtenishandler een thread blokkeert, blokkeert de systeemselector zelf ook, wat kan leiden tot moeilijk te vinden bugs.

Lost deze problemen op I/O-proactor, dat vaak een planner heeft die de belasting gelijkmatig verdeelt over een pool van threads, en ook een handiger API heeft. We zullen er later over praten, in mijn andere artikel.

Conclusie

Dit is waar onze reis van theorie rechtstreeks naar de uitlaat van de profiler ten einde is gekomen.

Je moet hier niet bij stilstaan, want er zijn veel andere, even interessante benaderingen voor het schrijven van netwerksoftware met verschillende niveaus van gemak en snelheid. Interessant, naar mijn mening, links worden hieronder gegeven.

Tot ziens!

Interessante projecten

Wat moet je nog meer lezen?

Bron: www.habr.com

Voeg een reactie