Sebapali se felletseng sa bare-C I/O

Sebapali se felletseng sa bare-C I/O

Selelekela

I/O реактор (ea khoele e le 'ngoe цикл событий) — это паттерн для написания высоконагруженного ПО, используемый во многих популярных решениях:

В данной статье мы рассмотрим подноготную I/O реактора и принцип его работы, напишем реализацию на меньше, чем 200 строк кода и заставим простой HTTP сервер обрабатывать свыше 40 миллионов запросов/мин.

Tlhaloso

  • Статья написана с целью помочь разобраться в функционировании I/O реактора, а значит и осознать риски при его использовании.
  • Для усвоения статьи требуется знание основ C puo и небольшой опыт разработки сетевых приложений.
  • Весь код написан на языке Си строго по (осторожно: длинный PDF) стандарту C11 bakeng sa Linux mme e fumaneha ho GitHub.

Ke hobane'ng ha see se hlokahala?

С ростом популярности Интернета веб-серверам стало нужно обрабатывать большое количество соединений одновременно, в связи с чем было опробовано два подхода: блокирующее I/O на большом числе потоков ОС и неблокирующее I/O в комбинации с системой оповещения о событиях, ещё называемой «системным селектором» (epoll/kqueue/IOCP/etc).

Первый подход подразумевал создание нового потока ОС для каждого входящего соединения. Его недостатком является плохая масштабируемость: операционной системе придётся осуществлять множество переходов контекста и системных вызовов. Ke ts'ebetso e theko e boima 'me e ka lebisa ho haelloe ke RAM ea mahala e nang le palo e tsotehang ea likhokahano.

Модифицированная версия выделяет фиксированное число потоков (thread pool), тем самым не позволяя системе аварийно прекратить исполнение, но вместе с тем привносит новую проблему: если в данный момент времени пул потоков блокируют продолжительные операции чтения, то другие сокеты, которые уже в состоянии принять данные, не смогут этого сделать.

Второй подход использует систему оповещения о событиях (системный селектор), которую предоставляет ОС. В данной статье рассмотрен наиболее часто встречающийся вид системного селектора, основанный на оповещениях (событиях, уведомлениях) о готовности к I/O операциям, нежели на оповещениях об их завершении. Упрощённый пример его использования можно представить следующей блок-схемой:

Sebapali se felletseng sa bare-C I/O

Разница между данными подходами заключается в следующем:

  • Блокирующие I/O операции приостанавливают пользовательский поток ho fihlela, пока ОС должным образом не дефрагментирует поступающие IP пакеты ho phalla (TCP, получение данных) или не освободится достаточно места во внутренних буферах записи для последующей отправки через NIC (ho romela lintlha).
  • Системный селектор ka mor'a nako e itseng уведомляет программу о том, что ОС se дефрагментировала IP пакеты (TCP, получение данных) или достаточно места во внутренних буферах записи se доступно (отправка данных).

Ho e akaretsa, ho boloka khoele ea OS bakeng sa I/O ka 'ngoe ke tšenyo ea matla a komporo, hobane ha e le hantle, likhoele ha li etse mosebetsi oa bohlokoa (ka hona poleloana e reng " "software e sitisa"). Системный селектор решает эту проблему, позволяя пользовательской программе расходовать ресурсы ЦПУ значительно экономнее.

Модель I/O реактора

I/O reactor e sebetsa e le lera lipakeng tsa mokhethoa oa sistimi le khoutu ea mosebelisi. Molao-motheo oa ts'ebetso ea ona o hlalosoa ke setšoantšo se latelang sa block:

Sebapali se felletseng sa bare-C I/O

  • Напомню, что событие — это уведомление о том, что определённый сокет в состоянии выполнить неблокирующую I/O операцию.
  • Обработчик событий — это функция, вызываемая I/O реактором при получении события, которая далее совершает неблокирующую I/O операцию.

Важно отметить, что I/O реактор по определению однопоточен, но ничего не мешает использовать концепт в многопточной среде в отношении 1 поток: 1 реактор, тем самым утилизируя все ядра ЦПУ.

Ts'ebetsong

Публичный интерфейс мы поместим в файл reactor.h, а реализацию — в reactor.c. reactor.h будет состоять из следующих объявлений:

Показать объявления в 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);

Структура I/O реактора состоит из tlhaloso ea faele селектора epoll и хеш-таблицы GHashTable, которая каждый сокет сопоставляет с CallbackData (структура из обработчика события и аргумента пользователя для него).

Hlahisa Reactor le CallbackData

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

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

Обратите внимание, что мы задействовали возможность обращения с mofuta o sa fellang ho latela index. IN reactor.h мы объявляем структуру reactorle ho reactor.c её определяем, тем самым не позволяя пользователю явно изменять её поля. Это один из паттернов ho pata lintlha, лаконично вписывающийся в семантику Си.

Mesebetsi reactor_register, reactor_deregister и reactor_reregister обновляют список интересующих сокетов и соответствующих обработчиков событий в системном селекторе и в хеш-таблице.

Bontša mesebetsi ea ho ngolisa

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

Kamora hore sebapali sa I/O se kenelle ketsahalo ka mohlalosi fd, он вызывает соответствующего обработчика события, в который передаёт fd, битовую маску сгенерированных событий и пользовательский указатель на void.

Показать функцию 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;
}

Подводя итог, цепочка вызовов функций в пользовательском коде будет принимать следующий вид:

Sebapali se felletseng sa bare-C I/O

Однопоточный сервер

E le ho leka mochine oa I / O tlas'a mojaro o phahameng, re tla ngola sebatli se bonolo sa HTTP se arabelang kopo leha e le efe e nang le setšoantšo.

Краткая справка по протолу HTTP

http — это протокол прикладного уровня, преимущественно использующийся для взаимодействия сервера с браузером.

HTTP можно с лёгкостью использовать поверх транспортного melaoana TCP, отправляя и принимая сообщения формата, определённого tlhaloso.

Формат запроса

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

  • CRLF — это последовательность из двух символов: r и n, разделяющая первую строку запроса, заголовки и данные.
  • <КОМАНДА> — одно из CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Браузер нашему серверу будет отправлять команду GET, e bolelang "Nthomelle litaba tsa faele."
  • <URI> - yнифицированный идентификатор ресурса. Например, если URI = /index.html, joale mofani o kōpa leqephe le ka sehloohong la sebaka seo.
  • <ВЕРСИЯ HTTP> — версия протокола HTTP в формате HTTP/X.Y. Phetolelo e sebelisoang haholo kajeno ke HTTP/1.1.
  • <ЗАГОЛОВОК N> — это пара ключ-значение в формате <КЛЮЧ>: <ЗНАЧЕНИЕ>, отправляемая серверу для дальнейшего анализа.
  • <ДАННЫЕ> — data e hlokoang ke seva ho etsa ts'ebetso. Hangata ho bonolo JSON или любой другой формат.

Формат ответа

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

  • <КОД СТАТУСА> ke nomoro e emelang sephetho sa ts'ebetso. Seva ea rona e tla lula e khutlisetsa boemo ba 200 (ts'ebetso e atlehileng).
  • <ОПИСАНИЕ СТАТУСА> — строковое представление кода статуса. Для кода статуса 200 — это OK.
  • <ЗАГОЛОВОК N> - sehlooho sa sebopeho se tšoanang le sa kopo. Re tla khutlisetsa litlotla Content-Length (boholo ba faele) le Content-Type: text/html (тип возвращаемых данных).
  • <ДАННЫЕ> — запрашиваемые пользователем данные. В нашем случае это путь к изображению в HTML.

faele http_server.c (однопоточный сервер) включает файл common.h, который содержит следующие прототипы функций:

Bontša li-prototypes tsa ts'ebetso tse tšoanang.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);

Также описан функциональный макрос SAFE_CALL() mme mosebetsi o hlalositsoe fail(). Макрос сравнивает значение выражения с ошибкой, и если условие выпонилось, вызывает функцию fail():

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

Mosebetsi fail() печатает переданные аргументы в терминал (как printf()) и завершает работу программы с кодом 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);
}

Mosebetsi new_server() e khutlisa tlhaloso ea faele ea sokete ea "server" e entsoeng ke mehala ea sistimi socket(), bind() и listen() и способного принимать входящие соединения в неблокирующем режиме.

Показать функцию 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;
}

  • Обратите внимание, что сокет изначально создаётся в неблокирующем режиме с помощью флага SOCK_NONBLOCKe le hore mosebetsing on_accept() (читать дальше) системный вызов accept() не остановил исполнение потока.
  • haeba reuse_port e lekana le true, joale mosebetsi ona o tla lokisa sokete ka khetho SO_REUSEPORT ka setsockopt(), чтобы использовать один и тот же порт в многопоточной среде (смотреть секцию «Многопоточный сервер»).

Обработчик событий on_accept() e bitsoa ka mor'a hore OS e hlahise ketsahalo EPOLLIN, в данном случае означающее, что новое соединение может быть принято. on_accept() e amohela khokahanyo e ncha, e e fetolela ho mokhoa o sa thibeleng ebe e ngolisa le motho ea sebetsanang le liketsahalo on_recv() в I/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);
}

Обработчик событий on_recv() e bitsoa ka mor'a hore OS e hlahise ketsahalo EPOLLIN, tabeng ena ho bolela hore khokahano e ngolisitsoe on_accept(), готово к принятию данных.

on_recv() считывает данные из соединения до тех пор, пока HTTP запрос полностью не будет получен, затем она регистрирует обработчик on_send() для отправки HTTP ответа. Если клиент оборвал соединение, то сокет дерегистрируется и закрывается посредством close().

Показать функцию 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);
    }
}

