Nan atik sa a, nou pral gade nan sa ki genyen nan yon reaktè I/O ak fason li fonksyone, ekri yon aplikasyon nan mwens pase 200 liy nan kòd, epi fè yon pwosesis senp sèvè HTTP plis pase 40 milyon demann / min.
Avètisman
Atik la te ekri pou ede konprann fonksyònman I/O réacteurs, Et donc konprann risk ki genyen lè w ap itilize li.
Konesans de baz yo oblije konprann atik la. lang C ak kèk eksperyans nan devlopman aplikasyon rezo.
Tout kòd ekri nan lang C entèdi dapre (prekosyon: PDF long) nan estanda C11 pou Linux ak disponib sou GitHub.
Poukisa sa nesesè?
Avèk popilarite k ap grandi nan Entènèt la, sèvè entènèt yo te kòmanse bezwen okipe yon gwo kantite koneksyon ansanm, ak Se poutèt sa de apwòch yo te eseye: bloke I / O sou yon gwo kantite fil OS ak ki pa bloke I / O an konbinezon ak yon sistèm notifikasyon evènman, yo rele tou "seleksyon sistèm" (epol/kqueue/IOCP/etc).
Premye apwòch la enplike kreye yon nouvo fil OS pou chak koneksyon fèk ap rantre. Dezavantaj li se pòv évolutivité: sistèm operasyon an ap gen pou aplike anpil tranzisyon kontèks и apèl sistèm yo. Yo se operasyon chè epi yo ka mennen nan yon mank de RAM gratis ak yon kantite enpresyonan koneksyon.
Vèsyon an modifye mete aksan sou kantite fil fiks (pisin fil), kidonk anpeche sistèm nan avòte ekzekisyon, men an menm tan an entwodwi yon nouvo pwoblèm: si yon pisin fil se kounye a bloke pa operasyon lekti long, Lè sa a, lòt sipò ki deja kapab resevwa done yo pa pral kapab. fè sa.
Dezyèm apwòch la itilize sistèm notifikasyon evènman (Seleksyon sistèm) ki ofri pa OS la. Atik sa a diskite sou kalite ki pi komen nan seleksyon sistèm, ki baze sou alèt (evènman, notifikasyon) sou preparasyon pou operasyon I/O, olye ke sou notifikasyon sou fini yo. Yon egzanp senplifye itilizasyon li yo ka reprezante pa dyagram blòk sa a:
Diferans ki genyen ant apwòch sa yo se jan sa a:
Bloke operasyon I/O sispann koule itilizatè jiskaskejiskaske eksplwatasyon an byen defragmantasyon fèk ap rantre IP pake to byte stream (Tchp, k ap resevwa done) oswa pa pral gen ase espas ki disponib nan tanpon ekri entèn yo pou voye apre yo atravè Nic (voye done).
Seleksyon sistèm sou tan notifye pwogram nan ke OS la deja pake IP defragmante (TCP, resepsyon done) oswa ase espas nan tanpon ekri entèn yo deja disponib (voye done).
Pou rezime li, rezève yon fil OS pou chak I/O se yon fatra nan pouvwa informatique, paske an reyalite, fil yo pa fè travay itil (kidonk tèm nan "entèwonp lojisyèl"). Seleksyon sistèm nan rezoud pwoblèm sa a, ki pèmèt pwogram itilizatè a sèvi ak resous CPU pi plis ekonomikman.
I/O modèl réacteurs
Reaktè I/O aji kòm yon kouch ant seleksyon sistèm lan ak kòd itilizatè a. Prensip operasyon li yo dekri nan dyagram blòk sa a:
Kite m fè w sonje ke yon evènman se yon notifikasyon ke yon priz sèten kapab fè yon operasyon I/O ki pa bloke.
Yon moun kap okipe evènman an se yon fonksyon ki rele I/O réacteurs lè yo resevwa yon evènman, ki answit fè yon operasyon I/O ki pa bloke.
Li enpòtan pou sonje ke I/O reyaktè a se pa definisyon yon sèl-threaded, men pa gen anyen sispann konsèp nan yo te itilize nan yon anviwònman milti-threaded nan yon rapò nan 1 fil: 1 raktor, kidonk resiklaj tout nwayo CPU.
Aplikasyon
Nou pral mete koòdone piblik la nan yon dosye reactor.h, ak aplikasyon - nan reactor.c. reactor.h pral konpoze de anons sa yo:
Montre deklarasyon nan 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);
Estrikti réacteurs I/O a konsiste de deskriptè dosye seleksyon epol и tab hashGHashTable, ki kat chak priz CallbackData (estrikti yon moun kap okipe evènman ak yon agiman itilizatè pou li).
Tanpri sonje ke nou te pèmèt kapasite nan okipe kalite enkonplè dapre endèks la. NAN reactor.h nou deklare estrikti a reactor, ak nan reactor.c nou defini li, kidonk anpeche itilizatè a chanje klèman jaden li yo. Sa a se youn nan modèl yo kache done yo, ki byen anfòm nan C semantik.
Fonksyon reactor_register, reactor_deregister и reactor_reregister aktyalize lis sipò enterè yo ak moun kap okipe evènman ki koresponn lan nan seleksyon sistèm lan ak tab hash.
Apre I/O reyaktè a te entèsepte evènman an ak deskriptè a fd, li rele moun kap okipe evènman ki koresponn lan, kote li pase fd, ti jan mask evènman ki te pwodwi ak yon konsèy itilizatè a void.
Montre fonksyon 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;
}
Pou rezime, chèn apèl fonksyon nan kòd itilizatè a pral pran fòm sa a:
Single sèvè threaded
Yo nan lòd yo teste I / O reyaktè a anba gwo chaj, nou pral ekri yon senp sèvè entènèt HTTP ki reponn a nenpòt demann ak yon imaj.
Yon referans rapid sou pwotokòl HTTP
HTTP - sa a se pwotokòl la nivo aplikasyon an, prensipalman itilize pou entèraksyon sèvè-navigatè.
HTTP ka fasil pou itilize sou transpò pwotokòl Tchp, voye ak resevwa mesaj nan yon fòma espesifye spesifikasyon.
CRLF se yon sekans de karaktè: r и n, separe premye liy demann lan, headers ak done.
<КОМАНДА> - youn nan CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Navigatè a pral voye yon lòd sou sèvè nou an GET, sa vle di "Voye m 'sa ki nan dosye a."
<URI> - idantifyan resous inifòm. Pou egzanp, si URI = /index.html, Lè sa a, kliyan an mande paj prensipal la nan sit la.
<ВЕРСИЯ HTTP> — vèsyon pwotokòl HTTP a nan fòma a HTTP/X.Y. Vèsyon ki pi souvan itilize jodi a se HTTP/1.1.
<ЗАГОЛОВОК N> se yon pè kle-valè nan fòma a <КЛЮЧ>: <ЗНАЧЕНИЕ>, voye bay sèvè a pou plis analiz.
<ДАННЫЕ> — done sèvè a mande pou fè operasyon an. Souvan li senp JSON oswa nenpòt lòt fòma.
<КОД СТАТУСА> se yon nimewo ki reprezante rezilta operasyon an. Sèvè nou an ap toujou retounen estati 200 (operasyon siksè).
<ОПИСАНИЕ СТАТУСА> — reprezantasyon kòd estati a. Pou estati kòd 200 sa a se OK.
<ЗАГОЛОВОК N> — header nan menm fòma ak nan demann lan. Nou pral retounen tit yo Content-Length (gwosè dosye) ak Content-Type: text/html (retounen kalite done).
<ДАННЫЕ> - done itilizatè a mande yo. Nan ka nou an, sa a se chemen an nan imaj la nan HTML.
dosye http_server.c (sèl sèvè threaded) gen ladann fichye common.h, ki gen pwototip fonksyon sa yo:
Montre pwototip fonksyon an komen.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);
Yo dekri tou makro fonksyonèl la SAFE_CALL() epi fonksyon an defini fail(). Makro a konpare valè ekspresyon an ak erè a, epi si kondisyon an se vre, li rele fonksyon an fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)
Fonksyon fail() enprime agiman yo pase nan tèminal la (tankou printf()) epi mete fen nan pwogram nan ak kòd la EXIT_FAILURE:
Fonksyon new_server() retounen deskriptè dosye a nan priz "sèvè" ki te kreye pa apèl sistèm yo socket(), bind() и listen() ak kapab aksepte koneksyon fèk ap rantre nan yon mòd ki pa bloke.
Remake byen ke priz la okòmansman kreye nan mòd ki pa bloke lè l sèvi avèk drapo a SOCK_NONBLOCKse konsa ke nan fonksyon an on_accept() (li plis) apèl sistèm accept() pa t sispann ekzekisyon fil la.
Si reuse_port egal true, Lè sa a, fonksyon sa a pral configured priz la ak opsyon an SO_REUSEPORT pa setsockopt()pou itilize menm pò a nan yon anviwònman milti-threaded (gade seksyon "Multi-threaded sèvè").
Evènman Handler on_accept() rele apre OS la jenere yon evènman EPOLLIN, nan ka sa a sa vle di ke nouvo koneksyon an ka aksepte. on_accept() aksepte yon nouvo koneksyon, chanje li nan mòd ki pa bloke epi anrejistre ak yon moun kap okipe evènman an on_recv() nan yon réacteurs I/O.
Evènman Handler on_recv() rele apre OS la jenere yon evènman EPOLLIN, nan ka sa a sa vle di ke koneksyon an anrejistre on_accept(), pare pou resevwa done.
on_recv() li done ki soti nan koneksyon an jiskaske demann HTTP an konplètman resevwa, Lè sa a, li anrejistre yon moun kap okipe on_send() pou voye yon repons HTTP. Si kliyan an kraze koneksyon an, priz la derejistre epi fèmen lè l sèvi avèk close().
Montre fonksyon 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);
}
}
Evènman Handler on_send() rele apre OS la jenere yon evènman EPOLLOUT, sa vle di ke koneksyon an anrejistre on_recv(), pare pou voye done. Fonksyon sa a voye yon repons HTTP ki gen HTML ak yon imaj bay kliyan an epi li chanje moun kap okipe evènman an on_recv().
Epi finalman, nan dosye a http_server.c, nan fonksyon main() nou kreye yon I/O réacteurs itilize reactor_new(), kreye yon priz sèvè epi anrejistre li, kòmanse raktor a lè l sèvi avèk reactor_run() pou egzakteman yon minit, ak Lè sa a, nou lage resous ak sòti pwogram nan.
Ann tcheke ke tout bagay ap travay jan yo espere. Konpile (chmod a+x compile.sh && ./compile.sh nan rasin pwojè a) epi lanse sèvè a ekri pwòp tèt ou, louvri http://127.0.0.1:18470 nan navigatè a epi wè sa nou te espere:
Ann mezire pèfòmans yon sèvè yon sèl-threaded. Ann louvri de tèminal: nan youn nou pral kouri ./http_server, nan yon lòt - travay. Apre yon minit, estatistik sa yo pral parèt nan dezyèm tèminal la:
Sèvè yon sèl fil nou an te kapab trete plis pase 11 milyon demann pou chak minit ki soti nan 100 koneksyon. Se pa yon move rezilta, men èske li ka amelyore?
Sèvè Multithreaded
Kòm mansyone pi wo a, yo ka kreye réacteurs I/O nan fil separe, kidonk itilize tout nwayo CPU. Ann mete apwòch sa a an pratik:
Tanpri sonje ke agiman an fonksyon new_server() defansè yo true. Sa vle di ke nou bay opsyon a nan priz sèvè a SO_REUSEPORTpou itilize li nan yon anviwònman milti-threaded. Ou ka li plis detay isit la.
Dezyèm kouri
Koulye a, kite a mezire pèfòmans nan yon sèvè milti-threaded:
Sèvi ak CPU afinite, konpilasyon ak -march=native, PGO, yon ogmantasyon nan kantite frape kachèt, ogmante MAX_EVENTS epi sèvi ak EPOLLET pa t bay yon ogmantasyon siyifikatif nan pèfòmans. Men, sa k ap pase si ou ogmante kantite koneksyon similtane?
Yo te jwenn rezilta a vle, epi avèk li yon graf enteresan ki montre depandans kantite demann trete nan 1 minit sou kantite koneksyon:
Nou wè ke apre yon koup la san koneksyon, kantite demann trete pou tou de sèvè gout sevè (nan vèsyon an milti-threaded sa a se pi plis aparan). Èske sa a gen rapò ak aplikasyon Linux TCP/IP pil? Ezite ekri sipozisyon ou sou konpòtman sa a nan graf la ak optimize pou opsyon milti-threaded ak yon sèl-threaded nan kòmantè yo.
Kòm te note nan kòmantè yo, tès pèfòmans sa a pa montre konpòtman I/O réacteurs anba chay reyèl, paske prèske toujou sèvè a reyaji ak baz done a, pwodiksyon mòso bwa, sèvi ak kriptografi ak tl elatriye, kòm yon rezilta chaj la vin pa inifòm (dinamik). Tès ansanm ak eleman twazyèm pati yo pral fèt nan atik la sou I/O proactor la.
Dezavantaj nan I/O réacteurs
Ou bezwen konprann ke I/O reyaktè a pa san dezavantaj li yo, sètadi:
Sèvi ak yon réacteurs I/O nan yon anviwonman milti-threaded yon ti jan pi difisil, paske w ap gen manyèlman jere koule yo.
Pratike montre ke nan pifò ka chay la pa inifòm, sa ki ka mennen nan yon sèl fil antre pandan yon lòt okipe ak travay.
Si yon sèl moun kap okipe evènman bloke yon fil, Lè sa a, seleksyon sistèm nan tèt li pral bloke tou, sa ki ka mennen nan pinèz difisil pou jwenn.
Rezoud pwoblèm sa yo I/O proactor, ki souvan gen yon pwogramè ki respire distribye chay la nan yon pisin nan fil, epi tou li gen yon API ki pi pratik. Nou pral pale sou li pita, nan lòt atik mwen an.
Konklizyon
Sa a se kote vwayaj nou an soti nan teyori tou dwat nan tiyo echapman an profiler rive nan yon fen.
Ou pa ta dwe rete sou sa a, paske gen anpil lòt apwòch egalman enteresan nan ekri lojisyèl rezo ak diferan nivo konvenyans ak vitès. Enteresan, nan opinyon mwen, yo bay lyen anba a.