Функционален I/O реактор на чист C

Функционален I/O реактор на чист C

Въведение

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

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

Предговор

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

Защо е необходимо?

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

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

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

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

Функционален I/O реактор на чист C

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

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

В заключение, резервирането на потока на ОС за всяко I/O е безсмислено изразходване на изчислителна мощност, тъй като на практика, потокът не е зает с полезна работа (оттук произлиза термина «програмно прекъсване»). Системният селектор решава този проблем, позволявайки на потребителската програма да изразходва ресурси на CPU значително по-икономично.

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

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

Функционален I/O реактор на чист C

  • Напомням, че събитие е уведомление, че определен сокет е в състояние да извърши неблокираща 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;

Имайте предвид, че използвахме възможността за работа с непълен тип чрез указател. В reactor.h обявяваме структурата reactor, а в reactor.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 table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

В заключение, веригата от функции в потребителския код ще изглежда по следния начин:

Функционален I/O реактор на чист C

Однопоточен сървър

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

Кратко ръководство по HTTP протокола

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

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

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

CRLF
CRLF
CRLF
CRLF CRLF

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

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

CRLF
CRLF
CRLF
CRLF CRLF

  • <КОД СТАТУСА> — това число представлява резултата от операцията. Нашият сървър винаги ще връща статус 200 (успешна операция).
  • <ОПИСАНИЕ СТАТУСА> — строково представяне на статус кода. За статус код 200 — това е OK.
  • — това е двойка ключ-стойност в формат — заглавие в същия формат, както при заявката. Ние ще връщаме заглавията 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() печатa предадените аргументи в терминала (както 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 е равно на истинно, тогава тази функция ще конфигурира сокета с опцията 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()

статичен 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 реактор на чист C

Замерване на производителността

Покажи характеристиките на машината ми

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

Сега всеки поток владее собствен реактор:

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

Обърнете внимание, че аргументът на функцията new_server() е истинно. Това означава, че присвояваме на сървърния сокет опция 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 пъти! Но до кръгла цифра не достигнаха почти ~два милиона, да опитаме да го поправим.

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

Използване на аффинност на ЦПУ, компилация с -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 реактор на чист C

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

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

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

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

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

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

Заключение

С това нашето пътуване от теория директно към изхода на профайлера завършва.

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

До нови срещи!

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

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

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

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster