Reactor d'E/S bare-C amb totes les funcions

Reactor d'E/S bare-C amb totes les funcions

Introducció

Reactor d'E/S (un sol fil bucle d'esdeveniments) és un patró per escriure programari d'alta càrrega, utilitzat en moltes solucions populars:

En aquest article, veurem els detalls d'un reactor d'E/S i com funciona, escriurem una implementació en menys de 200 línies de codi i realitzarem un procés senzill de servidor HTTP de més de 40 milions de sol·licituds/min.

Prefaci

  • L'article va ser escrit per ajudar a entendre el funcionament del reactor d'E/S i, per tant, entendre els riscos en utilitzar-lo.
  • Es requereix coneixements bàsics per entendre l'article. llenguatge C i una mica d'experiència en el desenvolupament d'aplicacions de xarxa.
  • Tot el codi està escrit en llenguatge C estrictament segons (precaució: PDF llarg) a l'estàndard C11 per a Linux i disponible a GitHub.

Per què fer-ho?

Amb la creixent popularitat d'Internet, els servidors web van començar a necessitar gestionar un gran nombre de connexions simultàniament i, per tant, es van provar dos enfocaments: bloquejar E/S en un gran nombre de fils del sistema operatiu i E/S no bloquejant en combinació amb un sistema de notificació d'esdeveniments, també anomenat "selector del sistema" (epoll/kqueue/IOCP/etc).

El primer enfocament consistia a crear un nou fil del sistema operatiu per a cada connexió entrant. El seu inconvenient és la poca escalabilitat: el sistema operatiu n'haurà d'implementar molts transicions de context и trucades al sistema. Són operacions cares i poden provocar una manca de memòria RAM lliure amb un nombre impressionant de connexions.

La versió modificada destaca nombre fix de fils (agrupació de fils), evitant així que el sistema avorti l'execució, però al mateix temps introdueix un nou problema: si actualment un grup de fils està bloquejat per operacions de lectura llarga, llavors altres sòcols que ja poden rebre dades no podran Fes-ho.

El segon enfocament utilitza sistema de notificació d'esdeveniments (selector del sistema) proporcionat pel sistema operatiu. Aquest article tracta el tipus més comú de selector del sistema, basat en alertes (esdeveniments, notificacions) sobre la preparació per a operacions d'E/S, en lloc de notificacions sobre la seva finalització. Un exemple simplificat del seu ús es pot representar amb el següent diagrama de blocs:

Reactor d'E/S bare-C amb totes les funcions

La diferència entre aquests enfocaments és la següent:

  • Bloqueig d'operacions d'E/S suspendre flux d'usuari fins quefins que el sistema operatiu estigui correctament desfragmenta entrant paquets IP al flux de bytes (TCP, rebent dades) o no hi haurà prou espai disponible als buffers d'escriptura interns per a l'enviament posterior mitjançant NIC (enviament de dades).
  • Selector del sistema al llarg del temps notifica al programa que el sistema operatiu ja paquets IP desfragmentats (TCP, recepció de dades) o espai suficient als buffers d'escriptura interns ja disponible (enviament de dades).

En resum, reservar un fil del sistema operatiu per a cada E/S és un malbaratament de potència de càlcul, perquè en realitat, els fils no fan feina útil (d'aquí el terme "interrupció de programari"). El selector del sistema resol aquest problema, permetent al programa d'usuari utilitzar els recursos de la CPU de manera molt més econòmica.

Model de reactor d'E/S

El reactor d'E/S actua com una capa entre el selector del sistema i el codi d'usuari. El principi del seu funcionament es descriu al següent diagrama de blocs:

Reactor d'E/S bare-C amb totes les funcions

  • Permeteu-me que us recordi que un esdeveniment és una notificació que un determinat sòcol és capaç de realitzar una operació d'E/S sense bloqueig.
  • Un controlador d'esdeveniments és una funció cridada pel reactor d'E/S quan es rep un esdeveniment, que després realitza una operació d'E/S sense bloqueig.

És important tenir en compte que el reactor d'E/S és, per definició, d'un sol fil, però no hi ha res que impedeixi que el concepte s'utilitzi en un entorn de múltiples fils amb una proporció d'1 fil: 1 reactor, reciclant així tots els nuclis de la CPU.

Implementació

Col·locarem la interfície pública en un fitxer reactor.h, i implementació - en reactor.c. reactor.h constarà dels següents anuncis:

Mostra declaracions al 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);

