Reator de E/S bare-C completo

Reator de E/S bare-C completo

Introdução

Reator de E/S (rosca única ciclo de eventos) é um padrão para escrever software de alta carga, usado em muitas soluções populares:

Neste artigo, veremos os meandros de um reator de E/S e como ele funciona, escreveremos uma implementação em menos de 200 linhas de código e faremos um servidor HTTP simples processar mais de 40 milhões de solicitações/min.

Prefácio

  • O artigo foi escrito para ajudar a entender o funcionamento do reator de E/S e, portanto, entender os riscos ao utilizá-lo.
  • É necessário conhecimento do básico para entender o artigo. Linguagem C e alguma experiência em desenvolvimento de aplicativos de rede.
  • Todo o código é escrito em linguagem C estritamente de acordo com (cuidado: PDF longo) para o padrão C11 para Linux e disponível em GitHub.

Por que isso?

Com a crescente popularidade da Internet, os servidores web começaram a precisar lidar com um grande número de conexões simultaneamente e, portanto, duas abordagens foram tentadas: bloqueio de E/S em um grande número de threads do sistema operacional e E/S sem bloqueio em combinação com um sistema de notificação de eventos, também chamado de “seletor de sistema” (epoll/kqueue/IOCP/etc).

A primeira abordagem envolveu a criação de um novo thread de sistema operacional para cada conexão de entrada. Sua desvantagem é a baixa escalabilidade: o sistema operacional terá que implementar muitos transições de contexto и chamadas do sistema. São operações caras e podem levar à falta de RAM livre com um número impressionante de conexões.

A versão modificada destaca número fixo de threads (pool de threads), evitando assim que o sistema aborte a execução, mas ao mesmo tempo introduzindo um novo problema: se um pool de threads estiver atualmente bloqueado por operações de leitura longas, então outros soquetes que já são capazes de receber dados não serão capazes de faça isso.

A segunda abordagem usa sistema de notificação de eventos (seletor de sistema) fornecido pelo sistema operacional. Este artigo discute o tipo mais comum de seletor de sistema, baseado em alertas (eventos, notificações) sobre prontidão para operações de E/S, em vez de notificações sobre sua conclusão. Um exemplo simplificado de sua utilização pode ser representado pelo seguinte diagrama de blocos:

Reator de E/S bare-C completo

A diferença entre essas abordagens é a seguinte:

  • Bloqueando operações de E/S suspender fluxo do usuário até entãoaté que o sistema operacional esteja corretamente desfragmenta entrada Pacotes IP para fluxo de bytes (TCP, recebendo dados) ou não haverá espaço suficiente disponível nos buffers de gravação internos para posterior envio via NIC (enviando dados).
  • Seletor de sistema hora extra notifica o programa de que o sistema operacional pacotes IP desfragmentados (TCP, recepção de dados) ou espaço suficiente em buffers de gravação internos disponível (envio de dados).

Resumindo, reservar um thread do sistema operacional para cada E/S é um desperdício de poder de computação, porque, na realidade, os threads não estão realizando um trabalho útil (daí o termo "interrupção de software"). O seletor de sistema resolve esse problema, permitindo que o programa do usuário utilize os recursos da CPU de maneira muito mais econômica.

Modelo de reator de E/S

O reator de E/S atua como uma camada entre o seletor do sistema e o código do usuário. O princípio de seu funcionamento é descrito pelo seguinte diagrama de blocos:

Reator de E/S bare-C completo

  • Deixe-me lembrá-lo de que um evento é uma notificação de que um determinado soquete é capaz de executar uma operação de E/S sem bloqueio.
  • Um manipulador de eventos é uma função chamada pelo reator de E/S quando um evento é recebido, que então executa uma operação de E/S sem bloqueio.

É importante notar que o reator de E/S é, por definição, single-threaded, mas nada impede que o conceito seja usado em um ambiente multi-threaded na proporção de 1 thread: 1 reator, reciclando assim todos os núcleos da CPU.

Implementação

Colocaremos a interface pública em um arquivo reactor.he implementação - em reactor.c. reactor.h consistirá nos seguintes anúncios:

Mostrar declarações em 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 reator de E/S consiste em descritor de arquivo seletor epoll и tabelas hash GHashTable, que mapeia cada soquete para CallbackData (estrutura de um manipulador de eventos e um argumento de usuário para ele).

Mostrar Reactor e CallbackData

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

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

Observe que ativamos a capacidade de lidar com tipo incompleto de acordo com o índice. EM reactor.h declaramos a estrutura reactore em reactor.c nós o definimos, evitando assim que o usuário altere explicitamente seus campos. Este é um dos padrões ocultando dados, que se encaixa sucintamente na semântica C.

