
Увядзенне
(аднаструменны ) - гэта патэрн для напісання высоканагружанага ПА, які выкарыстоўваецца ў многіх папулярных рашэннях:
- ...
У дадзеным артыкуле мы разгледзім паднаготную I/O рэактара і прынцып яго працы, напішам рэалізацыю на менш, чым 200 радкоў кода і прымусім просты HTTP сервер апрацоўваць звыш 40 мільёнаў запытаў/мін.
Прадмова
- Артыкул напісаны з мэтай дапамагчы разабрацца ў функцыянаванні I/O рэактара, а значыць і ўсвядоміць рызыкі пры яго выкарыстанні.
- Для засваення артыкула патрабуецца веданне асноў і невялікі досвед распрацоўкі сеткавых прыкладанняў.
- Увесь код напісаны на мове Сі строга па (асцярожна: доўгі PDF) для Linux і даступны на .
Навошта гэта трэба?
З ростам папулярнасці Інтэрнэту вэб-серверам стала трэба апрацоўваць вялікую колькасць злучэнняў адначасова, у сувязі з чым было апрабавана два падыходу: блакавальнае I/O на вялікім ліку струменяў АС і неблакавальнае I/O у камбінацыі з сістэмай абвесткі аб падзеях, яшчэ званай «сістэмным». селектарам» (///etc).
Першы падыход меў на ўвазе стварэнне новага струменя АС для кожнага ўваходнага злучэння. Яго недахопам з'яўляецца дрэнная маштабаванасць: аперацыйнай сістэме давядзецца ажыццяўляць мноства и . Яны з'яўляюцца дарагімі аперацыямі і могуць прывесці да недахопу вольнай АЗП пры вялікім ліку злучэнняў.
Мадыфікаваная версія вылучае (thread pool), тым самым не дазваляючы сістэме аварыйна спыніць выкананне, але разам з тым прыўносіць новую праблему: калі ў дадзены момант часу пул патокаў блакуюць працяглыя аперацыі чытання, то іншыя сокеты, якія ўжо ў стане прыняць дадзеныя, не змогуць гэтага зрабіць.
Другі падыход выкарыстоўвае (сістэмны селектар), якую падае АС. У дадзеным артыкуле разгледжаны найболей часта сустракаемы выгляд сістэмнага селектара, заснаваны на абвестках (падзеях, апавяшчэннях) аб гатовасці да I/O аперацыям, чым на . Спрошчаны прыклад яго выкарыстання можна ўявіць наступнай блок-схемай:

Розніца паміж дадзенымі падыходамі заключаецца ў наступным:
- Блакавальныя I/O аперацыі прыпыняюць карыстацкі паток да таго часу, пакуль АС належным чынам не абітурыенты у паток байт (, атрыманне дадзеных) або не вызваліцца дастаткова месца ва ўнутраных буферах запісы для наступнай адпраўкі праз (адпраўка дадзеных).
- Сістэмны селектар праз некаторы час паведамляе праграму аб тым, што АС ўжо дэфрагментавала IP пакеты (TCP, атрыманне дадзеных) або дастаткова месца ва ўнутраных буферах запісу ўжо даступна (адпраўка дадзеных).
Падводзячы вынік, рэзерваванне струменя АС для кожнага I/O – пустое марнаванне вылічальнай моцы, бо насамрэч, струмені не занятыя карыснай працай (адгэтуль бярэ свае карані тэрмін ). Сістэмны селектар вырашае гэтую праблему, дазваляючы карыстацкай праграме расходаваць рэсурсы ЦПУ значна эканомней.
Мадэль I/O рэактара
I/O рэактар выступае як праслойка паміж сістэмным селектарам і карыстацкім кодам. Прынцып яго працы апісаны наступнай блок-схемай:

- Нагадаю, што падзея – гэта апавяшчэнне аб тым, што пэўны сокет у стане выканаць неблакіруючую I/O аперацыю.
- Апрацоўшчык падзей – гэта функцыя, якая выклікаецца I/O рэактарам пры атрыманні падзеі, якая далей здзяйсняе неблакіруючую I/O аперацыю.
Важна адзначыць, што I/O рэактар па вызначэнні однопоточен, але нічога не мяшае выкарыстоўваць канцэпт у многопточной асяроддзі ў стаўленні 1 струмень: 1 рэактар, тым самым утылізуючы ўсе ядры ЦПУ.
Рэалізацыя
Публічны інтэрфейс мы змесцім у файл , а рэалізацыю - у . 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 рэактара складаецца з селектара и , якая кожны сокет супастаўляе з 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 < 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 рэактар на высокай нагрузцы, мы напішам просты HTTP вэб-сервер, на любы запыт які адказвае выявай.
Кароткая даведка па пратоле HTTP
- гэта пратакол , пераважна выкарыстоўваецца для ўзаемадзеяння сервера з браўзэрам.
HTTP можна з лёгкасцю выкарыстоўваць па-над пратаколу , адпраўляючы і прымаючы паведамленні фармату, вызначанага .
Фармат запыту
<КОМАНДА> <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>- гэта пара ключ-значэнне ў фармаце<КЛЮЧ>: <ЗНАЧЕНИЕ>, якая адпраўляецца серверу для далейшага аналізу.<ДАННЫЕ>- Дадзеныя, патрабаваныя серверу для выканання аперацыі. Часта гэта проста ці любы іншы фармат.
Фармат адказу
<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ><КОД СТАТУСА>- Гэты лік, якое ўяўляе сабой вынік аперацыі. Наш сервер будзе заўсёды вяртаць статут 200 (паспяховая аперацыя).<ОПИСАНИЕ СТАТУСА>- Радковае прадстаўленне кода статусу. Для кода статусу 200 - гэтаOK.<ЗАГОЛОВОК N>- загаловак таго ж фармату, што і ў запыце. Мы будзем вяртаць загалоўкіContent-Length(памер файла) іContent-Type: text/html(тып якія вяртаюцца дадзеных).<ДАННЫЕ>- Запытаныя карыстальнікам дадзеныя. У нашым выпадку гэта шлях да выявы ў .
файл (аднаструменны сервер) уключае файл , які змяшчае наступныя прататыпы функцый:
Паказаць прататыпы функцый у 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() друкуе перададзеныя аргументы ў тэрмінал (як ) і завяршае працу праграмы з кодам 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() вяртае файлавы дэскрыптар «сервернага» сокета, створанага сістэмнымі выклікамі , и і здольнага прымаць уваходныя злучэнні ў неблакавальным рэжыме.
Паказаць функцыю 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, то дадзеная функцыя сканфігуруе сокет з опцыяй з дапамогай , каб выкарыстоўваць адзін і той жа порт у шматструменным асяроддзі (глядзець секцыю «Шматструменны сервер»).
Апрацоўшчык падзей 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 адказу. Калі кліент абарваў злучэнне, то сокет дэрэгіструецца і зачыняецца пасродкам .
Паказаць функцыю 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 у корані праекта) і запускаем самапісны сервер, адкрываем у браўзэры і назіраем тое, што і чакалі:

