Reactor de E/S bare-C con todas as características

Reactor de E/S bare-C con todas as características

Introdución

Reactor de E/S (unha rosca bucle de eventos) é un patrón para escribir software de alta carga, usado en moitas solucións populares:

Neste artigo, analizaremos os pormenores e os contras dun reactor de E/S e como funciona, escribiremos unha implementación en menos de 200 liñas de código e realizaremos un proceso sinxelo de servidor HTTP con máis de 40 millóns de solicitudes/min.

Prefacio

  • O artigo foi escrito para axudar a comprender o funcionamento do reactor de E/S e, polo tanto, comprender os riscos ao usalo.
  • Requírese coñecementos básicos para comprender o artigo. Linguaxe C e algunha experiencia no desenvolvemento de aplicacións de rede.
  • Todo o código está escrito en linguaxe C estrictamente segundo (Atención: PDF longo) ao estándar C11 para Linux e dispoñible en GitHub.

Por que facelo?

Coa crecente popularidade de Internet, os servidores web comezaron a necesitar manexar un gran número de conexións simultáneamente e, polo tanto, probáronse dous enfoques: bloquear E/S nun gran número de fíos do sistema operativo e E/S sen bloqueo en combinación con un sistema de notificación de eventos, tamén chamado "selector de sistema" (epollo/kqueue/IOCP/etc).

O primeiro enfoque consistiu na creación dun novo fío de sistema operativo para cada conexión entrante. A súa desvantaxe é a escasa escalabilidade: o sistema operativo terá que implementar moitos transicións de contexto и chamadas ao sistema. Son operacións caras e poden levar á falta de memoria RAM libre cun número impresionante de conexións.

Destaca a versión modificada número fixo de fíos (grupo de fíos), evitando así o fallo do sistema, pero ao mesmo tempo introducindo un novo problema: se un grupo de fíos está actualmente bloqueado por operacións de lectura longas, entón outros sockets que xa poden recibir datos non poderán facelo. así.

O segundo enfoque utiliza sistema de notificación de eventos (selector do sistema) proporcionado polo SO. Este artigo analiza o tipo máis común de selector de sistema, baseado en alertas (eventos, notificacións) sobre a preparación para operacións de E/S, en lugar de notificacións sobre a súa finalización. Un exemplo simplificado do seu uso pódese representar no seguinte diagrama de bloques:

Reactor de E/S bare-C con todas as características

A diferenza entre estes enfoques é a seguinte:

  • Bloqueo de operacións de E/S suspender fluxo de usuarios ata queata que o sistema operativo estea correctamente desfragmenta entrante paquetes IP ao fluxo de bytes (TCP, recibindo datos) ou non haberá suficiente espazo dispoñible nos búfers de escritura internos para o envío posterior a través de NIC (enviando datos).
  • Selector do sistema co paso do tempo notifica ao programa que o SO xa paquetes IP desfragmentados (TCP, recepción de datos) ou espazo suficiente nos búferes de escritura internos xa dispoñible (envío de datos).

En resumo, reservar un fío de SO para cada E/S é un desperdicio de potencia informática, porque en realidade, os fíos non están a facer un traballo útil (de aquí vén o termo "interrupción de software"). O selector do sistema resolve este problema, permitindo que o programa de usuario utilice os recursos da CPU dun xeito moito máis económico.

Modelo de reactor de E/S

O reactor de E/S actúa como unha capa entre o selector do sistema e o código de usuario. O principio do seu funcionamento descríbese no seguinte diagrama de bloques:

Reactor de E/S bare-C con todas as características

  • Permíteme recordarche que un evento é unha notificación de que un determinado socket é capaz de realizar unha operación de E/S sen bloqueo.
  • Un controlador de eventos é unha función chamada polo reactor de E/S cando se recibe un evento, que despois realiza unha operación de E/S sen bloqueo.

É importante ter en conta que o reactor de E/S é, por definición, dun só subproceso, pero non hai nada que impida que o concepto se use nun ambiente multiproceso nunha proporción de 1 fío: 1 reactor, polo que se reciclan todos os núcleos da CPU.

Implantación

Colocaremos a interface pública nun ficheiro reactor.h, e implementación - en reactor.c. reactor.h constará dos seguintes anuncios:

Mostrar declaracións no 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);

A estrutura do reactor de E/S consta de descriptor de ficheiro selector epollo и táboas hash GHashTable, que asigna cada socket CallbackData (estrutura dun controlador de eventos e un argumento de usuario para el).

Mostrar Reactor e CallbackData

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

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

