F'dan l-artikolu, se nħarsu lejn l-iżvantaġġi ta 'reattur I/O u kif jaħdem, niktbu implimentazzjoni f'inqas minn 200 linja ta' kodiċi, u nagħmlu proċess sempliċi ta 'server HTTP fuq 40 miljun talba/min.
Daħla
L-artiklu nkiteb biex jgħin biex jifhem il-funzjonament tar-reattur I/O, u għalhekk jifhem ir-riskji meta jużah.
L-għarfien tal-baŜi huwa meħtieġ biex tifhem l-artiklu. Lingwa C u xi esperjenza fl-iżvilupp ta 'applikazzjoni tan-netwerk.
Il-kodiċi kollu huwa miktub bil-lingwa C strettament skont (attenzjoni: PDF twil) għall-istandard C11 għal Linux u disponibbli fuq GitHub.
Għaliex dan huwa meħtieġ?
Bil-popolarità dejjem tikber tal-Internet, is-servers tal-web bdew jeħtieġu li jimmaniġġjaw numru kbir ta 'konnessjonijiet fl-istess ħin, u għalhekk ġew ippruvati żewġ approċċi: imblukkar I/O fuq numru kbir ta' ħjut tal-OS u I/O li ma jimblokkax flimkien ma ' sistema ta’ notifika ta’ avveniment, imsejħa wkoll “selettur tas-sistema” (epoll/kqueue/IOCP/ eċċ).
L-ewwel approċċ kien jinvolvi l-ħolqien ta 'ħajt OS ġdid għal kull konnessjoni deħlin. L-iżvantaġġ tiegħu huwa l-iskalabbiltà fqira: is-sistema operattiva se jkollha timplimenta ħafna transizzjonijiet tal-kuntest и sejħiet tas-sistema. Huma operazzjonijiet għaljin u jistgħu jwasslu għal nuqqas ta 'RAM ħielsa b'numru impressjonanti ta' konnessjonijiet.
Il-verżjoni modifikata tenfasizza numru fiss ta 'ħjut (thread pool), u b'hekk tipprevjeni lis-sistema milli tħassar l-eżekuzzjoni, iżda fl-istess ħin tintroduċi problema ġdida: jekk thread pool bħalissa tkun imblukkata minn operazzjonijiet ta' qari fit-tul, allura sockets oħra li diġà jistgħu jirċievu data ma jkunux jistgħu tagħmel hekk.
It-tieni approċċ juża sistema ta’ notifika tal-avvenimenti (selettur tas-sistema) ipprovdut mill-OS. Dan l-artikolu jiddiskuti l-aktar tip komuni ta’ selettur tas-sistema, ibbażat fuq twissijiet (avvenimenti, notifiki) dwar it-tħejjija għal operazzjonijiet I/O, aktar milli fuq notifiki dwar it-tlestija tagħhom. Eżempju simplifikat tal-użu tiegħu jista’ jiġi rappreżentat bid-dijagramma blokk li ġejja:
Id-differenza bejn dawn l-approċċi hija kif ġej:
Imblukkar ta 'operazzjonijiet I/O jissospendi fluss tal-utent sakemmsakemm l-OS huwa kif suppost deframmenti deħlin Pakketti IP għal byte stream (TCP, tirċievi dejta) jew ma jkunx hemm biżżejjed spazju disponibbli fil-buffers tal-kitba interni biex jintbagħtu sussegwenti permezz NIC (jibgħat id-dejta).
Selettur tas-sistema maż-żmien jinnotifika lill-programm li l-OS diġà pakketti IP deframmentati (TCP, riċeviment tad-dejta) jew spazju biżżejjed fil-buffers tal-kitba interni diġà disponibbli (li tibgħat id-dejta).
Fil-qosor, ir-riżerva ta’ ħajt tal-OS għal kull I/O hija ħela ta’ qawwa tal-kompjuters, għax fir-realtà, il-ħjut mhux qed jagħmlu xogħol utli (għalhekk it-terminu "interruzzjoni tas-softwer"). Is-selettur tas-sistema jsolvi din il-problema, u jippermetti lill-programm tal-utent juża r-riżorsi tas-CPU b'mod ferm aktar ekonomiku.
Mudell tar-reattur I/O
Ir-reattur I/O jaġixxi bħala saff bejn is-selettur tas-sistema u l-kodiċi tal-utent. Il-prinċipju tat-tħaddim tiegħu huwa deskritt mid-dijagramma blokka li ġejja:
Ħa nfakkarkom li avveniment huwa notifika li ċertu socket huwa kapaċi jwettaq operazzjoni I/O li ma jimblokkax.
Maniġer tal-avvenimenti huwa funzjoni msejħa mir-reattur tal-I/O meta jiġi riċevut avveniment, li mbagħad iwettaq operazzjoni tal-I/O li ma jimblokkax.
Huwa importanti li wieħed jinnota li r-reattur I/O huwa b'definizzjoni b'ħajt wieħed, iżda m'hemm xejn li jwaqqaf il-kunċett milli jintuża f'ambjent b'ħafna kamini bi proporzjon ta 'ħajt 1: reattur 1, u b'hekk jiġu riċiklati l-qlub kollha tas-CPU.
Реализация
Se npoġġu l-interface pubblika f'fajl reactor.h, u l-implimentazzjoni - in reactor.c. reactor.h se jikkonsisti fl-avviżi li ġejjin:
Uri dikjarazzjonijiet fir-reattur.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);
L-istruttura tar-reattur I/O tikkonsisti minn deskrittur tal-fajl selettur epoll и imwejjed tal-hashGHashTable, li mapep kull socket għal CallbackData (struttura ta' handler tal-avvenimenti u argument tal-utent għalih).
Jekk jogħġbok innota li ppermettejna l-abbiltà li timmaniġġa tip mhux komplut skond l-indiċi. IN reactor.h aħna niddikjaraw l-istruttura reactor, u ġewwa reactor.c aħna niddefinixxuha, u b'hekk nipprevjenu lill-utent milli jbiddel espliċitament l-oqsma tiegħu. Din hija waħda mill-mudelli ħabi tad-data, li fil-qosor tidħol fis-semantika C.
Funzjonijiet reactor_register, reactor_deregister и reactor_reregister taġġorna l-lista tas-sokits ta 'interess u l-immaniġġjar tal-avvenimenti korrispondenti fis-selettur tas-sistema u t-tabella tal-hash.
Wara li r-reattur I/O ikun interċetta l-avveniment bid-deskrittur fd, isejjaħ lill-immaniġġjar tal-avvenimenti korrispondenti, li jgħaddi għalih fd, maskra bit avvenimenti ġġenerati u pointer tal-utent għal void.
Uri l-funzjoni 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;
}
Fil-qosor, il-katina ta 'sejħiet ta' funzjoni fil-kodiċi tal-utent se tieħu l-forma li ġejja:
Server b'kamin wieħed
Sabiex nittestjaw ir-reattur I/O taħt tagħbija għolja, aħna se niktbu server web HTTP sempliċi li jirrispondi għal kwalunkwe talba b'immaġni.
CRLF hija sekwenza ta' żewġ karattri: r и n, li tissepara l-ewwel linja tat-talba, l-intestaturi u d-dejta.
<КОМАНДА> - wieħed minn CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Il-browser se jibgħat kmand lis-server tagħna GET, li jfisser "Ibgħatli l-kontenut tal-fajl."
<КОД СТАТУСА> huwa numru li jirrappreżenta r-riżultat tal-operazzjoni. Is-server tagħna dejjem se jirritorna l-istatus 200 (operazzjoni b'suċċess).
<ОПИСАНИЕ СТАТУСА> — rappreżentazzjoni ta' string tal-kodiċi tal-istatus. Għall-kodiċi tal-istatus 200 dan huwa OK.
<ЗАГОЛОВОК N> — header tal-istess format bħal fit-talba. Se nirritornaw it-titli Content-Length (daqs tal-fajl) u Content-Type: text/html (it-tip tad-data tar-ritorn).
<ДАННЫЕ> — data mitluba mill-utent. Fil-każ tagħna, din hija t-triq għall-immaġni fil HTML.
fajl http_server.c (server b'kamin wieħed) jinkludi fajl common.h, li fih il-prototipi tal-funzjoni li ġejjin:
Uri prototipi tal-funzjoni b'mod komuni.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);
Il-makro funzjonali hija deskritta wkoll SAFE_CALL() u l-funzjoni hija definita fail(). Il-makro tqabbel il-valur tal-espressjoni mal-iżball, u jekk il-kundizzjoni hija vera, isejjaħ il-funzjoni fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)
Funzjoni fail() jistampa l-argumenti mgħoddija lit-terminal (bħal printf()) u jtemm il-programm bil-kodiċi EXIT_FAILURE:
Funzjoni new_server() jirritorna d-deskrittur tal-fajl tas-socket "server" maħluqa mis-sejħiet tas-sistema socket(), bind() и listen() u kapaċi li jaċċettaw konnessjonijiet deħlin f'mod li ma jimblokkax.
Innota li s-sokit inizjalment jinħoloq f'mod li ma jimblokka bl-użu tal-bandiera SOCK_NONBLOCKsabiex fil-funzjoni on_accept() (aqra aktar) sejħa tas-sistema accept() ma waqfitx l-eżekuzzjoni tal-ħajt.
Jekk reuse_port huwa daqs true, allura din il-funzjoni se tikkonfigura s-sokit bl-għażla SO_REUSEPORT permezz setsockopt()biex tuża l-istess port f'ambjent multi-threaded (ara t-taqsima "Multi-threaded server").
Maniġer tal-Avvenimenti on_accept() imsejħa wara li l-OS jiġġenera avveniment EPOLLIN, f'dan il-każ ifisser li l-konnessjoni l-ġdida tista' tiġi aċċettata. on_accept() jaċċetta konnessjoni ġdida, jaqilbu għall-mod li ma jimblokkax u jirreġistra ma' handler tal-avvenimenti on_recv() f'reattur I/O.
Maniġer tal-Avvenimenti on_recv() imsejħa wara li l-OS jiġġenera avveniment EPOLLIN, f'dan il-każ ifisser li l-konnessjoni rreġistrata on_accept(), lest biex jirċievi data.
on_recv() jaqra d-dejta mill-konnessjoni sakemm it-talba HTTP tiġi riċevuta kompletament, imbagħad tirreġistra handler on_send() biex tibgħat tweġiba HTTP. Jekk il-klijent jikser il-konnessjoni, is-sokit jiġi dereġistrat u jingħalaq bl-użu close().
Uri l-funzjoni 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);
}
}
Maniġer tal-Avvenimenti on_send() imsejħa wara li l-OS jiġġenera avveniment EPOLLOUT, li jfisser li l-konnessjoni rreġistrata on_recv(), lest biex jibgħat data. Din il-funzjoni tibgħat rispons HTTP li jkun fih HTML b'immaġni lill-klijent u mbagħad jibdel il-handler tal-avvenimenti lura għal on_recv().
U finalment, fil-fajl http_server.c, fil-funzjoni main() noħolqu reattur I/O bl-użu reactor_new(), Oħloq socket server u rreġistrah, ibda r-reattur bl-użu reactor_run() għal eżattament minuta, u mbagħad nirrilaxxaw ir-riżorsi u noħorġu mill-programm.
Ejja niċċekkjaw li kollox qed jaħdem kif mistenni. Kumpilazzjoni (chmod a+x compile.sh && ./compile.sh fl-għerq tal-proġett) u tniedi s-server miktub minnu nnifsu, miftuħ http://127.0.0.1:18470 fil-browser u ara dak li stennejna:
Ejja nkejlu l-prestazzjoni ta 'server b'ħajt wieħed. Ejja niftħu żewġ terminals: f'wieħed niġru ./http_server, b'mod differenti - xogħol. Wara minuta, l-istatistika li ġejja tintwera fit-tieni terminal:
Is-server single-threaded tagħna kien kapaċi jipproċessa aktar minn 11-il miljun talba kull minuta li joriġinaw minn 100 konnessjoni. Mhux riżultat ħażin, imma jista 'jitjieb?
Server multithreaded
Kif imsemmi hawn fuq, ir-reattur I/O jista 'jinħoloq f'ħjut separati, u b'hekk jutilizzaw il-qlub kollha tas-CPU. Ejja npoġġu dan l-approċċ fil-prattika:
Jekk jogħġbok innota li l-argument tal-funzjoni new_server() jippreferi true. Dan ifisser li aħna jassenjaw l-għażla lis-socket tas-server SO_REUSEPORTbiex tużah f'ambjent b'ħafna kamini. Tista' taqra aktar dettalji hawn.
It-tieni ġirja
Issa ejja nkejlu l-prestazzjoni ta 'server multi-threaded:
In-numru ta' talbiet ipproċessati f'minuta żdied b'~1 darbiet! Imma konna biss ~ 3.28 miljuni inqas min-numru tond, allura ejja nippruvaw nirranġaw dan.
L-ewwel ejja nħarsu lejn l-istatistika ġġenerata perfetta:
Bl-użu ta 'CPU Affinity, kumpilazzjoni ma -march=native, PGO, żieda fin-numru ta 'hits cache, żid MAX_EVENTS u l-użu EPOLLET ma tawx żieda sinifikanti fil-prestazzjoni. Imma x'jiġri jekk iżżid in-numru ta 'konnessjonijiet simultanji?
Inkiseb ir-riżultat mixtieq, u magħha grafika interessanti li turi d-dipendenza tan-numru ta 'talbiet ipproċessati f'minuta 1 fuq in-numru ta' konnessjonijiet:
Naraw li wara ftit mijiet ta 'konnessjonijiet, in-numru ta' talbiet ipproċessati għaż-żewġ servers jonqos drastikament (fil-verżjoni multi-threaded dan huwa aktar notevoli). Dan huwa relatat mal-implimentazzjoni tal-munzell Linux TCP/IP? Ħossok liberu li tikteb is-suppożizzjonijiet tiegħek dwar din l-imġieba tal-graff u l-ottimizzazzjonijiet għal għażliet b'ħafna kamini u b'kamin wieħed fil-kummenti.
Kif innota fil-kummenti, dan it-test tal-prestazzjoni ma jurix l-imġieba tar-reattur I/O taħt tagħbijiet reali, għax kważi dejjem is-server jinteraġixxi mad-database, joħroġ zkuk, juża kriptografija b' TLS eċċ., li b'riżultat tiegħu t-tagħbija ssir mhux uniformi (dinamika). It-testijiet flimkien ma 'komponenti ta' partijiet terzi se jitwettqu fl-artiklu dwar il-proactor I/O.
Żvantaġġi tar-reattur I/O
Trid tifhem li r-reattur I/O mhuwiex mingħajr l-iżvantaġġi tiegħu, jiġifieri:
L-użu ta 'reattur I/O f'ambjent b'ħafna kamini huwa kemmxejn aktar diffiċli, għaliex ser ikollok timmaniġġja manwalment il-flussi.
Il-prattika turi li fil-biċċa l-kbira tal-każijiet it-tagħbija mhix uniformi, li tista 'twassal għal qtugħ ta' ħajta filwaqt li ieħor ikun okkupat bix-xogħol.
Jekk wieħed jimmaniġġja l-avvenimenti jimblokka ħajta, allura s-selettur tas-sistema nnifsu jimblokka wkoll, li jista 'jwassal għal bugs diffiċli biex issibhom.
Issolvi dawn il-problemi Proattur I/O, li ħafna drabi għandu Scheduler li jqassam it-tagħbija b'mod ugwali għal ġabra ta 'ħjut, u għandu wkoll API aktar konvenjenti. Nitkellmu dwarha aktar tard, fl-artiklu l-ieħor tiegħi.
Konklużjoni
Dan huwa fejn il-vjaġġ tagħna mit-teorija dritt għall-exhaust tal-profiler wasal fi tmiemu.
M'għandekx toqgħod fuq dan, għaliex hemm ħafna approċċi oħra ugwalment interessanti għall-kitba ta 'softwer tan-netwerk b'livelli differenti ta' konvenjenza u veloċità. Interessanti, fl-opinjoni tiegħi, links huma mogħtija hawn taħt.