Замер прадукцыйнасці
Паказаць характарыстыкі маёй машыны
$ 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. Гэта значыць, што мы прысвойваем сервернаму сокету опцыю , Каб выкарыстоўваць яго ў шматструменным асяроддзі. Падрабязней можаце пачытаць .
Другі заход
Зараз вымераем прадукцыйнасць шматструменнага сервера:
$ 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 разы! Але да круглага ліку не хапіла ўсяго ~два мільёны, паспрабуем гэта выправіць.
Спачатку паглядзім на статыстыку, згенераваную :
$ 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 хвіліну ад колькасці злучэнняў:

Бачым, што пасля пары сотняў злучэнняў лік апрацаваных запытаў у абодвух сервераў рэзка падае (у шматструменнага варыянту гэта больш прыкметна). Ці звязана гэта з рэалізацыяй TCP/IP стэка Linux? Свае здагадкі наконт такіх паводзін графіка і аптымізацый шматструменнага і аднаструменнага варыянтаў смела пішыце ў каментарах.
Як у каментарах, дадзены тэст прадукцыйнасці не паказвае паводзін I/O рэактара на рэальных нагрузках, бо амаль заўсёды сервер узаемадзейнічае з БД, выводзіць логі, выкарыстае крыптаграфію з і г.д., з прычыны чаго нагрузка становіцца неаднароднай (дынамічнай). Тэсты разам з іншымі кампанентамі будуць праведзены ў артыкуле пра I/O праактар.
Недахопы I/O рэактара
Трэба разумець, што I/O рэактар не пазбаўлены недахопаў, а менавіта:
- Карыстацца I/O рэактарам у шматструменным асяроддзі некалькі складаней, т.я. давядзецца ўручную кіраваць патокамі.
- Практыка паказвае, што ў большасці выпадкаў нагрузка неаднастайная, што можа прывесці да таго, што адзін струмень будзе прастаўляць, пакуль іншы будзе загружаны працай.
- Калі адзін апрацоўшчык падзеі заблакуе струмень, то таксама заблакуецца і сам сістэмны селектар, што можа прывесці да цяжкаадлоўных багаў.
Гэтыя праблемы вырашае , часта які мае планавальнік, які раўнамерна размяркоўвае нагрузку ў пул струменяў, і да таго ж які мае больш зручны API. Гаворка пра яго пойдзе пазней, у маім іншым артыкуле.
Заключэнне
На гэтым наша вандраванне з тэорыі наўпрост у выхлап прафайлера падышло да канца.
Не варта на гэтым спыняцца, бо існуюць мноства іншых не менш цікавых падыходаў да напісання сеткавага ПЗ з розным узроўнем зручнасці і хуткасці. Цікавыя, на мой погляд, спасылкі прыведзены ніжэй.
Да новых сустрэч!
Цікавыя праекты
- сі
Што яшчэ пачытаць?
Крыніца: habr.com