Teña en conta que activamos a capacidade de manexar tipo incompleto segundo o índice. EN reactor.h declaramos a estrutura reactor, e dentro reactor.c definímolo, evitando así que o usuario modifique explícitamente os seus campos. Este é un dos patróns ocultar datos, que encaixa de forma sucinta na semántica C.

Funcións reactor_register, reactor_deregister и reactor_reregister actualizar a lista de sockets de interese e os controladores de eventos correspondentes no selector do sistema e na táboa hash.

Mostrar funcións de rexistro

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

Despois de que o reactor de E/S interceptase o evento co descritor fd, chama ao controlador de eventos correspondente, ao que pasa fd, máscara de bits eventos xerados e un punteiro de usuario a void.

Mostra a función 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 resumo, a cadea de chamadas de función no código de usuario terá a seguinte forma:

Reactor de E/S bare-C con todas as características

Servidor de fíos únicos

Para probar o reactor de E/S baixo alta carga, escribiremos un servidor web HTTP sinxelo que responda a calquera solicitude cunha imaxe.

Unha referencia rápida ao protocolo HTTP

HTTP - Este é o protocolo nivel de aplicación, usado principalmente para a interacción servidor-navegador.

HTTP pódese usar facilmente transporte protocolo TCP, enviando e recibindo mensaxes nun formato especificado especificación.

Formato de solicitude

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

  • CRLF é unha secuencia de dous caracteres: r и n, separando a primeira liña da solicitude, as cabeceiras e os datos.
  • <КОМАНДА> - un de CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. O navegador enviará un comando ao noso servidor GET, que significa "Envíame o contido do ficheiro".
  • <URI> - identificador de recurso uniforme. Por exemplo, se URI = /index.html, entón o cliente solicita a páxina principal do sitio.
  • <ВЕРСИЯ HTTP> — versión do protocolo HTTP no formato HTTP/X.Y. A versión máis usada hoxe é HTTP/1.1.
  • <ЗАГОЛОВОК N> é un par clave-valor no formato <КЛЮЧ>: <ЗНАЧЕНИЕ>, enviado ao servidor para unha análise posterior.
  • <ДАННЫЕ> — datos requiridos polo servidor para realizar a operación. Moitas veces é sinxelo JSON ou calquera outro formato.

Formato de resposta

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

  • <КОД СТАТУСА> é un número que representa o resultado da operación. O noso servidor sempre devolverá o estado 200 (operación exitosa).
  • <ОПИСАНИЕ СТАТУСА> — representación en cadea do código de estado. Para o código de estado 200, isto é OK.
  • <ЗАГОЛОВОК N> — cabeceira co mesmo formato que na solicitude. Devolveremos os títulos Content-Length (tamaño do ficheiro) e Content-Type: text/html (tipo de datos de retorno).
  • <ДАННЫЕ> - datos solicitados polo usuario. No noso caso, este é o camiño cara á imaxe HTML.

arquivo http_server.c (servidor de fío único) inclúe ficheiro common.h, que contén os seguintes prototipos de funcións:

Mostrar prototipos de funcións en común.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);

Tamén se describe a macro funcional SAFE_CALL() e defínese a función fail(). A macro compara o valor da expresión co erro e, se a condición é certa, chama á función fail():

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

Función fail() imprime os argumentos pasados ​​ao terminal (como printf()) e finaliza o programa co código 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ón new_server() devolve o descritor do ficheiro do socket "servidor" creado polas chamadas do sistema socket(), bind() и listen() e capaz de aceptar conexións entrantes nun modo sen bloqueo.

Mostrar a función 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;
}

  • Teña en conta que o socket créase inicialmente en modo sen bloqueo usando a bandeira SOCK_NONBLOCKpara que na función on_accept() (leer máis) chamada do sistema accept() non detivo a execución do fío.
  • Se reuse_port é igual a true, entón esta función configurará o socket coa opción SO_REUSEPORT a través setsockopt()para usar o mesmo porto nun ambiente multiproceso (consulte a sección "Servidor multi-fíos").

Manexador de eventos on_accept() chamado despois de que o sistema operativo xere un evento EPOLLIN, significando neste caso que se pode aceptar a nova conexión. on_accept() acepta unha nova conexión, cámbiaa ao modo sen bloqueo e rexístrase cun controlador de eventos on_recv() nun reactor de E/S.

Mostrar a función 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);
}

Manexador de eventos on_recv() chamado despois de que o sistema operativo xere un evento EPOLLIN, significando neste caso que a conexión rexistrada on_accept(), listo para recibir datos.

on_recv() le os datos da conexión ata que se recibe completamente a solicitude HTTP e, a continuación, rexistra un controlador on_send() para enviar unha resposta HTTP. Se o cliente rompe a conexión, o socket é cancelado e péchase usando close().

Mostrar función 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);
    }
}

