Reactor de E/S C desnudo con todas las funciones

Reactor de E/S C desnudo con todas las funciones

introducción

reactor de entrada/salida (rosca única bucle de eventos) es un patrón para escribir software de alta carga, utilizado en muchas soluciones populares:

En este artículo, veremos los entresijos de un reactor de E/S y cómo funciona, escribiremos una implementación en menos de 200 líneas de código y crearemos un servidor HTTP simple que procese más de 40 millones de solicitudes/min.

prefacio

  • El artículo fue escrito para ayudar a comprender el funcionamiento del reactor de E/S y, por lo tanto, comprender los riesgos al usarlo.
  • Se requieren conocimientos de los conceptos básicos para comprender el artículo. lenguaje c y cierta experiencia en el desarrollo de aplicaciones de red.
  • Todo el código está escrito en lenguaje C estrictamente de acuerdo con (precaución: PDF largo) según estándar C11 para Linux y disponible en GitHub.

¿Por qué lo hace?

Con la creciente popularidad de Internet, los servidores web comenzaron a necesitar manejar una gran cantidad de conexiones simultáneamente y, por lo tanto, se probaron dos enfoques: bloquear E/S en una gran cantidad de subprocesos del sistema operativo y E/S sin bloqueo en combinación con un sistema de notificación de eventos, también llamado "selector de sistema" (encuesta/cola/IOCP/ etc).

El primer enfoque implicó la creación de un nuevo subproceso del sistema operativo para cada conexión entrante. Su desventaja es la escasa escalabilidad: el sistema operativo tendrá que implementar muchos transiciones de contexto и llamadas al sistema. Son operaciones costosas y pueden provocar falta de RAM libre con un número impresionante de conexiones.

La versión modificada destaca número fijo de hilos (grupo de subprocesos), evitando así que el sistema falle, pero al mismo tiempo introduce un nuevo problema: si un grupo de subprocesos está actualmente bloqueado por operaciones de lectura largas, otros sockets que ya pueden recibir datos no podrán hacerlo. entonces.

El segundo enfoque utiliza sistema de notificación de eventos (selector de sistema) proporcionado por el sistema operativo. Este artículo analiza el tipo más común de selector de sistema, basado en alertas (eventos, notificaciones) sobre la preparación para operaciones de E/S, en lugar de en notificaciones sobre su finalización. Un ejemplo simplificado de su uso se puede representar mediante el siguiente diagrama de bloques:

Reactor de E/S C desnudo con todas las funciones

La diferencia entre estos enfoques es la siguiente:

  • Bloqueo de operaciones de E/S suspender flujo de usuarios hasta entonceshasta que el sistema operativo esté correctamente desfragmenta entrante paquetes IP a flujo de bytes (TCP, recibir datos) o no habrá suficiente espacio disponible en los buffers de escritura internos para el envío posterior a través de NIC (enviando datos).
  • Selector de sistema despues de un rato notifica al programa que el sistema operativo ya paquetes IP desfragmentados (TCP, recepción de datos) o suficiente espacio en los buffers de escritura internos ya disponible (enviando datos).

En resumen, reservar un subproceso del sistema operativo para cada E/S es un desperdicio de potencia informática, porque en realidad los subprocesos no realizan un trabajo útil (de aquí proviene el término). "interrupción de software"). El selector de sistema resuelve este problema, permitiendo que el programa de usuario utilice los recursos de la CPU de forma mucho más económica.

Modelo de reactor de E/S

El reactor de E/S actúa como una capa entre el selector del sistema y el código de usuario. El principio de su funcionamiento se describe en el siguiente diagrama de bloques:

Reactor de E/S C desnudo con todas las funciones

  • Permítame recordarle que un evento es una notificación de que un determinado socket puede realizar una operación de E/S sin bloqueo.
  • Un controlador de eventos es una función llamada por el reactor de E/S cuando se recibe un evento, que luego realiza una operación de E/S sin bloqueo.

Es importante tener en cuenta que el reactor de E/S es, por definición, de un solo subproceso, pero no hay nada que impida que el concepto se utilice en un entorno de múltiples subprocesos en una proporción de 1 subproceso: 1 reactor, reciclando así todos los núcleos de la CPU.

implementación

Colocaremos la interfaz pública en un archivo. reactor.h, e implementación - en reactor.c. reactor.h estará compuesto por los siguientes anuncios:

Mostrar declaraciones en 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);

La estructura del reactor de E/S consta de descriptor de archivo selector encuesta и tablas hash GHashTable, que asigna cada socket a CallbackData (estructura de un controlador de eventos y un argumento de usuario para ello).

Mostrar reactor y datos de devolución de llamada

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

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

Tenga en cuenta que hemos habilitado la capacidad de manejar tipo incompleto según el índice. EN reactor.h declaramos la estructura reactor, Y en reactor.c lo definimos, evitando así que el usuario cambie explícitamente sus campos. Este es uno de los patrones. ocultando datos, que encaja sucintamente en la semántica de C.

funciones reactor_register, reactor_deregister и reactor_reregister actualice la lista de sockets de interés y los controladores de eventos correspondientes en el selector del sistema y la tabla hash.

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

Después de que el reactor de E/S haya interceptado el evento con el descriptor fd, llama al controlador de eventos correspondiente, al que pasa fd, máscara de bits eventos generados y un puntero de usuario a void.

Mostrar 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 resumen, la cadena de llamadas a funciones en el código de usuario tomará la siguiente forma:

Reactor de E/S C desnudo con todas las funciones

Servidor de un solo subproceso

Para probar el reactor de E/S bajo carga alta, escribiremos un servidor web HTTP simple que responda a cualquier solicitud con una imagen.

Una referencia rápida al protocolo HTTP.

HTTP - este es el protocolo nivel de aplicación, utilizado principalmente para la interacción servidor-navegador.

HTTP se puede utilizar fácilmente transporte protocolo TCP, enviar y recibir mensajes en un formato especificado especificación.

Formato de solicitud

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

  • CRLF es una secuencia de dos caracteres: r и n, separando la primera línea de la solicitud, encabezados y datos.
  • <КОМАНДА> - uno de CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. El navegador enviará un comando a nuestro servidor. GET, que significa "Envíame el contenido del archivo".
  • <URI> - identificador uniforme de recursos. Por ejemplo, si URI = /index.html, luego el cliente solicita la página principal del sitio.
  • <ВЕРСИЯ HTTP> — versión del protocolo HTTP en el formato HTTP/X.Y. La versión más utilizada hoy en día es HTTP/1.1.
  • <ЗАГОЛОВОК N> es un par clave-valor en el formato <КЛЮЧ>: <ЗНАЧЕНИЕ>, enviado al servidor para su posterior análisis.
  • <ДАННЫЕ> — datos requeridos por el servidor para realizar la operación. A menudo es simple JSON o cualquier otro formato.

Formato de respuesta

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

  • <КОД СТАТУСА> es un número que representa el resultado de la operación. Nuestro servidor siempre devolverá el estado 200 (operación exitosa).
  • <ОПИСАНИЕ СТАТУСА> — representación en cadena del código de estado. Para el código de estado 200 esto es OK.
  • <ЗАГОЛОВОК N> — encabezado del mismo formato que en la solicitud. Devolveremos los títulos Content-Length (tamaño de archivo) y Content-Type: text/html (tipo de datos de retorno).
  • <ДАННЫЕ> — datos solicitados por el usuario. En nuestro caso, este es el camino a la imagen en HTML.

Expediente http_server.c (servidor de un solo subproceso) incluye archivo common.h, que contiene los siguientes prototipos de funciones:

Mostrar prototipos de funciones 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);

También se describe la macro funcional. SAFE_CALL() y la función está definida fail(). La macro compara el valor de la expresión con el error y, si la condición es verdadera, llama a la función. fail():

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

Función fail() imprime los argumentos pasados ​​en la terminal (como printf()) y finaliza el programa con el 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() devuelve el descriptor de archivo del socket "servidor" creado por llamadas al sistema socket(), bind() и listen() y capaz de aceptar conexiones entrantes en modo sin bloqueo.

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

  • Tenga en cuenta que el socket se crea inicialmente en modo sin bloqueo usando la bandera SOCK_NONBLOCKpara que en la función on_accept() (leer más) llamada al sistema accept() no detuvo la ejecución del hilo.
  • si reuse_port es igual a true, entonces esta función configurará el socket con la opción SO_REUSEPORT a través de setsockopt()para utilizar el mismo puerto en un entorno multiproceso (consulte la sección “Servidor multiproceso”).

Controlador de eventos on_accept() llamado después de que el sistema operativo genera un evento EPOLLIN, en este caso significa que se puede aceptar la nueva conexión. on_accept() acepta una nueva conexión, la cambia al modo sin bloqueo y se registra con un controlador de eventos on_recv() en un reactor de E/S.

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

Controlador de eventos on_recv() llamado después de que el sistema operativo genera un evento EPOLLIN, en este caso significa que la conexión registrada on_accept(), listo para recibir datos.

on_recv() lee datos de la conexión hasta que la solicitud HTTP se recibe por completo, luego registra un controlador on_send() para enviar una respuesta HTTP. Si el cliente interrumpe la conexión, el socket se cancela y se cierra 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);
    }
}

