Fuldt funktionel I/O reaktor på rå C

Fuldt funktionel I/O reaktor på rå C

Introduktion

I/O reaktor (enkernel begivenhedscyklussen) — er et mønster til at skrive højt belastet software, der bruges i mange populære løsninger:

I denne artikel vil vi undersøge de indre funktioner af I/O reaktoren og princippet bag dens drift, implementere en løsning på mindre end 200 linjer kode og få en simpel HTTP-server til at håndtere over 40 millioner forespørgsler/min.

Forord

  • Artiklen er skrevet med det formål at hjælpe med at forstå funktionen af I/O reaktoren, og dermed også at erkende risiciene ved dens brug.
  • For at forstå artiklen kræves kendskab til grundlæggende C-sprog og lidt erfaring med udvikling af netapplikationer.
  • Al koden er skrevet i C-strengt i henhold til (advarsel: lang PDF) C11-standard til Linux og er tilgængelig på GitHub.

Hvorfor er det nødvendigt?

Med den stigende popularitet af Internettet blev det nødvendigt for webservere at kunne håndtere et stort antal forbindelser samtidigt, hvilket resulterede i to tilgange: blokkerende I/O med mange OS-tråde og ikke-blokerende I/O i kombination med et begivenhedsovervågningssystem, også kaldet 'systemvælger' (epoll/kqueue/IOCP/etc).

Den første tilgang omfattede oprettelsen af en ny OS-tråd for hver indgående forbindelse. Dens ulempe er dårlig skalérbarhed: operativsystemet skal håndtere mange context switches og systemopkald.Disse er dyre operationer, og de kan føre til mangel på ledig RAM ved et stort antal forbindelser.

Den modificerede version tildeler et fast antal tråde (trådpool), hvilket forhindrer systemet i at stoppe udførelsen af sig selv, men samtidig introducerer et nyt problem: hvis trådpoolen i øjeblikket blokerer langvarige læseoperationer, vil andre sockets, der allerede kan modtage data, ikke være i stand til at gøre dette.

Den anden tilgang anvender begivenhedsovervågningssystemet (systemvælger), som tilbydes af OS. I denne artikel dækkes den mest almindeligt anvendte type systemvælger, der er baseret på meddelelser (begivenheder, notifikationer) om beredskab til I/O-operationer snarere end på meddelelser om deres afslutning.Et forenklet eksempel på brugen kan repræsenteres ved følgende flowdiagram:

Fuldt funktionel I/O reaktor på rå C

Forskellen mellem de to tilgange er som følger:

  • Blokerende I/O-operationer suspenderer brugertråden indtil, indtil OS korrekt defragmenterer de indkommende IP-pakker til byte-strøm (TCP, datahentning) eller ikke frigør tilstrækkelig plads i interne skrivebuffere til efterfølgende afsendelse via NIC (dataafsendelse).
  • Systemvælgeren underretter programmet efter et stykke tid om, at OS har defragmenteret IP-pakker (TCP, datahentning) eller tilstrækkelig plads i interne skrivebuffere allerede er tilgængelig (dataafsendelse). allerede For at opsummere, at reservere OS-strømmen for hver I/O er spild af computerkraft, fordi strømme i virkeligheden ikke er optaget af nyttigt arbejde (her stammer termen

"programafbrydelse" ). Systemvælgeren løser dette problem ved at tillade brugerprogrammet at forbruge CPU-ressourcer langt mere effektivt.I/O-reaktor-modellen

I/O-reaktor fungerer som et lag mellem systemvælgeren og brugerens kode. Princippet for dets arbejde er beskrevet i følgende flowdiagram:

Lad mig minde om, at en hændelse er en underretning om, at en bestemt socket er i stand til at udføre en ikke-blokerende I/O-operation.

Fuldt funktionel I/O reaktor på rå C

  • Hændelseshåndterer er en funktion, som kaldes af I/O-reaktoren ved modtagelse af en hændelse, som derefter udfører en ikke-blokerende I/O-operation.
  • Det er vigtigt at bemærke, at I/O-reaktoren per definition er enkeltrådet, men intet forhindrer at anvende konceptet i en flertrådet miljø i forhold til 1 tråd: 1 reaktor, hvilket udnytter alle CPU-kerner.

Den offentlige grænseflade placerer vi i filen

Implementering

reactor.h , og implementeringen ireactor.c vil bestå af følgende erklæringer:. , og implementeringen i Vis erklæringerne i reactor.h

Vis annoncer i reactor.h

typedef struct reactor Reactor;

/*
 * En pegep som vil blive kaldt af I/O reaktoren, når der modtages
 * et event fra systemvælgeren.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Returnerer `NULL` ved fejl, ikke-`NULL` pointer til `Reactor` i
 * modsætning tilfældet.
 */
Reactor *reactor_new(void);

/*
 * Frigør systemvælgeren, alle registrerede sockets på nuværende
 * tidspunkt og selve I/O reaktoren.
 *
 * Følgende funktioner returnerer -1 ved fejl, 0 ved succes.
 */
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);

