Fullfunksjons bare-C I/O-reaktor

Fullfunksjons bare-C I/O-reaktor

Innledning

I/O-reaktor (engjenget hendelsessløyfe) er et mønster for å skrive programvare med høy belastning, brukt i mange populære løsninger:

I denne artikkelen skal vi se på inn- og utsiden av en I/O-reaktor og hvordan den fungerer, skrive en implementering på mindre enn 200 linjer med kode, og lage en enkel HTTP-serverprosess over 40 millioner forespørsler/min.

Forord

  • Artikkelen ble skrevet for å hjelpe til med å forstå funksjonen til I/O-reaktoren, og derfor forstå risikoen ved bruk.
  • Kunnskap om det grunnleggende kreves for å forstå artikkelen. C språk og litt erfaring med utvikling av nettverksapplikasjoner.
  • All kode er skrevet på C-språk strengt i henhold til (forsiktig: lang PDF) til C11 standard for Linux og tilgjengelig på GitHub.

Hvorfor gjøre det?

Med Internetts økende popularitet begynte webservere å måtte håndtere et stort antall tilkoblinger samtidig, og derfor ble to tilnærminger prøvd: blokkering av I/O på et stort antall OS-tråder og ikke-blokkerende I/O i kombinasjon med et hendelsesvarslingssystem, også kalt "systemvelger" (epoll/kqueue/IOCP/etc).

Den første tilnærmingen innebar å opprette en ny OS-tråd for hver innkommende tilkobling. Ulempen er dårlig skalerbarhet: operativsystemet må implementere mange kontekstoverganger и systemanrop. De er dyre operasjoner og kan føre til mangel på ledig RAM med et imponerende antall tilkoblinger.

Den modifiserte versjonen fremhever fast antall tråder (trådpool), og forhindrer dermed systemet i å krasje, men introduserer samtidig et nytt problem: hvis en trådpool for øyeblikket er blokkert av lange leseoperasjoner, vil ikke andre sockets som allerede er i stand til å motta data kunne gjøre det så.

Den andre tilnærmingen bruker hendelsesvarslingssystem (systemvelger) levert av operativsystemet. Denne artikkelen diskuterer den vanligste typen systemvelger, basert på varsler (hendelser, varsler) om beredskap for I/O-operasjoner, snarere enn på varsler om fullføringen. Et forenklet eksempel på bruken kan representeres av følgende blokkdiagram:

Fullfunksjons bare-C I/O-reaktor

Forskjellen mellom disse tilnærmingene er som følger:

  • Blokkering av I/O-operasjoner utsette brukerflyt førtil OS er riktig defragmenterer innkommende IP-pakker å byte strøm (TCP, mottar data) eller det vil ikke være nok plass tilgjengelig i de interne skrivebufferne for påfølgende sending via NIC (sende data).
  • Systemvelger over tid varsler programmet om at operativsystemet allerede defragmenterte IP-pakker (TCP, datamottak) eller nok plass i interne skrivebuffere allerede tilgjengelig (sende data).

For å oppsummere, å reservere en OS-tråd for hver I/O er sløsing med datakraft, fordi trådene i virkeligheten ikke gjør nyttig arbeid (det er her begrepet kommer fra "programvareavbrudd"). Systemvelgeren løser dette problemet, slik at brukerprogrammet kan bruke CPU-ressurser mye mer økonomisk.

I/O-reaktormodell

I/O-reaktoren fungerer som et lag mellom systemvelgeren og brukerkoden. Prinsippet for driften er beskrevet av følgende blokkdiagram:

Fullfunksjons bare-C I/O-reaktor

  • La meg minne deg på at en hendelse er et varsel om at en bestemt socket er i stand til å utføre en ikke-blokkerende I/O-operasjon.
  • En hendelsesbehandler er en funksjon som kalles av I/O-reaktoren når en hendelse mottas, som deretter utfører en ikke-blokkerende I/O-operasjon.

Det er viktig å merke seg at I/O-reaktoren per definisjon er entrådet, men det er ingenting i veien for at konseptet kan brukes i et flertrådsmiljø i forholdet 1 tråd: 1 reaktor, og resirkulerer dermed alle CPU-kjerner.

implementering

Vi vil plassere det offentlige grensesnittet i en fil reactor.h, og implementering - i reactor.c. reactor.h vil bestå av følgende kunngjøringer:

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 av filbeskrivelse velger epoll и hasjtabeller GHashTable, som tilordner hver socket til CallbackData (struktur av en hendelsesbehandler og et brukerargument for det).

Vis reaktor- og tilbakeringingsdata

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

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

Vær oppmerksom på at vi har aktivert muligheten til å håndtere ufullstendig type ifølge indeksen. I reactor.h vi erklærer strukturen reactorog i reactor.c vi definerer det, og forhindrer dermed brukeren i å eksplisitt endre feltene. Dette er et av mønstrene skjule data, som kort og godt passer inn i C-semantikk.