Обработчик событий on_send() e bitsoa ka mor'a hore OS e hlahise ketsahalo EPOLLOUT, ho bolelang hore khokahano e ngolisitsoe on_recv(), готово к отправке данных. Эта функция отправляет HTTP ответ, содержащий HTML с изображением, клиенту, а затем меняет обработчик событий снова на on_recv().

Bontša on_send() tshebetso

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

И наконец, в файле http_server.c, mosebetsing main() мы создаём I/O реактор посредством reactor_new(), создаём серверный сокет и регистрируем его, запускаем реактор с помощью reactor_run() ровно на одну минуту, а затем освобождаем ресурсы и выходим из программы.

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

Ha re hlahlobeng hore na tsohle li sebetsa kamoo ho neng ho lebelletsoe. Ho bokella (chmod a+x compile.sh && ./compile.sh в корне проекта) и запускаем самописный сервер, открываем http://127.0.0.1:18470 ho sebatli 'me u bone seo re se lebelletseng:

Sebapali se felletseng sa bare-C I/O

Tekanyo ya tshebetso

Hlahisa lintlha tsa koloi eaka

$ 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

Измерим производительность однопоточного сервера. Откроем два терминала: в одном запустим ./http_server, в другом — wrk. Спустя минуту во втором терминале высветится следующая статистика:

$ 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

Наш однопоточный сервер смог обработать свыше 11 миллионов запросов в минуту, исходящих из 100 соединений. Неплохой результат, но можно ли его улучшить?

Многопоточный сервер

Как было сказано выше, I/O реактор можно создавать в отдельных потоках, тем самым утилизируя все ядра ЦПУ. Применим данный подход на практике:

Показать 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);
    }
}

Joale khoele e 'ngoe le e 'ngoe владеет собственным реактором:

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

Ka kopo hlokomela hore tlhaloso ea mosebetsi new_server() babuelli true. Это значит, что мы присваиваем серверному сокету опцию SO_REUSEPORTho e sebelisa sebakeng sa likhoele tse ngata. U ka bala lintlha tse ling mona.

Ho matha la bobeli

Теперь измерим производительность многопоточного сервера:

$ 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

Количество обработанных запросов за 1 минуту возросло в ~3.28 раза! Но до круглого числа не хватило всего ~два миллиона, попробуем это исправить.

Pele a re shebeng lipalo-palo tse hlahisitsoeng sehlahisoa:

$ 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

Ho sebelisa CPU Affinity, компиляция с -march=native, PGO, увеличение числа попаданий в polokelo, nyollelo MAX_EVENTS le tšebeliso EPOLLET не дало значительного прироста в производительности. Но что получится, если увеличить количество одновременных соединений?

Lipalopalo tsa likhokahano tse 352 ka nako e le 'ngoe:

$ 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

Желанный результат получен, а вместе с ним и интересный график, демонстрирующий зависимость числа обработанных запросов за 1 минуту от количества соединений:

Sebapali se felletseng sa bare-C I/O

Видим, что после пары сотен соединений число обработанных запросов у обоих серверов резко падает (у многопоточного варианта это более заметно). Связано ли это с реализацией TCP/IP стека Linux? Свои предположения насчёт такого поведения графика и оптимизаций многопоточного и однопоточного вариантов смело пишите в комментариях.

How hlokometsoe litlhalosong, tlhahlobo ena ea ts'ebetso ha e bontše boitšoaro ba mochine oa I / O tlas'a meroalo ea sebele, hobane hoo e batlang e le kamehla seva se sebelisana le database, se hlahisa lintlha, se sebelisa li-cryptography le TLS и т.д., вследствие чего нагрузка становится неоднородной (динамической). Тесты вместе со сторонними компонентами будут проведены в статье про I/O проактор.

Недостатки I/O реактора

U hloka ho utloisisa hore mochini oa I/O ha o na mathata, e leng:

  • Ho sebelisa mochine oa I/O sebakeng se nang le likhoele tse ngata ho batla ho le thata, hobane o tla tlameha ho laola phallo ka letsoho.
  • Практика показывает, что в большинстве случаев нагрузка неоднородна, что может привести к тому, что один поток будет проставивать, пока другой будет загружен работой.
  • Haeba sebatli se le seng sa ketsahalo se thiba khoele, joale mokhethoa oa sistimi ka boeona o tla thibela, e leng se ka lebisang ho liphoso tse thata ho fumana.

E rarolla mathata ana Sebapali sa I/O, зачастую имеющий планировщик, который равномерно распределяет нагрузку в пул потоков, и к тому же имеющий более удобный API. Речь о нём пойдёт позже, в моей другой статье.

fihlela qeto e

На этом наше путешествие из теории прямиком в выхлоп профайлера подошло к концу.

Не стоит на этом останавливаться, ведь существуют множество других не менее интересных подходов к написанию сетевого ПО с разным уровнем удобства и скорости. Интересные, на мой взгляд, ссылки приведены ниже.

Ho fihlela nako e tlang!

Интересные проекты

Что ещё почитать?

Source: www.habr.com

Eketsa ka tlhaloso