L'estructura del reactor d'E/S consta de descriptor de fitxer selector epoll и taules hash GHashTable, que assigna cada sòcol a CallbackData (estructura d'un controlador d'esdeveniments i un argument d'usuari per a aquest).

Mostra Reactor i CallbackData

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

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

Tingueu en compte que hem habilitat la capacitat de gestionar tipus incomplet segons l'índex. EN reactor.h declarem l'estructura reactori en reactor.c el definim, evitant així que l'usuari canviï explícitament els seus camps. Aquest és un dels patrons ocultar dades, que encaixa succintament en la semàntica C.

Funcions reactor_register, reactor_deregister и reactor_reregister actualitzeu la llista de sockets d'interès i els controladors d'esdeveniments corresponents al selector del sistema i a la taula hash.

Mostra les funcions de registre

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

Després que el reactor d'E/S hagi interceptat l'esdeveniment amb el descriptor fd, crida al controlador d'esdeveniments corresponent, al qual passa fd, màscara de bits esdeveniments generats i un punter d'usuari a void.

Mostra la funció 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;
}

En resum, la cadena de trucades de funcions al codi d'usuari tindrà la forma següent:

Reactor d'E/S bare-C amb totes les funcions

Servidor d'un sol fil

Per tal de provar el reactor d'E/S amb una càrrega elevada, escriurem un servidor web HTTP senzill que respongui a qualsevol sol·licitud amb una imatge.

Una referència ràpida al protocol HTTP

HTTP - Aquest és el protocol nivell d'aplicació, utilitzat principalment per a la interacció servidor-navegador.

HTTP es pot utilitzar fàcilment transport protocol TCP, enviant i rebent missatges en un format especificat especificació.

Format de sol·licitud

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

  • CRLF és una seqüència de dos caràcters: r и n, separant la primera línia de la sol·licitud, les capçaleres i les dades.
  • <КОМАНДА> - una de CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. El navegador enviarà una ordre al nostre servidor GET, que significa "Envia'm el contingut del fitxer".
  • <URI> - identificador de recurs uniforme. Per exemple, si URI = /index.html, aleshores el client sol·licita la pàgina principal del lloc.
  • <ВЕРСИЯ HTTP> — versió del protocol HTTP en el format HTTP/X.Y. La versió més utilitzada avui és HTTP/1.1.
  • <ЗАГОЛОВОК N> és una parella clau-valor en el format <КЛЮЧ>: <ЗНАЧЕНИЕ>, enviat al servidor per a una anàlisi posterior.
  • <ДАННЫЕ> — dades requerides pel servidor per realitzar l'operació. Sovint és senzill JSON o qualsevol altre format.

Format de resposta

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

  • <КОД СТАТУСА> és un nombre que representa el resultat de l'operació. El nostre servidor sempre retornarà l'estat 200 (operació correcta).
  • <ОПИСАНИЕ СТАТУСА> — representació en cadena del codi d'estat. Per al codi d'estat 200, això és OK.
  • <ЗАГОЛОВОК N> — capçalera del mateix format que a la sol·licitud. Tornarem els títols Content-Length (mida del fitxer) i Content-Type: text/html (tipus de dades de retorn).
  • <ДАННЫЕ> — dades sol·licitades per l'usuari. En el nostre cas, aquest és el camí cap a la imatge HTML.

expedient http_server.c (servidor d'un sol fil) inclou fitxer common.h, que conté els prototips de funcions següents:

Mostra prototips de funcions en comú.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);

També es descriu la macro funcional SAFE_CALL() i es defineix la funció fail(). La macro compara el valor de l'expressió amb l'error i, si la condició és certa, crida a la funció fail():

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

Funció fail() imprimeix els arguments passats al terminal (com ara printf()) i finalitza el programa amb el codi 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);
}

Funció new_server() retorna el descriptor de fitxer del sòcol "servidor" creat per les trucades del sistema socket(), bind() и listen() i capaç d'acceptar connexions entrants en un mode sense bloqueig.

Mostra la funció 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;
}

  • Tingueu en compte que el sòcol es crea inicialment en mode sense bloqueig mitjançant la bandera SOCK_NONBLOCKde manera que en la funció on_accept() (llegir més) trucada al sistema accept() no va aturar l'execució del fil.
  • Si reuse_port és igual a true, llavors aquesta funció configurarà el sòcol amb l'opció SO_REUSEPORT a través setsockopt()per utilitzar el mateix port en un entorn multifil (vegeu la secció "Servidor multifil").

Gestor d'esdeveniments on_accept() cridat després que el sistema operatiu generi un esdeveniment EPOLLIN, en aquest cas significa que es pot acceptar la nova connexió. on_accept() accepta una connexió nova, la canvia al mode sense bloqueig i es registra amb un gestor d'esdeveniments on_recv() en un reactor d'E/S.

Mostra la funció 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);
}

Gestor d'esdeveniments on_recv() cridat després que el sistema operatiu generi un esdeveniment EPOLLIN, en aquest cas significa que la connexió registrada on_accept(), llest per rebre dades.

on_recv() llegeix dades de la connexió fins que la sol·licitud HTTP es rep completament i després registra un controlador on_send() per enviar una resposta HTTP. Si el client trenca la connexió, el sòcol es dona de baixa i es tanca utilitzant close().

Mostra la funció 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);
    }
}