funksjoner reactor_register, reactor_deregister и reactor_reregister oppdater listen over sockets av interesse og tilsvarende hendelsesbehandlere i systemvelgeren og hashtabellen.

Vis registreringsfunksjoner

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

Etter at I/O-reaktoren har fanget opp hendelsen med beskrivelsen fd, kaller den den tilsvarende hendelsesbehandleren, som den går videre til fd, bit maske genererte hendelser og en brukerpeker til void.

Vis funksjonen 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) {
        // Ошибка
        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 å oppsummere vil kjeden av funksjonsanrop i brukerkode ha følgende form:

Fullfunksjons bare-C I/O-reaktor

Enkeltråds server

For å teste I/O-reaktoren under høy belastning, vil vi skrive en enkel HTTP-webserver som svarer på enhver forespørsel med et bilde.

En rask referanse til HTTP-protokollen

HTTP - Dette er protokollen applikasjonsnivå, primært brukt for server-nettleserinteraksjon.

HTTP kan enkelt brukes over transportere protokoll TCP, sende og motta meldinger i et spesifisert format spesifikasjon.

Forespørselsformat

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

  • CRLF er en sekvens av to tegn: r и n, som skiller den første linjen i forespørselen, overskrifter og data.
  • <КОМАНДА> - en av CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Nettleseren sender en kommando til serveren vår GET, som betyr "Send meg innholdet i filen."
  • <URI> - enhetlig ressursidentifikator. For eksempel hvis URI = /index.html, så ber klienten om hovedsiden til nettstedet.
  • <ВЕРСИЯ HTTP> — versjon av HTTP-protokollen i formatet HTTP/X.Y. Den mest brukte versjonen i dag er HTTP/1.1.
  • <ЗАГОЛОВОК N> er et nøkkelverdi-par i formatet <КЛЮЧ>: <ЗНАЧЕНИЕ>, sendt til serveren for videre analyse.
  • <ДАННЫЕ> — data som kreves av serveren for å utføre operasjonen. Ofte er det enkelt JSON eller et annet format.

Svarformat

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

  • <КОД СТАТУСА> er et tall som representerer resultatet av operasjonen. Serveren vår vil alltid returnere status 200 (vellykket operasjon).
  • <ОПИСАНИЕ СТАТУСА> — strengrepresentasjon av statuskoden. For statuskode 200 er dette OK.
  • <ЗАГОЛОВОК N> — overskrift i samme format som i forespørselen. Vi vil returnere titlene Content-Length (filstørrelse) og Content-Type: text/html (returdatatype).
  • <ДАННЫЕ> — data etterspurt av brukeren. I vårt tilfelle er dette veien til bildet inn HTML.

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

Vis funksjonsprototyper i fellesskap.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 funksjonelle makroen er også beskrevet SAFE_CALL() og funksjonen er definert fail(). Makroen sammenligner verdien av uttrykket med feilen, og kaller funksjonen hvis betingelsen er sann fail():

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

Funksjon fail() skriver ut de beståtte argumentene til terminalen (som printf()) og avslutter 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);
}

Funksjon new_server() returnerer filbeskrivelsen til "server"-socket opprettet av systemanrop socket(), bind() и listen() og i stand til å akseptere innkommende tilkoblinger i en ikke-blokkerende modus.

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

  • Merk at kontakten i utgangspunktet opprettes i ikke-blokkerende modus ved å bruke flagget SOCK_NONBLOCKslik at i funksjonen on_accept() (les mer) systemanrop accept() stoppet ikke trådkjøringen.
  • Hvis reuse_port er lik true, så vil denne funksjonen konfigurere kontakten med alternativet SO_REUSEPORT gjennom setsockopt()for å bruke den samme porten i et flertrådsmiljø (se avsnittet "Multi-tråds server").

Hendelsesbehandler on_accept() kalles etter at OS genererer en hendelse EPOLLIN, i dette tilfellet betyr at den nye forbindelsen kan aksepteres. on_accept() godtar en ny tilkobling, bytter den til ikke-blokkerende modus og registrerer seg hos en hendelsesbehandler on_recv() i en I/O-reaktor.

Vis on_accept() funksjonen

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

Hendelsesbehandler on_recv() kalles etter at OS genererer en hendelse EPOLLIN, i dette tilfellet betyr at forbindelsen registrert on_accept(), klar til å motta data.

on_recv() leser data fra tilkoblingen til HTTP-forespørselen er fullstendig mottatt, deretter registrerer den en behandler on_send() for å sende et HTTP-svar. Hvis klienten bryter forbindelsen, avregistreres stikkontakten og lukkes vha close().

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

