Целосно опремен гол В/И реактор

Целосно опремен гол В/И реактор

Вовед

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

Во оваа статија, ќе ги погледнеме критиките на влезниот/излезниот реактор и како тој работи, ќе напишеме имплементација во помалку од 200 линии код и ќе направиме едноставен процес на HTTP сервер над 40 милиони барања/мин.

предговорот

  • Статијата е напишана за да помогне да се разбере функционирањето на В/И реакторот и затоа да се разберат ризиците при неговото користење.
  • Потребно е познавање на основите за да се разбере статијата. C јазик и одредено искуство во развој на мрежни апликации.
  • Целиот код е напишан на C јазик строго според (претпазливост: долг PDF) до стандардот C11 за Linux и достапни на GitHub.

Зошто го направи тоа?

Со растечката популарност на Интернетот, веб-серверите почнаа да имаат потреба да работат со голем број врски истовремено, и затоа беа испробани два пристапа: блокирање на I/O на голем број нишки на оперативниот систем и неблокирање на I/O во комбинација со систем за известување за настани, исто така наречен „системски избирач“ (епол/редица/IOCP/ итн).

Првиот пристап вклучува создавање на нова нишка на ОС за секоја дојдовна врска. Неговиот недостаток е слабата приспособливост: оперативниот систем ќе мора да имплементира многу контекстни транзиции и системски повици. Тие се скапи операции и може да доведат до недостаток на слободна RAM меморија со импресивен број на конекции.

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

Вториот пристап користи систем за известување за настани (системски избирач) обезбеден од ОС. Оваа статија го разгледува најчестиот тип на системски избирач, базиран на предупредувања (настани, известувања) за подготвеноста за I/O операции, наместо на известувања за нивното завршување. Поедноставен пример за неговата употреба може да се претстави со следниов блок дијаграм:

Целосно опремен гол В/И реактор

Разликата помеѓу овие пристапи е како што следува:

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

Да се ​​сумира, резервирањето на нишка на ОС за секое I/O е губење на компјутерската моќ, бидејќи во реалноста, нишките не вршат корисна работа (оттука и терминот „софтверски прекин“). Изборот на системот го решава овој проблем, дозволувајќи му на корисничката програма да ги користи ресурсите на процесорот многу поекономично.

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

Влезниот/излезен реакторот делува како слој помеѓу избирачот на системот и корисничкиот код. Принципот на неговото функционирање е опишан со следниов блок дијаграм:

Целосно опремен гол В/И реактор

  • Да ве потсетам дека настанот е известување дека одреден сокет може да изврши неблокирачка I/O операција.
  • Ракувач на настани е функција која се повикува од В/И реакторот кога се прима настан, кој потоа врши неблокирана В/И операција.

Важно е да се напомене дека В/И реакторот е по дефиниција со еднонавој, но ништо не го спречува концептот да се користи во опкружување со повеќе нишки во сооднос од 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 (структура на управувач со настани и кориснички аргумент за него).

Прикажи Reactor и CallbackData

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

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

Ве молиме имајте предвид дека ја овозможивме можноста за ракување нецелосен тип според индексот. ВО 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;
}

Откако В/И реакторот ќе го пресретне настанот со дескрипторот 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;
}

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

Целосно опремен гол В/И реактор

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

За да го тестираме В/И реакторот под големо оптоварување, ќе напишеме едноставен веб-сервер 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, кој ги содржи следните прототипови на функции:

Прикажи заеднички прототипови на функции.ч

/*
 * Обработчик событий, который вызовется после того, как сокет будет
 * готов принять новое соединение.
 */
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() во В/И реактор.

Прикажи функција 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() создаваме В/И реактор користејќи 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 во прелистувачот и видете што очекувавме:

Целосно опремен гол В/И реактор

Мерење на перформансите

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

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

Сервер со повеќе нишки

Како што беше споменато погоре, В/И реакторот може да се креира во посебни нишки, со што се користат сите јадра на процесорот. Ајде да го примениме овој пристап во пракса:

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

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

$ 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 минута од бројот на конекции:

Целосно опремен гол В/И реактор

Гледаме дека по неколку стотици врски, бројот на обработени барања за двата сервери нагло опаѓа (во верзијата со повеќе нишки ова е позабележително). Дали е ова поврзано со имплементацијата на оџакот на Linux TCP/IP? Слободно можете да ги напишете вашите претпоставки за ваквото однесување на графикот и оптимизациите за опциите со повеќе нишки и со една нишка во коментарите.

Како забележано во коментарите, овој тест за изведба не го покажува однесувањето на В/И реакторот при реални оптоварувања, бидејќи скоро секогаш серверот е во интеракција со базата на податоци, излегува логови, користи криптографија со TLS итн., како резултат на што товарот станува нерамномерен (динамичен). Тестовите заедно со компонентите од трети страни ќе бидат извршени во написот за I/O проакторот.

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

Треба да разберете дека В/И реакторот не е без недостатоци, имено:

  • Користењето на В/И реактор во опкружување со повеќе навој е нешто потешко, бидејќи ќе мора рачно да управувате со тековите.
  • Практиката покажува дека во повеќето случаи оптоварувањето е нерамномерно, што може да доведе до сечење на една нишка додека друга е зафатена со работа.
  • Ако еден управувач со настани блокира нишка, самиот избирач на системот исто така ќе блокира, што може да доведе до грешки кои тешко се наоѓаат.

Ги решава овие проблеми I/O проактор, кој често има распоредувач кој рамномерно го распределува оптоварувањето на базен од нишки, а исто така има и поудобен API. Ќе зборуваме за тоа подоцна, во мојата друга статија.

Заклучок

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

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

До следниот пат!

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

Што друго да читам?

Извор: www.habr.com

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