I/O nga reaktor (usa ka sinulid loop sa panghitabo) maoy usa ka sumbanan sa pagsulat sa high-load software, nga gigamit sa daghang popular nga mga solusyon:
Niini nga artikulo, atong tan-awon ang ins and outs sa usa ka I/O reactor ug kung giunsa kini pagtrabaho, pagsulat sa usa ka pagpatuman sa ubos sa 200 ka linya sa code, ug paghimo sa usa ka yano nga proseso sa HTTP server nga labaw sa 40 ka milyon nga mga hangyo/min.
Pasiuna
Gisulat ang artikulo aron matabangan nga masabtan ang paggana sa I/O reactor, ug busa masabtan ang mga peligro kung gamiton kini.
Ang kahibalo sa mga sukaranan gikinahanglan aron masabtan ang artikulo. C nga pinulongan ug pipila ka kasinatian sa pagpalambo sa aplikasyon sa network.
Ang tanan nga kodigo gisulat sa C nga pinulongan nga estrikto sumala sa (pasidaan: taas nga PDF) sa C11 standard alang sa Linux ug magamit sa GitHub.
Nganong gikinahanglan kini?
Uban sa nagkadako nga pagkapopular sa Internet, ang mga web server nagsugod sa panginahanglan sa pagdumala sa usa ka dako nga gidaghanon sa mga koneksyon nga dungan, ug busa duha ka mga pamaagi ang gisulayan: pag-block sa I/O sa usa ka dako nga gidaghanon sa mga OS thread ug non-blocking I/O inubanan sa usa ka sistema sa pagpahibalo sa panghitabo, gitawag usab nga "system selector" (epoll/kqueue/IOCP/etc).
Ang una nga pamaagi naglakip sa paghimo og bag-ong OS thread alang sa matag umaabot nga koneksyon. Ang disbentaha niini mao ang dili maayo nga scalability: ang operating system kinahanglan nga ipatuman ang daghan mga transisyon sa konteksto и sistema nga tawag. Kini mga mahal nga operasyon ug mahimong mosangpot sa kakulang sa libre nga RAM nga adunay impresibo nga gidaghanon sa mga koneksyon.
Ang giusab nga bersyon nagpasiugda pirmi nga gidaghanon sa mga hilo (thread pool), sa ingon nagpugong sa sistema sa pag-abort sa pagpatuman, apan sa samang higayon nagpaila sa usa ka bag-ong problema: kung ang usa ka thread pool sa pagkakaron gibabagan sa dugay nga pagbasa nga mga operasyon, nan ang ubang mga socket nga nakadawat na og datos dili na makahimo buhata kana.
Ang ikaduha nga pamaagi gigamit sistema sa pagpahibalo sa panghitabo (system selector) nga gihatag sa OS. Kini nga artikulo naghisgot sa labing komon nga matang sa tigpili sa sistema, base sa mga alerto (mga panghitabo, mga pahibalo) mahitungod sa pagkaandam alang sa mga operasyon sa I/O, imbes sa mga pahibalo bahin sa ilang pagkompleto. Ang usa ka gipayano nga pananglitan sa paggamit niini mahimong irepresentar sa mosunod nga block diagram:
Ang kalainan tali niini nga mga pamaagi mao ang mosunod:
Pag-block sa mga operasyon sa I/O suspensohon dagan sa tiggamit hangtodhangtod sa husto ang OS mga defragment umaabot Mga pakete sa IP sa byte stream (TCP, pagdawat sa datos) o walay igo nga luna nga magamit sa internal nga pagsulat buffers alang sa sunod nga pagpadala pinaagi sa NIC (pagpadala sa datos).
Tigpili sa sistema sa paglabay sa panahon nagpahibalo sa programa nga ang OS na defragmented IP packets (TCP, data reception) o igo nga luna sa internal write buffers na magamit (pagpadala data).
Sa pagsumada niini, ang pagreserba sa usa ka OS nga thread alang sa matag I/O usa ka pag-usik sa gahum sa pag-compute, tungod kay sa pagkatinuod, ang mga hilo wala nagabuhat sa mapuslanon nga trabaho (kini diin ang termino gikan sa "pagputol sa software"). Gisulbad sa tigpili sa sistema kini nga problema, nga gitugotan ang programa sa gumagamit nga magamit ang mga kapanguhaan sa CPU nga labi ka ekonomikanhon.
I/O reactor nga modelo
Ang I/O reactor naglihok isip layer tali sa system selector ug sa user code. Ang prinsipyo sa operasyon niini gihulagway sa mosunod nga block diagram:
Pahinumdumi ko nimo nga ang usa ka panghitabo usa ka pahibalo nga ang usa ka socket makahimo sa usa ka non-blocking nga operasyon sa I/O.
Ang event handler usa ka function nga gitawag sa I/O reactor kung ang usa ka event madawat, nga unya mohimo ug non-blocking I/O operation.
Mahinungdanon nga hinumdoman nga ang I / O reactor pinaagi sa kahulugan nga single-threaded, apan wala’y makapugong sa konsepto nga magamit sa usa ka multi-threaded nga palibot sa ratio nga 1 thread: 1 reactor, sa ingon gi-recycle ang tanan nga mga cores sa CPU.
Pagpatuman
Atong ibutang ang public interface sa usa ka file reactor.h, ug pagpatuman - sa reactor.c. reactor.h maglangkob sa mosunod nga mga pahibalo:
Ipakita ang mga deklarasyon sa 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);
Ang I/O reactor structure naglangkob sa deskriptor sa file tigpili epoll и hash nga mga lamesaGHashTable, nga nag-mapa sa matag socket sa CallbackData (istruktura sa usa ka tigdumala sa panghitabo ug usa ka argumento sa tiggamit alang niini).
Palihug timan-i nga kami nakahimo sa abilidad sa pagdumala dili kompleto nga tipo sumala sa indeks. SA reactor.h gipahayag namon ang istruktura reactorug sa reactor.c gihubit namo kini, sa ingon nagpugong sa tiggamit sa dayag nga pagbag-o sa mga natad niini. Kini usa sa mga pattern pagtago sa datos, nga haom kaayo sa C semantics.
Mga katuyoan reactor_register, reactor_deregister и reactor_reregister i-update ang lista sa mga socket sa interes ug katugbang nga mga tigdumala sa panghitabo sa system selector ug hash table.
Human ma-intercept sa I/O reactor ang panghitabo gamit ang descriptor fd, kini nagtawag sa katugbang nga tigdumala sa panghitabo, diin kini moagi fd, gamay nga maskara namugna nga mga panghitabo ug usa ka pointer sa user sa void.
Ipakita ang function sa 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;
}
Sa pag-summarize, ang kadena sa mga tawag sa function sa user code magkuha sa mosunod nga porma:
Usa ka thread nga server
Aron masulayan ang I/O reactor ubos sa taas nga load, magsulat kami og yano nga HTTP web server nga motubag sa bisan unsang hangyo gamit ang imahe.
Usa ka dali nga pakisayran sa HTTP protocol
http - kini ang protocol lebel sa aplikasyon, panguna nga gigamit alang sa interaksyon sa server-browser.
Ang HTTP dali nga magamit transportasyon protokol TCP, pagpadala ug pagdawat sa mga mensahe sa usa ka porma nga gitakda espesipikasyon.
CRLF usa ka han-ay sa duha ka karakter: r и n, nga nagbulag sa unang linya sa hangyo, mga ulohan ug datos.
<КОМАНДА> - usa sa CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Ang browser magpadala usa ka mando sa among server GET, nagpasabut nga "Ipadala kanako ang mga sulod sa file."
<URI> - uniporme nga resource identifier. Pananglitan, kung URI = /index.html, unya gihangyo sa kliyente ang panguna nga panid sa site.
<ВЕРСИЯ HTTP> — bersyon sa HTTP protocol sa format HTTP/X.Y. Ang labing kasagarang gigamit nga bersyon karon mao ang HTTP/1.1.
<ЗАГОЛОВОК N> kay usa ka key-value pares sa format <КЛЮЧ>: <ЗНАЧЕНИЕ>, gipadala sa server para sa dugang nga pagtuki.
<ДАННЫЕ> - datos nga gikinahanglan sa server aron mahimo ang operasyon. Kasagaran kini yano JSON o bisan unsang lain nga pormat.
<КОД СТАТУСА> mao ang numero nga nagrepresentar sa resulta sa operasyon. Ang among server kanunay nga mobalik sa status 200 (malampuson nga operasyon).
<ОПИСАНИЕ СТАТУСА> — string nga representasyon sa status code. Para sa status code 200 kini OK.
<ЗАГОЛОВОК N> — header sa parehas nga format sama sa gihangyo. Atong ibalik ang mga titulo Content-Length (gidak-on sa file) ug Content-Type: text/html (ibalik ang tipo sa datos).
<ДАННЫЕ> - data nga gihangyo sa user. Sa among kaso, kini ang agianan sa imahe sa HTML.
file http_server.c (single threaded server) naglakip sa file common.h, nga naglangkob sa mosunod nga function prototypes:
Ipakita ang mga prototype sa function nga parehas.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);
Gihulagway usab ang functional macro SAFE_CALL() ug ang function gihubit fail(). Gitandi sa macro ang kantidad sa ekspresyon sa sayup, ug kung tinuod ang kondisyon, tawgon ang function fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)
function fail() nag-imprinta sa gipasa nga mga argumento sa terminal (sama sa printf()) ug tapuson ang programa gamit ang code EXIT_FAILURE:
function new_server() ibalik ang file descriptor sa "server" socket nga gihimo sa mga tawag sa sistema socket(), bind() и listen() ug makahimo sa pagdawat sa umaabot nga mga koneksyon sa usa ka non-blocking mode.
Timan-i nga ang socket sa sinugdan gihimo sa non-blocking mode gamit ang bandila SOCK_NONBLOCKaron sa function on_accept() (basaha ang dugang) tawag sa sistema accept() wala mihunong sa pagpatuman sa thread.
kon reuse_port parehas sa true, unya kini nga function mag-configure sa socket nga adunay kapilian SO_REUSEPORT pinaagi sa setsockopt()sa paggamit sa samang pantalan sa usa ka multi-threaded nga palibot (tan-awa ang seksyon nga "Multi-threaded server").
Handler sa Panghitabo on_accept() gitawag human ang OS makamugna og panghitabo EPOLLIN, sa kini nga kaso nagpasabut nga ang bag-ong koneksyon mahimong madawat. on_accept() modawat ug bag-ong koneksyon, mobalhin niini ngadto sa non-blocking mode ug magparehistro sa usa ka event handler on_recv() sa usa ka I/O reactor.
Handler sa Panghitabo on_recv() gitawag human ang OS makamugna og panghitabo EPOLLIN, sa kini nga kaso nagpasabut nga ang koneksyon narehistro on_accept(), andam sa pagdawat sa datos.
on_recv() nagbasa sa datos gikan sa koneksyon hangtud nga ang HTTP nga hangyo hingpit nga madawat, unya kini nagparehistro sa usa ka handler on_send() aron magpadala usa ka tubag sa HTTP. Kung ang kliyente maguba ang koneksyon, ang socket matangtang sa rehistro ug sirado ang paggamit close().
Ipakita ang function 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);
}
}
Handler sa Panghitabo on_send() gitawag human ang OS makamugna og panghitabo EPOLLOUT, nagpasabot nga ang koneksyon narehistro on_recv(), andam sa pagpadala sa datos. Ang kini nga function nagpadala usa ka tubag sa HTTP nga adunay HTML nga adunay usa ka imahe sa kliyente ug dayon giusab ang tigdumala sa panghitabo balik on_recv().
Ug sa katapusan, sa file http_server.c, sa pag-obra main() naghimo kita ug I/O reactor gamit reactor_new(), paghimo ug server socket ug irehistro kini, sugdi ang reactor gamit reactor_run() sa eksaktong usa ka minuto, ug dayon buhian namo ang mga kapanguhaan ug mogawas sa programa.
Atong susihon nga ang tanan nagtrabaho sama sa gipaabut. Pag-compile (chmod a+x compile.sh && ./compile.sh sa gamut sa proyekto) ug ilunsad ang self-written server, bukas http://127.0.0.1:18470 sa browser ug tan-awa kung unsa ang among gipaabut:
Atong sukdon ang performance sa usa ka single-threaded server. Atong ablihan ang duha ka mga terminal: sa usa kita modagan ./http_server, sa lahi nga - wrk. Human sa usa ka minuto, ang mosunod nga estadistika ipakita sa ikaduhang terminal:
Ang among single-threaded server nakahimo sa pagproseso sa kapin sa 11 ka milyon nga hangyo kada minuto gikan sa 100 ka koneksyon. Dili usa ka daotan nga resulta, apan mahimo ba kini nga mapauswag?
Multithreaded nga server
Sama sa nahisgutan sa ibabaw, ang I/O reactor mahimong mabuhat sa lainlain nga mga hilo, sa ingon magamit ang tanan nga mga core sa CPU. Atong ibutang kini nga pamaagi sa praktis:
Palihug timan-i nga ang function argument new_server() pabor true. Nagpasabot kini nga gi-assign namo ang opsyon sa socket sa server SO_REUSEPORTsa paggamit niini sa usa ka multi-threaded palibot. Makabasa ka ug dugang detalye dinhi.
Ikaduhang dagan
Karon atong sukdon ang performance sa usa ka multi-threaded server:
Ang gidaghanon sa mga hangyo nga giproseso sulod sa 1 ka minuto misaka ug ~3.28 ka beses! Apan kulang ra kami sa ~XNUMX milyon sa round number, busa sulayan naton nga ayohon kana.
Una atong tan-awon ang mga estadistika nga nahimo perf:
Paggamit sa CPU Affinity, compilation uban sa -march=native, PGO, pagdugang sa gidaghanon sa mga hit cache, pagdugang MAX_EVENTS ug gamit EPOLLET wala maghatag ug dakong pagsaka sa performance. Apan unsa ang mahitabo kung imong dugangan ang gidaghanon sa dungan nga mga koneksyon?
Nakuha ang gitinguha nga resulta, ug uban niini ang usa ka makapaikag nga graph nga nagpakita sa pagsalig sa gidaghanon sa giproseso nga mga hangyo sa 1 minuto sa gidaghanon sa mga koneksyon:
Nakita namon nga pagkahuman sa usa ka gatos nga mga koneksyon, ang gidaghanon sa giproseso nga mga hangyo alang sa duha nga mga server mikunhod pag-ayo (sa multi-threaded nga bersyon kini mas mamatikdan). May kalabotan ba kini sa pagpatuman sa Linux TCP/IP stack? Mobati nga gawasnon sa pagsulat sa imong mga pangagpas mahitungod niini nga kinaiya sa graph ug mga pag-optimize alang sa multi-threaded ug single-threaded nga mga kapilian sa mga komento.
Sa unsang paagi nga namatikdan sa mga komentaryo, kini nga performance test wala magpakita sa kinaiya sa I/O reactor ubos sa tinuod nga mga load, tungod kay halos kanunay ang server nakig-interact sa database, output logs, naggamit sa cryptography nga adunay TLS ug uban pa, ingon usa ka sangputanan diin ang karga mahimong dili managsama (dinamikong). Ang mga pagsulay kauban ang mga sangkap sa ikatulo nga partido himuon sa artikulo bahin sa I/O proactor.
Mga disbentaha sa I/O reactor
Kinahanglan nimong masabtan nga ang I/O reactor kay walay mga kakulian, nga mao:
Ang paggamit sa usa ka I/O reactor sa usa ka multi-threaded nga palibot medyo mas lisud, tungod kay kinahanglan nimo nga mano-mano ang pagdumala sa mga dagan.
Gipakita sa praktis nga sa kadaghanan nga mga kaso ang load dili uniporme, nga mahimong mosangpot sa usa ka thread logging samtang ang lain busy sa trabaho.
Kung ang usa ka event handler mag-block sa usa ka thread, ang system selector mismo mo-block usab, nga mahimong mosangpot sa lisud nga pagpangita sa mga bug.
Pagsulbad niini nga mga problema I/O proactor, nga sa kasagaran adunay usa ka scheduler nga parehas nga nag-apod-apod sa load sa usa ka pool sa mga hilo, ug usab adunay usa ka mas kombenyente nga API. Atong hisgotan kini sa ulahi, sa akong laing artikulo.
konklusyon
Dinhi natapos ang among panaw gikan sa teorya diretso sa tambutso sa profiler.
Dili nimo kini kinahanglan nga hunahunaon, tungod kay adunay daghang uban pang parehas nga makapaikag nga mga pamaagi sa pagsulat sa software sa network nga adunay lainlaing lebel sa kasayon ug katulin. Makapainteres, sa akong opinyon, ang mga link gihatag sa ubos.