Gestor d'esdeveniments on_send() cridat després que el sistema operatiu generi un esdeveniment EPOLLOUT, el que significa que la connexió registrada on_recv(), llest per enviar dades. Aquesta funció envia una resposta HTTP que conté HTML amb una imatge al client i després torna a canviar el controlador d'esdeveniments on_recv().

Mostra la funció 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);
}

I finalment, a l'arxiu http_server.c, en funció main() creem un reactor d'E/S utilitzant reactor_new(), creeu un sòcol de servidor i registreu-lo, engegueu el reactor fent servir reactor_run() durant exactament un minut, i després alliberem recursos i sortim del programa.

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

Comprovem que tot funciona com s'esperava. Compilant (chmod a+x compile.sh && ./compile.sh a l'arrel del projecte) i inicieu el servidor escrit per si mateix, obre http://127.0.0.1:18470 al navegador i mireu què esperàvem:

Reactor d'E/S bare-C amb totes les funcions

Mesurament del rendiment

Mostra les especificacions del meu cotxe

$ 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

Mesurem el rendiment d'un servidor d'un sol fil. Obrim dos terminals: en un anirem executant ./http_server, d'una manera diferent - treball. Al cap d'un minut, es mostraran les estadístiques següents al segon 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

El nostre servidor d'un sol fil va poder processar més d'11 milions de sol·licituds per minut procedents de 100 connexions. No és un mal resultat, però es pot millorar?

Servidor multifils

Com s'ha esmentat anteriorment, el reactor d'E/S es pot crear en fils separats, utilitzant així tots els nuclis de la CPU. Posem en pràctica aquest enfocament:

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

Ara cada fil posseeix la seva reactor:

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

Tingueu en compte que l'argument de la funció new_server() favors true. Això vol dir que assignem l'opció al sòcol del servidor SO_REUSEPORTper utilitzar-lo en un entorn multifils. Podeu llegir més detalls aquí.

Segona tirada

Ara mesurem el rendiment d'un servidor multiprocés:

$ 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

El nombre de sol·licituds processades en 1 minut ha augmentat ~3.28 vegades! Però només ens quedaven uns XNUMX milions per arribar al número rodó, així que intentem arreglar-ho.

Primer mirem les estadístiques generades perfecte:

$ 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

Ús de CPU Affinity, recopilació amb -march=native, PGO, un augment del nombre de visites memòria cau, augmentar MAX_EVENTS i ús EPOLLET no ha donat un augment significatiu del rendiment. Però què passa si augmenteu el nombre de connexions simultànies?

Estadístiques de 352 connexions simultànies:

$ 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

Es va obtenir el resultat desitjat, i amb ell un interessant gràfic que mostra la dependència del nombre de peticions processades en 1 minut del nombre de connexions:

Reactor d'E/S bare-C amb totes les funcions

Veiem que després d'un parell de centenars de connexions, el nombre de sol·licituds processades per als dos servidors disminueix bruscament (a la versió multifils això es nota més). Està relacionat amb la implementació de la pila TCP/IP de Linux? No dubteu a escriure els vostres supòsits sobre aquest comportament del gràfic i les optimitzacions per a opcions de fils múltiples i d'un sol fil als comentaris.

Com assenyalat als comentaris, aquesta prova de rendiment no mostra el comportament del reactor d'E/S sota càrregues reals, perquè gairebé sempre el servidor interactua amb la base de dades, genera registres, utilitza criptografia amb TLS etc., com a conseqüència de la qual cosa la càrrega esdevé no uniforme (dinàmica). Les proves juntament amb components de tercers es realitzaran a l'article sobre el proactor d'E/S.

Inconvenients del reactor d'E/S

Heu d'entendre que el reactor d'E/S no té els seus inconvenients, a saber:

  • L'ús d'un reactor d'E/S en un entorn multifil és una mica més difícil, perquè hauràs de gestionar manualment els fluxos.
  • La pràctica demostra que, en la majoria dels casos, la càrrega no és uniforme, cosa que pot provocar que un fil registri mentre un altre estigui ocupat amb la feina.
  • Si un gestor d'esdeveniments bloqueja un fil, el selector del sistema també es bloquejarà, cosa que pot provocar errors difícils de trobar.

Soluciona aquests problemes Proactor d'E/S, que sovint té un programador que distribueix uniformement la càrrega a un conjunt de fils i també té una API més convenient. En parlarem més endavant, en el meu altre article.

Conclusió

Aquí és on s'ha acabat el nostre viatge des de la teoria fins a l'escapament del perfilador.

No us hauríeu de detenir en això, perquè hi ha molts altres enfocaments igualment interessants per escriure programari de xarxa amb diferents nivells de comoditat i velocitat. Interessant, al meu entendre, es donen enllaços a continuació.

Us tornem a veure

Projectes interessants

Què més he de llegir?

Font: www.habr.com

Afegeix comentari