Controlador de eventos on_send() llamado después de que el sistema operativo genera un evento EPOLLOUT, lo que significa que la conexión registrada on_recv(), listo para enviar datos. Esta función envía una respuesta HTTP que contiene HTML con una imagen al cliente y luego cambia el controlador de eventos nuevamente a on_recv().

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

Y finalmente, en el archivo. http_server.c, en función main() Creamos un reactor de E/S usando reactor_new(), cree un socket de servidor y regístrelo, inicie el reactor usando reactor_run() durante exactamente un minuto, y luego liberamos recursos y salimos del 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);
}

Comprobemos que todo funciona como se esperaba. Compilando (chmod a+x compile.sh && ./compile.sh en la raíz del proyecto) e inicie el servidor autoescrito, abra http://127.0.0.1:18470 en el navegador y ver lo que esperábamos:

Reactor de E/S C desnudo con todas las funciones

Medición del desempeño

Mostrar las especificaciones de mi auto

$ 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 el rendimiento de un servidor de un solo subproceso. Abramos dos terminales: en una ejecutaremos ./http_server, de una manera diferente - trabajar. Después de un minuto, se mostrarán las siguientes estadísticas en la segunda 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

Nuestro servidor de subproceso único pudo procesar más de 11 millones de solicitudes por minuto provenientes de 100 conexiones. No es un mal resultado, pero ¿se puede mejorar?

Servidor multiproceso

Como se mencionó anteriormente, el reactor de E/S se puede crear en subprocesos separados, utilizando así todos los núcleos de la CPU. Pongamos este enfoque en práctica:

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

Ahora cada hilo es dueño de lo suyo reactor:

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

Tenga en cuenta que el argumento de la función new_server() defensores true. Esto significa que asignamos la opción al socket del servidor. SO_REUSEPORTpara usarlo en un entorno multiproceso. Puedes leer más detalles aquí.

Segunda carrera

Ahora midamos el rendimiento de un 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

¡La cantidad de solicitudes procesadas en 1 minuto aumentó ~3.28 veces! Pero solo nos faltaban ~XNUMX millones para alcanzar el número redondo, así que intentemos solucionarlo.

Primero veamos las estadísticas generadas. 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 afinidad de CPU, compilación con -march=native, PGO, un aumento en el número de visitas tarde, aumentar MAX_EVENTS y usar EPOLLET no dio un aumento significativo en el rendimiento. Pero ¿qué pasa si aumentas el número de conexiones simultáneas?

Estadísticas para 352 conexiones 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

Se obtuvo el resultado deseado, y con él un interesante gráfico que muestra la dependencia del número de solicitudes procesadas en 1 minuto del número de conexiones:

Reactor de E/S C desnudo con todas las funciones

Vemos que después de un par de cientos de conexiones, la cantidad de solicitudes procesadas para ambos servidores cae drásticamente (en la versión multiproceso esto es más notable). ¿Está esto relacionado con la implementación de la pila TCP/IP de Linux? No dude en escribir sus suposiciones sobre este comportamiento del gráfico y las optimizaciones para opciones de subprocesos múltiples y de un solo subproceso en los comentarios.

cómo señaló En los comentarios, esta prueba de rendimiento no muestra el comportamiento del reactor de E/S bajo cargas reales, porque casi siempre el servidor interactúa con la base de datos, genera registros, utiliza criptografía con TLS etc., como resultado de lo cual la carga se vuelve no uniforme (dinámica). Las pruebas junto con componentes de terceros se realizarán en el artículo sobre el proactor de E/S.

Desventajas del reactor de E/S

Debe comprender que el reactor de E/S no está exento de inconvenientes, a saber:

  • Usar un reactor de E/S en un entorno de subprocesos múltiples es algo más difícil, porque Tendrás que gestionar manualmente los flujos.
  • La práctica muestra que en la mayoría de los casos la carga no es uniforme, lo que puede provocar que un subproceso se registre mientras otro está ocupado con el trabajo.
  • Si un controlador de eventos bloquea un hilo, entonces el selector del sistema también lo bloqueará, lo que puede provocar errores difíciles de encontrar.

Resuelve estos problemas proactor de E/S, que a menudo tiene un programador que distribuye uniformemente la carga a un grupo de subprocesos y también tiene una API más conveniente. Hablaremos de ello más adelante, en mi otro artículo.

Conclusión

Aquí termina nuestro viaje desde la teoría hasta el escape perfilador.

No debería insistir en esto, porque existen muchos otros enfoques igualmente interesantes para escribir software de red con diferentes niveles de conveniencia y velocidad. Interesantes, en mi opinión, se proporcionan enlaces a continuación.

Hasta la próxima!

Proyectos interesantes

¿Qué más debería leer?

Fuente: habr.com

Añadir un comentario