/*
 * Starter en event-loop med timeout `timeout`.
 *
 * Denne funktion vil overgive kontrol til den kaldende kode, hvis den tildelte tid er udløbet
 * eller/og der ikke er registrerede sockets.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

Strukturen af I/O reaktoren består af fildescriptor vælgeren epoll og hash-tabel GHashTable, som matcher hver socket med CallbackData (struktur fra event handler og brugerargument til den).

Vis Reactor og CallbackData

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

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

Bemærk, at vi har udnyttet evnen til at håndtere ufuldstændig type ved pointer. I , og implementeringen i deklærer vi strukturen reactor, og der sendes noget til den, vil flytning af denne task til sektionen vil bestå af følgende erklæringer: vi definerer den, hvilket forhindrer brugeren i at ændre dens felter direkte. Dette er et af mønstrene dataskjult, der elegant passer ind i C's semantik.

Functions reactor_register, reactor_deregister og reactor_reregister opdaterer listen over interessante sockets og de tilsvarende event-handlere i systemvælgeren og i hash-tabellen.

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

Når I/O reaktoren har opfanget et event fra descriptoren fd, kalder den den tilsvarende event-handler, som sender fd, bitmaske for de genererede events og brugerpegeren til void.

Vis funktionen 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) {
        // Fejl
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Tid udløbet
        case 0:
            result = 0;
            goto cleanup;
        // Operationen var succesfuld
        default:
            // Udfør event-handlerne
            for (int i = 0; i table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

Sammenfattende vil kæden af funktionsopkald i brugerens kode se således ud:

Fuldt funktionel I/O reaktor på rå C

Enkelt-trådet server

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

Kort information om HTTP-protokollen

HTTP — er en protokol på applikationsniveau, der primært anvendes til kommunikation mellem server og browser.

HTTP kan nemt anvendes ovenfor transportniveauet protokol TCP, ved at sende og modtage beskeder i det format, der er defineret af specifikationen.

Anmodningsformat

CRLF
CRLF
CRLF
CRLF CRLF

  • CRLF — er en sekvens af to tegn: r og n, der adskiller den første linje af anmodningen, hovedoverskrifterne og dataene.
  • <КОМАНДА> — en af CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Browseren vil sende kommandoen til vores server, der betyder 'Send mig indholdet af filen'. GET, hvilket betyder "Send mig indholdet af filen."
  • den ensartede ressourceidentifikator — . For eksempel, hvis URI =, vil klienten anmode om startsiden af websitet. /index.html, så klienten anmoder om hovedsiden på webstedet.
  • — versionen af HTTP-protokollen i formatet HTTP/X.Y. . Den mest anvendte version i dag erHTTP/1.1
    .
  • — dette er et nøgle-værdi par i formatet : , der sendes til serveren til videre behandling.
  • — de data, serveren har brug for for at udføre operationen. Ofte er det blot eller et hvilket som helst andet format. JSON Responsformat

CRLF
CRLF
CRLF
CRLF CRLF

CRLF
CRLF
CRLF
CRLF CRLF

  • <КОД СТАТУСА> — dette tal repræsenterer resultatet af en operation. Vores server vil altid returnere status 200 (vellykket operation).
  • <ОПИСАНИЕ СТАТУСА> — den strenge repræsentation af statuskoden. For statuskode 200 er det OK.
  • — dette er et nøgle-værdi par i formatet — headeren i samme format som i anmodningen. Vi vil returnere headers Content-Length (filstørrelse) og Content-Type: text/html (typen af de returnerede data).
  • — de data, serveren har brug for for at udføre operationen. Ofte er det blot — de data, der anmodes om af brugeren. I vores tilfælde er det stien til billedet i HTML.

Fil http_server.c (enkelttrådet server) inkluderer filen common.h, som indeholder følgende funktionsprototyper:

Vis funktionsprototyper i common.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);

Derudover er der beskrevet en funktionel makro SAFE_CALL() og funktionen fail(). Makroen sammenligner værdien af udtrykket med fejlen, og hvis betingelsen er opfyldt, kaldes funktionen fail():

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

Funktion fail() den udskriver de angivne 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 en filbeskrivelses 'server' socket, oprettet af systemopkaldene socket(), bind() og listen() og i stand til at acceptere indgående forbindelser i ikke-blokerende tilstand.

Vis funktionen new_server()

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

kaldes, efter at OS har genereret en hændelse on_accept() EPOLLIN , hvilket i dette tilfælde betyder, at en ny forbindelse kan accepteres.accepterer en ny forbindelse, skifter den til ikke-blokerende tilstand og registrerer med hændelseshåndtereren on_accept() on_recv() i I/O reaktoren. Vis funktionen on_accept()

Vis funktionen on_accept()

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

kaldes, efter at OS har genereret en hændelse i I/O reaktoren. EPOLLIN , hvilket i dette tilfælde betyder, at en ny forbindelse kan accepteres., hvilket betyder, at forbindelsen, der er registreret on_accept(), er klar til at modtage data.

i I/O reaktoren. læser data fra forbindelsen, indtil HTTP-anmodningen er helt modtaget, hvorefter den registrerer behandleren on_send() til at sende HTTP-svaret. Hvis klienten afbryder forbindelsen, bliver socket'en afregistreret og lukket ved hjælp af close().

Vis funktionen on_recv()

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

    // Modtager input, indtil recv returnerer 0 eller en fejl
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Klienten afbrød forbindelsen
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read returnerede en fejl, der ikke er en fejl, hvor kaldet blokerer
    // tråden
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Fuld HTTP-anmodning modtaget fra klienten. Nu registrerer vi behandleren
    // for events for at sende data
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

kaldes, efter at OS har genereret en hændelse on_send() EPOLLIN EPOLLOUT, hvilket betyder, at forbindelsen, der er registreret i I/O reaktoren., er klar til at sende data. Denne funktion sender et HTTP-svar, der indeholder HTML med et billede, til klienten, og ændrer derefter begivenhedsbehandleren tilbage til i I/O reaktoren..

Vis funktionen on_send()

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 slut, i filen http_server.c, i funktionen main() opretter vi en I/O reaktor ved hjælp af reactor_new(), opretter en server socket og registrerer den, kører reaktoren med reactor_run() i præcist et minut, og derefter frigiver vi ressourcerne og forlader 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 kontrollere, at alt fungerer som det skal. Kompilerer (chmod a+x compile.sh && ./compile.sh i rodmappen af projektet) og starter den hjemmebyggede server, åbner http://127.0.0.1:18470 i browseren og observerer det, vi forventede:

Fuldt funktionel I/O reaktor på rå C

Benchmarking

Vis specifikationerne for min maskine

$ 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ål præstationen af en enkelttrådet server. Åbn to terminaler: kør ./http_server, i den anden — wrk. Efter et minut vil den anden terminal vise følgende statistik:

$ 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ådet server var i stand til at behandle over 11 millioner anmodninger per minut, der kom fra 100 forbindelser. Et godt resultat, men kan det forbedres?

Multitrådet server

Som nævnt tidligere, kan I/O reaktoren oprettes i separate tråde, hvilket udnytter alle CPU-kerner. Lad os anvende 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 ejer hver tråd sin egen reaktor:

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

Bemærk, at argumentet i funktionen new_server() fremtiden vil komme true. Dette betyder, at vi tildeler serverens socket en mulighed gennem, for at bruge den i et multitrådet miljø. Læs mere her.

Anden omgang

Nu måler vi ydeevnen af den multitrådede 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 behandlede anmodninger per minut er steget med ~3.28 gange! Men vi mangler kun ~to millioner for at nå et rundt tal, lad os prøve at rette det.

Lad os først se på statistikken, der er genereret 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-affinitets brug, kompilering med -march=native, PGO, stigningen i antallet af hits i cachen, stigningen MAX_EVENTS og brugen af EPOLLET gav ikke en væsentlig stigning i ydeevnen. Men hvad sker der, hvis vi øger antallet af samtidige forbindelser?

Statistik ved 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 tråde og 352 forbindelser
  Trådstatistik  Gennemsnit  Stdev  Max  +/ - Stdev
    Latency     2.12ms    3.79ms  68.23ms   87.49%
    Req/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 anmodninger i 1.00m, 5.48GB læst
Anmodninger/s: 665789.26
Overførsel/s:     93.34MB

Det ønskede resultat er opnået, og samtidig en interessant graf, der viser afhængigheden af antallet af behandlede anmodninger pr. minut fra antallet af forbindelser:

Fuldt funktionel I/O reaktor på rå C

Vi ser, at efter et par hundrede forbindelser falder antallet af behandlede anmodninger dramatisk for begge servere (for den multitrådede variant er det mere udtalt). Er dette relateret til implementeringen af TCP/IP-stakken i Linux? Del gerne dine antagelser om denne graf og optimering af de multitrådede og enkelttrådede varianter i kommentarerne.

siger blev bemærket af i kommentarerne, denne ydeevnetest viser ikke opførselen af I/O-reaktoren under reelle belastninger, da serveren næsten altid interagerer med databasen, udskriver logs, bruger kryptografi med TLS osv., hvilket gør belastningen uensartet (dynamisk). Tests sammen med tredjeparts komponenter vil blive udført i artiklen om I/O-reaktoren.

Ulemper ved I/O-reaktoren

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

  • Det er noget mere kompliceret at bruge I/O-reaktoren i et multitrådet miljø, da man skal styre trådene manuelt.
  • Praktik viser, at belastningen i de fleste tilfælde er ujævn, hvilket kan føre til, at én tråd bliver overbelastet, mens en anden er optaget af arbejde.
  • Hvis én hændelsesbehandler blokerer en tråd, vil systemvælgeren også blive blokeret, hvilket kan føre til svære fejl at spore.

Disse problemer løser I/O-proaktor, som ofte har en planlægger, der jævnt fordeler belastningen i trådpuljen, og som også har et mere brugervenligt API. Det vil blive diskuteret senere i min anden artikel.

Konklusion

Hermed er vores rejse fra teori direkte til profilerens udslip afsluttet.

Man skal ikke stoppe her, da der findes mange andre interessante tilgange til at skrive netværkssoftware med forskellige niveauer af brugervenlighed og hastighed. Interessante links, efter min mening, er nævnt nedenfor.

På gensyn!

Interessante projekter

Hvad ellers at læse?

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster