Потпуно функционалан И/О реактор са голим Ц

Потпуно функционалан И/О реактор са голим Ц

Увод

И/О реактор (једноструки навој петља догађаја) је образац за писање софтвера са великим оптерећењем, који се користи у многим популарним решењима:

У овом чланку ћемо погледати све детаље И/О реактора и како он функционише, написати имплементацију у мање од 200 линија кода и направити једноставан ХТТП серверски процес преко 40 милиона захтева/мин.

Предговор

  • Чланак је написан да би помогао у разумевању функционисања И/О реактора, а самим тим и разумевању ризика при његовом коришћењу.
  • За разумевање чланка потребно је познавање основа. Ц језик и одређено искуство у развоју мрежних апликација.
  • Сав код је написан на језику Ц стриктно према (опрез: дугачак ПДФ) према стандарду Ц11 за Линук и доступно на ГитХуб.

Зашто то?

Са растућом популарношћу Интернета, веб сервери су почели да имају потребу да истовремено руководе великим бројем конекција, па су стога покушана два приступа: блокирање И/О на великом броју ОС нити и неблокирање И/О у комбинацији са систем за обавештавање о догађајима, који се такође назива „селектор система“ (еполл/ккуеуе/ИОЦП/етц).

Први приступ је укључивао креирање нове ОС нити за сваку долазну везу. Његов недостатак је лоша скалабилност: оперативни систем ће морати да имплементира многе прелазе контекста и системски позиви. Оне су скупе операције и могу довести до недостатка слободне РАМ меморије са импресивним бројем конекција.

Измењена верзија истиче фиксни број нити (скуп нити), чиме се спречава систем да прекине извршење, али у исто време уводи нови проблем: ако је скуп нити тренутно блокиран дугим операцијама читања, онда друге утичнице које већ могу да примају податке неће моћи да урадити тако.

Други приступ користи систем обавештавања о догађајима (селектор система) који обезбеђује ОС. Овај чланак говори о најчешћем типу селектора система, заснованом на упозорењима (догађаји, обавештења) о спремности за И/О операције, а не на обавештења о њиховом завршетку. Поједностављени пример његове употребе може бити представљен следећим блок дијаграмом:

Потпуно функционалан И/О реактор са голим Ц

Разлика између ових приступа је следећа:

  • Блокирање И/О операција суспендовати проток корисника све докдок ОС не буде исправно дефрагментира долазни ИП пакети у ток бајтова (ТЦП, пријем података) или неће бити довољно слободног простора у интерним баферима за уписивање за накнадно слање путем НИЦ (слање података).
  • Системски бирач током времена обавештава програм да ОС већ дефрагментирани ИП пакети (ТЦП, пријем података) или довољно простора у интерним баферима за писање већ доступно (слање података).

Да сумирамо, резервисање ОС нити за сваки И/О је губитак рачунарске снаге, јер у стварности, нити не обављају користан посао (отуда термин "софтверски прекид"). Системски селектор решава овај проблем, дозвољавајући корисничком програму да много економичније користи ЦПУ ресурсе.

Модел И/О реактора

И/О реактор делује као слој између селектора система и корисничког кода. Принцип његовог рада описан је следећим блок дијаграмом:

Потпуно функционалан И/О реактор са голим Ц

  • Дозволите ми да вас подсетим да је догађај обавештење да је одређена утичница у стању да изврши неблокирајућу И/О операцију.
  • Руковалац догађаја је функција коју позива И/О реактор када се прими догађај, а која затим изводи неблокирајућу У/И операцију.

Важно је напоменути да је И/О реактор по дефиницији једнонитни, али ништа не спречава да се концепт користи у вишенитном окружењу у односу 1 нит: 1 реактор, чиме се рециклирају сва ЦПУ језгра.

Имплементација

Јавни интерфејс ћемо поставити у датотеку reactor.h, а имплементација - у reactor.c. 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);

Структура И/О реактора састоји се од дескриптор датотеке селектор еполл и хеш табеле GHashTable, који мапира сваку утичницу у 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;
}

Након што је И/О реактор пресрео догађај са дескриптором fd, позива одговарајући обрађивач догађаја, на који пролази fd, бит маска генерисани догађаји и показивач корисника на void.

Прикажи функцију реацтор_рун().

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

Да резимирамо, ланац позива функција у корисничком коду ће имати следећи облик:

Потпуно функционалан И/О реактор са голим Ц

Сервер са једним навојем

Да бисмо тестирали И/О реактор под великим оптерећењем, написаћемо једноставан ХТТП веб сервер који на сваки захтев одговара сликом.

Брза референца на ХТТП протокол

ХТТП - ово је протокол ниво апликације, првенствено се користи за интеракцију између сервера и претраживача.

ХТТП се може лако користити преко транспорт protokola ТЦП, слање и примање порука у одређеном формату спецификација.

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

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

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

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

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

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

фајл http_server.c (сингле тхреадед сервер) укључује датотеку 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() и способан да прихвати долазне везе у режиму без блокирања.

Прикажи функцију нев_сервер().

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() у И/О реактору.

Прикажи функцију он_аццепт().

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() чита податке из везе све док ХТТП захтев није у потпуности примљен, а затим региструје руковалац on_send() да бисте послали ХТТП одговор. Ако клијент прекине везу, утичница се одјављује и затвара помоћу close().

Прикажи функцију он_рецв()

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(), спреман за слање података. Ова функција шаље клијенту ХТТП одговор који садржи ХТМЛ са сликом, а затим враћа обрађивач догађаја на on_recv().

Прикажи функцију он_сенд().

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() креирамо И/О реактор користећи reactor_new(), креирајте серверску утичницу и региструјте је, покрените реактор користећи reactor_run() тачно један минут, а затим ослобађамо ресурсе и излазимо из програма.

Прикажи хттп_сервер.ц

#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 у претраживачу и видите шта смо очекивали:

Потпуно функционалан И/О реактор са голим Ц

Мерење учинка

Покажи спецификације мог аутомобила

$ 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 веза. Није лош резултат, али може ли се побољшати?

Вишенитни сервер

Као што је горе поменуто, И/О реактор се може креирати у одвојеним нитима, користећи на тај начин сва ЦПУ језгра. Хајде да применимо овај приступ у пракси:

Прикажи хттп_сервер_мултитхреадед.ц

#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 милиона до округлог броја, па хајде да покушамо да то поправимо.

Прво погледајмо генерисану статистику перф:

$ 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, ПГО, повећање броја погодака готовина, повећати 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 минут од броја веза:

Потпуно функционалан И/О реактор са голим Ц

Видимо да након пар стотина конекција број обрађених захтева за оба сервера нагло опада (у верзији са више нити то је уочљивије). Да ли је ово повезано са имплементацијом Линук ТЦП/ИП стека? Слободно напишите своје претпоставке о оваквом понашању графикона и оптимизацијама за вишенитне и једнонитне опције у коментарима.

Као приметио у коментарима, овај тест перформанси не показује понашање И/О реактора под реалним оптерећењем, јер скоро увек сервер комуницира са базом података, излази евиденције, користи криптографију са ТЛС итд., услед чега оптерећење постаје неуједначено (динамичко). Тестови заједно са компонентама треће стране биће спроведени у чланку о И/О проактору.

Недостаци И/О реактора

Морате схватити да И/О реактор није без својих недостатака, а то су:

  • Коришћење И/О реактора у вишенитном окружењу је нешто теже, јер мораћете ручно да управљате токовима.
  • Пракса показује да је оптерећење у већини случајева неуједначено, што може довести до евидентирања једне нити док је друга заузета послом.
  • Ако један обрађивач догађаја блокира нит, онда ће и сам системски бирач блокирати, што може довести до грешака које је тешко пронаћи.

Решава ове проблеме И/О проацтор, који често има планер који равномерно распоређује оптерећење на скуп нити, а такође има и практичнији АПИ. О томе ћемо касније, у мом другом чланку.

Закључак

Ово је место где се наше путовање од теорије директно до издувног система профилера завршило.

Не би требало да се задржавате на томе, јер постоји много других подједнако занимљивих приступа писању мрежног софтвера са различитим нивоима погодности и брзине. Занимљиво, по мом мишљењу, линкови су дати испод.

До следећег пута!

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

Шта још да читам?

Извор: ввв.хабр.цом

Додај коментар