funções reactor_register, reactor_deregister и reactor_reregister atualize a lista de soquetes de interesse e manipuladores de eventos correspondentes no seletor do sistema e na tabela hash.

Mostrar funções de registro

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

Após o reator de E/S ter interceptado o evento com o descritor fd, ele chama o manipulador de eventos correspondente, para o qual passa fd, máscara de bits eventos gerados e um ponteiro do usuário para void.

Mostrar função 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;
}

Para resumir, a cadeia de chamadas de função no código do usuário terá a seguinte forma:

Reator de E/S bare-C completo

Servidor de thread único

Para testar o reator de E/S sob alta carga, escreveremos um servidor web HTTP simples que responda a qualquer solicitação com uma imagem.

Uma referência rápida ao protocolo HTTP

HTTP - este é o protocolo nível de aplicação, usado principalmente para interação servidor-navegador.

HTTP pode ser facilmente usado em transporte protocolo TCP, enviando e recebendo mensagens em um formato especificado especificação.

Formato de solicitação

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

  • CRLF é uma sequência de dois caracteres: r и n, separando a primeira linha da solicitação, cabeçalhos e dados.
  • <КОМАНДА> - um de CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. O navegador enviará um comando para o nosso servidor GET, que significa "Envie-me o conteúdo do arquivo".
  • <URI> - Identificador de Recurso Uniforme. Por exemplo, se URI = /index.html, então o cliente solicita a página principal do site.
  • <ВЕРСИЯ HTTP> — versão do protocolo HTTP no formato HTTP/X.Y. A versão mais usada atualmente é HTTP/1.1.
  • <ЗАГОЛОВОК N> é um par de valores-chave no formato <КЛЮЧ>: <ЗНАЧЕНИЕ>, enviado ao servidor para análise posterior.
  • <ДАННЫЕ> — dados exigidos pelo servidor para executar a operação. Muitas vezes é simples JSON ou qualquer outro formato.

Formato de resposta

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

  • <КОД СТАТУСА> é um número que representa o resultado da operação. Nosso servidor sempre retornará o status 200 (operação bem-sucedida).
  • <ОПИСАНИЕ СТАТУСА> — representação em string do código de status. Para o código de status 200, isso é OK.
  • <ЗАГОЛОВОК N> — cabeçalho do mesmo formato da solicitação. Devolveremos os títulos Content-Length (tamanho do arquivo) e Content-Type: text/html (tipo de dados de retorno).
  • <ДАННЫЕ> — dados solicitados pelo usuário. No nosso caso, este é o caminho para a imagem em HTML.

arquivo http_server.c (servidor de thread único) inclui arquivo common.h, que contém os seguintes protótipos de função:

Mostrar protótipos de funções em comum.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);

A macro funcional também é descrita SAFE_CALL() e a função é definida fail(). A macro compara o valor da expressão com o erro e, se a condição for verdadeira, chama a função fail():

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

Função fail() imprime os argumentos passados ​​​​no terminal (como printf()) e finaliza o programa com o 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);
}

Função new_server() retorna o descritor de arquivo do soquete "servidor" criado pelas chamadas do sistema socket(), bind() и listen() e capaz de aceitar conexões de entrada em modo sem bloqueio.

Mostrar função 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;
}

  • Observe que o soquete é inicialmente criado em modo sem bloqueio usando o sinalizador SOCK_NONBLOCKde modo que na função on_accept() (leia mais) chamada de sistema accept() não interrompeu a execução do thread.
  • Se reuse_port é igual a true, então esta função irá configurar o soquete com a opção SO_REUSEPORT através de setsockopt()usar a mesma porta em um ambiente multithread (consulte a seção “Servidor multithread”).

Manipulador de eventos on_accept() chamado depois que o sistema operacional gera um evento EPOLLIN, neste caso significando que a nova conexão pode ser aceita. on_accept() aceita uma nova conexão, alterna para o modo sem bloqueio e registra-se em um manipulador de eventos on_recv() em um reator de E/S.

Mostrar função 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);
}

Manipulador de eventos on_recv() chamado depois que o sistema operacional gera um evento EPOLLIN, neste caso significando que a conexão registrada on_accept(), pronto para receber dados.

on_recv() lê os dados da conexão até que a solicitação HTTP seja completamente recebida e então registra um manipulador on_send() para enviar uma resposta HTTP. Se o cliente interromper a conexão, o soquete será cancelado e fechado usando close().

Mostrar função 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);
    }
}

Manipulador de eventos on_send() chamado depois que o sistema operacional gera um evento EPOLLOUT, o que significa que a conexão registrada on_recv(), pronto para enviar dados. Esta função envia uma resposta HTTP contendo HTML com uma imagem para o cliente e então altera o manipulador de eventos de volta para on_recv().

Mostrar função 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 finalmente, no arquivo http_server.c, em função main() criamos um reator de E/S usando reactor_new(), crie um soquete de servidor e registre-o, inicie o reator usando reactor_run() por exatamente um minuto, e então 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);
}

Vamos verificar se tudo está funcionando conforme o esperado. Compilando (chmod a+x compile.sh && ./compile.sh na raiz do projeto) e inicie o servidor auto-escrito, abra http://127.0.0.1:18470 no navegador e veja o que esperávamos:

Reator de E/S bare-C completo

Medição de desempenho

Mostrar as especificações do meu carro

$ 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

Vamos medir o desempenho de um servidor de thread único. Vamos abrir dois terminais: em um rodaremos ./http_server, em um diferente - trabalhar. Após um minuto, as seguintes estatísticas serão exibidas no segundo 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

Nosso servidor single-threaded foi capaz de processar mais de 11 milhões de solicitações por minuto provenientes de 100 conexões. Não é um resultado ruim, mas pode ser melhorado?

Servidor multithread

Conforme mencionado acima, o reator de E/S pode ser criado em threads separados, utilizando assim todos os núcleos da CPU. Vamos colocar essa abordagem em prática:

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 tópico possui o seu próprio reator:

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

Observe que o argumento da função new_server() defensores true. Isso significa que atribuímos a opção ao soquete do servidor SO_REUSEPORTpara usá-lo em um ambiente multithread. Você pode ler mais detalhes aqui.

Segunda corrida

Agora vamos medir o desempenho de um servidor multithread:

$ 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 solicitações processadas em 1 minuto aumentou cerca de 3.28 vezes! Mas faltavam apenas cerca de XNUMX milhões para o número redondo, então vamos tentar consertar isso.

Primeiro vamos dar uma olhada nas estatísticas geradas 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

Usando afinidade de CPU, compilação com -march=native, PGO, um aumento no número de acessos tarde, aumentar MAX_EVENTS e usar EPOLLET não deu um aumento significativo no desempenho. Mas o que acontece se você aumentar o número de conexões simultâneas?

Estatísticas para 352 conexões 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

O resultado desejado foi obtido, e com ele um gráfico interessante mostrando a dependência do número de solicitações processadas em 1 minuto do número de conexões:

Reator de E/S bare-C completo

Vemos que após algumas centenas de conexões, o número de solicitações processadas para ambos os servidores cai drasticamente (na versão multithread isso é mais perceptível). Isso está relacionado à implementação da pilha TCP/IP do Linux? Sinta-se à vontade para escrever suas suposições sobre esse comportamento do gráfico e otimizações para opções multithread e single-thread nos comentários.

Как anotado nos comentários, este teste de desempenho não mostra o comportamento do reator de E/S sob cargas reais, pois quase sempre o servidor interage com o banco de dados, gera logs, usa criptografia com TLS etc., como resultado a carga se torna não uniforme (dinâmica). Testes em conjunto com componentes de terceiros serão realizados no artigo sobre o proator de I/O.

Desvantagens do reator de E/S

Você precisa entender que o reator de E/S tem suas desvantagens, a saber:

  • Usar um reator de E/S em um ambiente multithread é um pouco mais difícil, porque você terá que gerenciar manualmente os fluxos.
  • A prática mostra que na maioria dos casos a carga não é uniforme, o que pode levar ao log de um thread enquanto outro está ocupado com o trabalho.
  • Se um manipulador de eventos bloquear um thread, o próprio seletor do sistema também bloqueará, o que pode levar a bugs difíceis de encontrar.

Resolve esses problemas Proator de E/S, que geralmente possui um agendador que distribui uniformemente a carga para um pool de threads e também possui uma API mais conveniente. Falaremos sobre isso mais tarde, em meu outro artigo.

Conclusão

É aqui que nossa jornada da teoria direto para o escapamento do perfilador chegou ao fim.

Você não deve insistir nisso, porque existem muitas outras abordagens igualmente interessantes para escrever software de rede com diferentes níveis de conveniência e velocidade. Interessantes, na minha opinião, os links são fornecidos abaixo.

Até nos encontrarmos novamente!

Projetos interessantes

O que mais devo ler?

Fonte: habr.com

Adicionar um comentário