Manexador de eventos on_send() chamado despois de que o sistema operativo xere un evento EPOLLOUT, o que significa que a conexión rexistrada on_recv(), listo para enviar datos. Esta función envía unha resposta HTTP que contén HTML cunha imaxe ao cliente e despois cambia de novo o controlador de eventos on_recv().

Mostrar a función 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);
}

E por último, no arquivo http_server.c, en función main() creamos un reactor de E/S utilizando reactor_new(), crea un socket de servidor e rexístrao, inicia o reactor usando reactor_run() durante exactamente un minuto, e despois liberamos recursos e saímos do programa.

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

Comprobamos que todo funciona como se esperaba. Compilación (chmod a+x compile.sh && ./compile.sh na raíz do proxecto) e inicie o servidor autoescrito, abra http://127.0.0.1:18470 no navegador e mira o que esperabamos:

Reactor de E/S bare-C con todas as características

Medición do rendemento

Mostra as especificacións do meu coche

$ 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

Midamos o rendemento dun servidor de fío único. Abrimos dous terminais: nun executaremos ./http_server, de forma diferente - traballo. Despois dun minuto, no segundo terminal mostraranse as seguintes estatísticas:

$ 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

O noso servidor de fío único puido procesar máis de 11 millóns de solicitudes por minuto procedentes de 100 conexións. Non é mal resultado, pero pódese mellorar?

Servidor multiproceso

Como se mencionou anteriormente, o reactor de E/S pódese crear en fíos separados, utilizando así todos os núcleos da CPU. Poñamos en práctica este enfoque:

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

Agora cada fío posúe o seu reactor:

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

Teña en conta que o argumento da función new_server() defensores true. Isto significa que asignamos a opción ao socket do servidor SO_REUSEPORTpara usalo nun ambiente multiproceso. Podes ler máis detalles aquí.

Segunda tirada

Agora imos medir o rendemento dun servidor multiproceso:

$ 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

O número de solicitudes procesadas en 1 minuto aumentou en ~3.28 veces. Pero só nos quedamos ~XNUMX millóns para alcanzar o número redondo, así que imos tentar solucionalo.

Primeiro vexamos as estatísticas xeradas perfecto:

$ 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

Usando CPU Affinity, compilación con -march=native, PGO, un aumento no número de visitas caché, Aumentar MAX_EVENTS e uso EPOLLET non deu un aumento significativo no rendemento. Pero que pasa se aumentas o número de conexións simultáneas?

Estatísticas de 352 conexións simultáneas:

$ 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

Obtívose o resultado desexado, e con el un gráfico interesante que mostra a dependencia do número de solicitudes procesadas en 1 minuto co número de conexións:

Reactor de E/S bare-C con todas as características

Vemos que despois dun par de centos de conexións, o número de solicitudes procesadas para ambos os servidores cae drasticamente (na versión multifío isto nótase máis). ¿Está relacionado coa implementación da pila TCP/IP de Linux? Non dubides en escribir nos comentarios as túas suposicións sobre este comportamento do gráfico e as optimizacións para opcións de fíos múltiples e de fío único.

Como anotado nos comentarios, esta proba de rendemento non mostra o comportamento do reactor de E/S baixo cargas reais, porque case sempre o servidor interactúa coa base de datos, emite rexistros, usa criptografía con TLS etc., como resultado do cal a carga se fai non uniforme (dinámica). As probas xunto con compoñentes de terceiros realizaranse no artigo sobre o proactor de E/S.

Desvantaxes do reactor de E/S

Debe entender que o reactor de E/S non está exento de inconvenientes, a saber:

  • Usar un reactor de E/S nun ambiente multiproceso é algo máis difícil, porque terás que xestionar manualmente os fluxos.
  • A práctica mostra que na maioría dos casos a carga non é uniforme, o que pode levar a que un fío rexistre mentres outro está ocupado co traballo.
  • Se un controlador de eventos bloquea un fío, entón o propio selector do sistema tamén se bloqueará, o que pode provocar erros difíciles de atopar.

Resolve estes problemas Proactor de E/S, que a miúdo ten un programador que distribúe uniformemente a carga a un conxunto de fíos e tamén ten unha API máis conveniente. Diso falaremos máis adiante, no meu outro artigo.

Conclusión

Aquí é onde a nosa viaxe desde a teoría ata o escape do perfilador chegou ao seu fin.

Non debería deterse nisto, porque hai moitos outros enfoques igualmente interesantes para escribir software de rede con diferentes niveis de comodidade e velocidade. Interesantes, na miña opinión, as ligazóns están a continuación.

Ata a próxima!

Proxectos interesantes

Que máis debo ler?

Fonte: www.habr.com

Engadir un comentario