Hendelsesbehandler on_send() kalles etter at OS genererer en hendelse EPOLLOUT, som betyr at forbindelsen er registrert on_recv(), klar til å sende data. Denne funksjonen sender et HTTP-svar som inneholder HTML med et bilde til klienten og endrer deretter hendelsesbehandleren tilbake til on_recv().

Vis on_send() funksjon

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 slutt, i filen http_server.c, i funksjon main() vi lager en I/O-reaktor ved hjelp av reactor_new(), lag en serversocket og registrer den, start reaktoren med reactor_run() i nøyaktig ett minutt, og så slipper vi ressurser og avslutter 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);
}

La oss sjekke at alt fungerer som forventet. Kompilere (chmod a+x compile.sh && ./compile.sh i prosjektroten) og start den selvskrevne serveren, åpne http://127.0.0.1:18470 i nettleseren og se hva vi forventet:

Fullfunksjons bare-C I/O-reaktor

Prestasjonsmåling

Vis bilens spesifikasjoner

$ 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

La oss måle ytelsen til en enkelt-tråds server. La oss åpne to terminaler: i den ene kjører vi ./http_server, i en annen - wrk. Etter et minutt vil følgende statistikk vises i den andre 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 entrådede server var i stand til å behandle over 11 millioner forespørsler per minutt fra 100 tilkoblinger. Ikke et dårlig resultat, men kan det forbedres?

Multithreaded server

Som nevnt ovenfor kan I/O-reaktoren lages i separate tråder, og dermed utnytte alle CPU-kjerner. La oss sette denne tilnærmingen ut i livet:

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

Nå hver tråd eier sin egen reaktor:

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

Vær oppmerksom på at funksjonsargumentet new_server() talsmenn true. Dette betyr at vi tilordner alternativet til serverkontakten SO_REUSEPORTå bruke den i et flertrådsmiljø. Du kan lese flere detaljer her.

Andre kjøring

La oss nå måle ytelsen til en flertrådsserver:

$ 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 forespørsler behandlet på 1 minutt økte med ~3.28 ganger! Men vi manglet bare ~XNUMX millioner av det runde tallet, så la oss prøve å fikse det.

La oss først se på statistikken som genereres 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

Bruker CPU Affinity, sammenstilling med -march=native, PGO, en økning i antall treff cache, øke MAX_EVENTS og bruk EPOLLET ga ikke nevneverdig økning i ytelsen. Men hva skjer hvis du øker antall samtidige forbindelser?

Statistikk for 352 samtidige tilkoblinger:

$ 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 resultatet ble oppnådd, og med det en interessant graf som viser avhengigheten av antall behandlede forespørsler på 1 minutt på antall tilkoblinger:

Fullfunksjons bare-C I/O-reaktor

Vi ser at etter et par hundre tilkoblinger synker antallet behandlede forespørsler for begge serverne kraftig (i flertrådsversjonen er dette mer merkbart). Er dette relatert til Linux TCP/IP-stackimplementeringen? Skriv gjerne dine antakelser om denne oppførselen til grafen og optimaliseringer for flertrådede og enkeltrådede alternativer i kommentarfeltet.

Som bemerket i kommentarfeltet viser ikke denne ytelsestesten oppførselen til I/O-reaktoren under reelle belastninger, fordi serveren nesten alltid samhandler med databasen, sender ut logger, bruker kryptografi med TLS etc., som et resultat av at belastningen blir ujevn (dynamisk). Tester sammen med tredjepartskomponenter vil bli utført i artikkelen om I/O-proaktoren.

Ulemper med I/O-reaktor

Du må forstå at I/O-reaktoren ikke er uten ulemper, nemlig:

  • Å bruke en I/O-reaktor i et flertrådsmiljø er noe vanskeligere, fordi du må administrere flytene manuelt.
  • Praksis viser at i de fleste tilfeller er belastningen ujevn, noe som kan føre til at en tråd logger mens en annen er opptatt med arbeid.
  • Hvis en hendelsesbehandler blokkerer en tråd, vil selve systemvelgeren også blokkere, noe som kan føre til vanskelige feil.

Løser disse problemene I/O proaktor, som ofte har en planlegger som jevnt fordeler belastningen til en pool av tråder, og har også en mer praktisk API. Vi vil snakke om det senere, i min andre artikkel.

Konklusjon

Det er her reisen vår fra teori rett inn i profileringseksosen har kommet til en slutt.

Du bør ikke dvele ved dette, fordi det er mange andre like interessante tilnærminger til å skrive nettverksprogramvare med forskjellige nivåer av bekvemmelighet og hastighet. Interessante, etter min mening, er lenker gitt nedenfor.

Til neste gang!

Interessante prosjekter

Hva annet bør jeg lese?

Kilde: www.habr.com

Legg til en kommentar