Пълнофункционален баре-С I/O реактор

Пълнофункционален баре-С I/O реактор

въведение

I/O реактор (една резба цикъл на събитието) е модел за писане на софтуер с високо натоварване, използван в много популярни решения:

В тази статия ще разгледаме тънкостите на един I/O реактор и как работи, ще напишем имплементация в по-малко от 200 реда код и ще направим прост HTTP сървърен процес над 40 милиона заявки/мин.

предговор

  • Статията е написана, за да ви помогне да разберете функционирането на I/O реактора и следователно да разберете рисковете при използването му.
  • Познаването на основите е необходимо, за да разберете статията. C език и известен опит в разработката на мрежови приложения.
  • Целият код е написан на език C строго според (Внимание: дълъг PDF) по стандарт C11 за Linux и достъпен на GitHub.

Защо го направи?

С нарастващата популярност на Интернет, уеб сървърите започнаха да се нуждаят да обработват голям брой връзки едновременно и затова бяха изпробвани два подхода: блокиране на I/O на голям брой OS нишки и неблокиращ I/O в комбинация с система за уведомяване за събития, наричана още „системен селектор“ (epoll/kqueue/IOCP/и т.н.).

Първият подход включваше създаване на нова OS нишка за всяка входяща връзка. Неговият недостатък е слабата мащабируемост: операционната система ще трябва да внедри много контекстни преходи и системни повиквания. Те са скъпи операции и могат да доведат до липса на свободна RAM памет при внушителен брой връзки.

Модифицираната версия подчертава фиксиран брой нишки (пул от нишки), като по този начин предотвратява срив на системата, но в същото време въвежда нов проблем: ако пул от нишки в момента е блокиран от дълги операции за четене, тогава други сокети, които вече могат да получават данни, няма да могат да правят така.

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

Пълнофункционален баре-С I/O реактор

Разликата между тези подходи е следната:

  • Блокиране на I/O операции спирам потребителски поток додокато ОС не се оправи дефрагментира входящи IP пакети към байтов поток (TCP, получаване на данни) или няма да има достатъчно място във вътрешните буфери за запис за последващо изпращане чрез NIC (изпращане на данни).
  • Системен селектор с течение на времето уведомява програмата, че ОС вече дефрагментирани IP пакети (TCP, приемане на данни) или достатъчно място във вътрешните буфери за запис вече налични (изпращане на данни).

За да обобщим, запазването на OS нишка за всеки I/O е загуба на изчислителна мощност, защото в действителност нишките не вършат полезна работа (оттук идва терминът "софтуерно прекъсване"). Системният селектор решава този проблем, като позволява на потребителската програма да използва ресурсите на процесора много по-икономично.

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

I/O реакторът действа като слой между системния селектор и потребителския код. Принципът на неговата работа е описан със следната блокова схема:

Пълнофункционален баре-С I/O реактор

  • Нека ви напомня, че събитието е известие, че определен сокет може да извърши неблокираща I/O операция.
  • Манипулатор на събитие е функция, извикана от I/O реактора при получаване на събитие, която след това изпълнява неблокираща I/O операция.

Важно е да се отбележи, че I/O реакторът по дефиниция е еднонишков, но нищо не пречи концепцията да се използва в многонишкова среда при съотношение 1 нишка: 1 реактор, като по този начин се рециклират всички CPU ядра.

Изпълнение

Ще поставим публичния интерфейс във файл 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 реактора се състои от файлов дескриптор селектор epoll и хеш таблици GHashTable, който картографира всеки сокет към CallbackData (структура на манипулатор на събитие и потребителски аргумент за него).

Показване на Reactor и CallbackData

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

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

Моля, имайте предвид, че сме активирали възможността за обработка непълен тип според индекса. IN reactor.h ние декларираме структурата reactorИ в reactor.c ние го дефинираме, като по този начин не позволяваме на потребителя изрично да променя неговите полета. Това е една от моделите скриване на данни, което накратко се вписва в семантиката на C.

функции reactor_register, reactor_deregister и reactor_reregister актуализирайте списъка с интересни сокети и съответните манипулатори на събития в системния селектор и хеш таблицата.

Показване на функциите за регистрация

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

След като I/O реакторът прихване събитието с дескриптора 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;
}

За да обобщим, веригата от извиквания на функции в потребителския код ще приеме следната форма:

Пълнофункционален баре-С I/O реактор

Сървър с една нишка

За да тестваме I/O реактора при високо натоварване, ще напишем прост HTTP уеб сървър, който отговаря на всяка заявка с изображение.

Кратка справка за HTTP протокола

HTTP - това е протоколът ниво на приложение, използван предимно за взаимодействие сървър-браузър.

HTTP може лесно да се използва над транспорт протокол TCP, изпращане и получаване на съобщения в зададен формат спецификация.

Формат на заявката

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

  • CRLF е последователност от два знака: r и n, разделяйки първия ред на заявката, заглавките и данните.
  • <КОМАНДА> - един от CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Браузърът ще изпрати команда до нашия сървър GET, което означава "Изпратете ми съдържанието на файла."
  • <URI> - унифициран идентификатор на ресурс. Например, ако URI = /index.html, тогава клиентът иска главната страница на сайта.
  • <ВЕРСИЯ HTTP> — версия на HTTP протокола във формата HTTP/X.Y. Най-често използваната версия днес е HTTP/1.1.
  • <ЗАГОЛОВОК N> е двойка ключ-стойност във формата <КЛЮЧ>: <ЗНАЧЕНИЕ>, изпратени до сървъра за допълнителен анализ.
  • <ДАННЫЕ> — данни, изисквани от сървъра за извършване на операцията. Често е просто JSON или всеки друг формат.

Формат на отговора

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

  • <КОД СТАТУСА> е число, представляващо резултата от операцията. Нашият сървър винаги ще връща статус 200 (успешна операция).
  • <ОПИСАНИЕ СТАТУСА> — низово представяне на кода на състоянието. За код на състояние 200 това е OK.
  • <ЗАГОЛОВОК N> — заглавка със същия формат като в заявката. Ще върнем заглавията Content-Length (размер на файла) и Content-Type: text/html (връщан тип данни).
  • <ДАННЫЕ> — данни, поискани от потребителя. В нашия случай това е пътят към изображението в HTML.

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

Показване на прототипи на функции в common.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() и функцията е дефинирана fail(). Макросът сравнява стойността на израза с грешката и ако условието е вярно, извиква функцията fail():

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

Функция 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);
}

Функция new_server() връща файловия дескриптор на "сървърния" сокет, създаден от системни извиквания 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_NONBLOCKтака че във функцията on_accept() (прочетете повече) системно повикване accept() не спря изпълнението на нишката.
  • ако reuse_port е равно на true, тогава тази функция ще конфигурира гнездото с опцията SO_REUSEPORT през setsockopt()да използвате същия порт в многонишкова среда (вижте раздела „Многонишков сървър“).

Манипулатор на събития on_accept() извиква се след като ОС генерира събитие EPOLLIN, в този случай означава, че новата връзка може да бъде приета. on_accept() приема нова връзка, превключва я в неблокиращ режим и се регистрира в манипулатор на събития 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() извиква се след като ОС генерира събитие EPOLLIN, в този случай означава, че връзката е регистрирана 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() извиква се след като ОС генерира събитие EPOLLOUT, което означава, че връзката е регистрирана on_recv(), готов за изпращане на данни. Тази функция изпраща HTTP отговор, съдържащ HTML с изображение, към клиента и след това променя манипулатора на събития обратно на on_recv().

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

И накрая, във файла http_server.c, във функция main() ние създаваме I/O реактор, използвайки reactor_new(), създайте сървърен сокет и го регистрирайте, стартирайте реактора, като използвате reactor_run() за точно една минута, след което освобождаваме ресурси и излизаме от програмата.

Показване на 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);
}

Нека проверим дали всичко работи според очакванията. Компилиране (chmod a+x compile.sh && ./compile.sh в корена на проекта) и стартирайте самонаписания сървър, отворете http://127.0.0.1:18470 в браузъра и вижте какво очаквахме:

Пълнофункционален баре-С I/O реактор

Измерване на резултатите

Покажи спецификациите на колата ми

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

Сега всяка нишка притежава своя собствена реактор:

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

Моля, обърнете внимание, че аргументът на функцията new_server() актове true. Това означава, че присвояваме опцията на сървърния сокет SO_REUSEPORTда го използвате в многонишкова среда. Можете да прочетете повече подробности тук.

Второ бягане

Сега нека измерим производителността на многонишков сървър:

$ 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 пъти! Но не ни достигаха само ~XNUMX милиона до кръглото число, така че нека се опитаме да поправим това.

Първо нека да разгледаме генерираната статистика 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

Използване на CPU Affinity, компилация с -march=native, PGO, увеличаване на броя на посещенията скривалище, нараства MAX_EVENTS и използвайте EPOLLET не даде значително увеличение на производителността. Но какво се случва, ако увеличите броя на едновременните връзки?

Статистика за 352 едновременни връзки:

$ 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 минута от броя на връзките:

Пълнофункционален баре-С I/O реактор

Виждаме, че след няколкостотин връзки броят на обработените заявки за двата сървъра рязко спада (в многонишковата версия това е по-забележимо). Това свързано ли е с внедряването на TCP/IP стека на Linux? Чувствайте се свободни да напишете вашите предположения относно това поведение на графиката и оптимизациите за многонишкови и еднонишкови опции в коментарите.

Като отбеляза в коментарите, този тест за производителност не показва поведението на I/O реактора при реални натоварвания, защото почти винаги сървърът взаимодейства с базата данни, извежда регистрационни файлове, използва криптография с TLS и др., в резултат на което натоварването става неравномерно (динамично). Тестовете заедно с компоненти на трети страни ще бъдат извършени в статията за I/O proactor.

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

Трябва да разберете, че I/O реакторът не е без своите недостатъци, а именно:

  • Използването на I/O реактор в многонишкова среда е малко по-трудно, защото ще трябва ръчно да управлявате потоците.
  • Практиката показва, че в повечето случаи натоварването е неравномерно, което може да доведе до регистриране на една нишка, докато друга е заета с работа.
  • Ако един манипулатор на събития блокира нишка, самият системен селектор също ще блокира, което може да доведе до трудни за намиране грешки.

Решава тези проблеми I/O проектор, който често има планировчик, който равномерно разпределя натоварването към набор от нишки, а също така има по-удобен API. Ще говорим за това по-късно, в другата ми статия.

Заключение

Това е мястото, където нашето пътуване от теорията направо към профилиращия ауспух приключи.

Не трябва да се спирате на това, защото има много други също толкова интересни подходи за писане на мрежов софтуер с различни нива на удобство и скорост. Интересни, според мен, връзки са дадени по-долу.

Ще се видим отново!

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

Какво друго да прочета?

Източник: www.habr.com

Добавяне на нов коментар