An dësem Artikel wäerte mir d'Ins an d'Outs vun engem I / O Reaktor kucken a wéi et funktionnéiert, eng Implementatioun a manner wéi 200 Zeilen Code schreiwen an en einfachen HTTP-Serverprozess iwwer 40 Milliounen Ufroen / min maachen.
Viruerteel
Den Artikel gouf geschriwwen fir de Fonctionnement vum I / O Reaktor ze verstoen, an dofir d'Risiken ze verstoen wann se se benotzt.
Wëssen iwwer d'Basis ass erfuerderlech fir den Artikel ze verstoen. C Sprooch an e puer Erfahrung an der Entwécklung vun der Netzwierkapplikatioun.
All Code ass an C Sprooch geschriwwen strikt no (opgepasst: laang PDF) zu C11 Norm fir Linux a verfügbar op GitHub.
Firwat ass dat néideg?
Mat der wuessender Popularitéit vum Internet hunn d'Webserver ugefaang eng grouss Zuel vu Verbindungen gläichzäiteg ze handhaben, an dofir goufen zwou Approche probéiert: I/O blockéieren op enger grousser Zuel vun OS Threads an net blockéierend I/O a Kombinatioun mat en Event Notifikatiounssystem, och "System Selector" genannt (epoll/kqueue/IOCP/etc).
Déi éischt Approche huet involvéiert en neien OS Thread fir all erakommen Verbindung ze kreéieren. Säin Nodeel ass eng schlecht Skalierbarkeet: de Betribssystem muss vill ëmsetzen Kontext Iwwergäng и System rifft. Si sinn deier Operatiounen a kënnen zu engem Mangel u gratis RAM mat enger impressionanter Unzuel u Verbindungen féieren.
Déi geännert Versioun Highlights fix Zuel vun thread (Thread Pool), doduerch verhënnert datt de System d'Ausféierung ofbriechen, awer gläichzäiteg en neie Problem aféieren: wann e thread Pool de Moment duerch laang Liesoperatioune blockéiert ass, da kënnen aner Sockets, déi scho fäeg sinn Daten ze kréien, net fäeg sinn maachen esou.
Déi zweet Approche benotzt Event Notifikatioun System (System Selector) vum OS geliwwert. Dësen Artikel diskutéiert déi allgemeng Aart vu Systemselektor, baséiert op Alarmer (Evenementer, Notifikatiounen) iwwer Bereetschaft fir I/O Operatiounen, anstatt op Notifikatiounen iwwer hir Fäerdegstellung. E vereinfacht Beispill vu senger Benotzung kann duerch de folgende Blockdiagram vertruede ginn:
Den Ënnerscheed tëscht dësen Approche ass wéi follegt:
Blockéieren I / O Operatiounen suspendéieren Benotzer Flux bisbis d'OS richteg ass defragmentéiert erakommen IP Pakete zu Byte Stream (TCP, Daten empfänken) oder et gëtt net genuch Plaz an den internen Schreifbuffer verfügbar fir spéider ze schécken via NIC (Daten schécken).
System selector am Zäitoflaf informéiert de Programm datt d'OS schonn defragmentéiert IP Pakete (TCP, Dateempfang) oder genuch Plaz an intern Schreifbuffer schonn verfügbar (Daten schécken).
Fir et ze resuméieren, en OS Thread fir all I/O ze reservéieren ass e Verschwendung vu Rechenkraaft, well a Wierklechkeet maachen d'Threads keng nëtzlech Aarbecht (dofir de Begrëff "Software Ënnerbriechung"). De Systemselektor léist dëse Problem, wat de Benotzerprogramm erlaabt CPU Ressourcen vill méi wirtschaftlech ze benotzen.
I/O Reaktormodell
Den I/O Reaktor handelt als Schicht tëscht dem Systemselektor an dem Benotzercode. De Prinzip vu senger Operatioun gëtt duerch de folgende Blockdiagramm beschriwwen:
Loosst mech Iech drun erënneren datt en Event eng Notifikatioun ass datt e bestëmmte Socket fäeg ass eng net blockéierend I/O Operatioun auszeféieren.
En Event Handler ass eng Funktioun déi vum I/O Reaktor genannt gëtt wann en Event kritt gëtt, deen dann eng net blockéierend I/O Operatioun ausféiert.
Et ass wichteg ze bemierken datt den I / O Reaktor per Definitioun Single-threaded ass, awer et gëtt näischt verhënnert datt d'Konzept an engem Multi-threaded Ëmfeld benotzt gëtt an engem Verhältnis vun 1 thread: 1 Reaktor, an doduerch all CPU Cores recycléiert.
Ëmsetzung
Mir setzen den ëffentlechen Interface an engem Fichier reactor.h, an Ëmsetzung - an reactor.c. reactor.h besteet aus folgenden Ukënnegungen:
Show Deklaratioune an 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);
D'I/O Reaktorstruktur besteet aus Dateibeschreiwung selector epoll и hash DëscherGHashTable, déi all Socket Kaarten ze CallbackData (Struktur vun engem Event Handler an engem Benotzer Argument dofir).
Maacht weg datt mir d'Fäegkeet aktivéiert hunn ze handhaben onkomplett Typ no dem Index. IN reactor.h mir erklären d'Struktur reactoran a reactor.c mir definéieren et, doduerch datt de Benotzer seng Felder explizit verännert. Dëst ass ee vun de Musteren verstoppt Donnéeën, déi präzis an d'C Semantik passt.
Functions reactor_register, reactor_deregister и reactor_reregister update d'Lëscht vun Sockets vun Interessi an entspriechend Event Handler am System selector an hash Dësch.
Nodeems den I/O Reaktor d'Evenement mam Deskriptor ofgefaangen huet fd, et rifft de entspriechende Event Handler, un déi et passéiert fd, bëssen Mask generéiert Evenementer an e Benotzer Pointer op void.
Show reactor_run () Funktioun
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;
}
Fir ze resuméieren, wäert d'Kette vu Funktiounsruffen am Benotzercode déi folgend Form huelen:
Single threaded Server
Fir den I/O Reaktor ënner héijer Belaaschtung ze testen, schreiwen mir en einfachen HTTP Webserver deen op all Ufro mat engem Bild reagéiert.
Eng séier Referenz op den HTTP-Protokoll
HTTP - dëst ass de Protokoll Applikatioun Niveau, haaptsächlech fir Server-Browser Interaktioun benotzt.
HTTP kann einfach iwwer benotzt ginn Transport Protokoll TCP, Messagen an engem spezifizéierte Format schécken a kréien Spezifizéierung.
CRLF ass eng Sequenz vun zwee Zeechen: r и n, Trennt déi éischt Zeil vun der Ufro, Header an Daten.
<КОМАНДА> - ee vun CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. De Browser schéckt e Kommando un eise Server GET, dat heescht "Schéckt mir den Inhalt vum Fichier."
<КОД СТАТУСА> ass eng Zuel déi d'Resultat vun der Operatioun representéiert. Eise Server gëtt ëmmer Status 200 zréck (erfollegräich Operatioun).
<ОПИСАНИЕ СТАТУСА> - String Representatioun vum Statuscode. Fir Status Code 200 dëst ass OK.
<ЗАГОЛОВОК N> - Header vum selwechte Format wéi an der Ufro. Mir ginn d'Titelen zréck Content-Length (Dateigréisst) an Content-Type: text/html (zréck Datentyp).
<ДАННЫЕ> - Daten gefrot vum Benotzer. An eisem Fall ass dëst de Wee zum Bild an HTML.
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов принять новое соединение.
*/
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);
De funktionnelle Makro gëtt och beschriwwen SAFE_CALL() an d'Funktioun ass definéiert fail(). De Makro vergläicht de Wäert vum Ausdrock mam Feeler, a wann d'Konditioun richteg ass, rifft d'Funktioun fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)
Funktioun fail() dréckt déi passéiert Argumenter op den Terminal (wéi printf()) a schléisst de Programm mam Code of EXIT_FAILURE:
Funktioun new_server() gëtt de Dateideskriptor vum "Server" Socket zréck, deen duerch Systemriff erstallt gëtt socket(), bind() и listen() a kapabel erakommen Verbindungen an engem net-blockéierende Modus ze akzeptéieren.
Bedenkt datt de Socket am Ufank am Net-Blockéierungsmodus erstallt gëtt mam Fändel SOCK_NONBLOCKsou datt an der Funktioun on_accept() (liest méi) System Opruff accept() huet d'Thread Ausféierung net gestoppt.
wann reuse_port ass gläich true, da wäert dës Funktioun de Socket mat der Optioun konfiguréieren SO_REUSEPORT duerch setsockopt()déi selwecht port an engem Multi-threaded Ëmfeld ze benotzen (kuckt Rubrik "Multi-threaded Server").
Event Handler on_accept() genannt nodeems d'OS en Event generéiert EPOLLIN, an dësem Fall bedeit datt déi nei Verbindung akzeptéiert ka ginn. on_accept() akzeptéiert eng nei Verbindung, wiesselt et op net-blockéierend Modus a registréiert mat engem Eventhandler on_recv() an engem I/O Reaktor.
Event Handler on_recv() genannt nodeems d'OS en Event generéiert EPOLLIN, an dësem Fall bedeit datt d'Verbindung ugemellt ass on_accept(), prett fir Daten ze kréien.
on_recv() liest Daten aus der Verbindung bis d'HTTP-Ufro komplett kritt ass, da registréiert en Handler on_send() eng HTTP Äntwert ze schécken. Wann de Client d'Verbindung brécht, gëtt de Socket deregistréiert an zougemaach close().
Show Funktioun 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);
}
}
Event Handler on_send() genannt nodeems d'OS en Event generéiert EPOLLOUT, Bedeitung, datt d'Verbindung registréiert on_recv(), prett Daten ze schécken. Dës Funktioun schéckt eng HTTP Äntwert mat HTML mat engem Bild un de Client an ännert dann den Event Handler zréck op on_recv().
A schlussendlech am Dossier http_server.c, an der Funktioun main() mir schafen eng ech / O Reakter benotzt reactor_new(), erstellt e Server Socket a registréiert et, start de Reakter mat reactor_run() fir genee eng Minutt, an dann Fräisetzung mir Ressourcen an Sortie de Programm.
Loosst eis kucken ob alles funktionnéiert wéi erwaart. Zesummesetzung (chmod a+x compile.sh && ./compile.sh am Projet root) a starten de selbstgeschriwwe Server, oppen http://127.0.0.1:18470 am Browser a kuckt wat mir erwaart hunn:
Loosst eis d'Performance vun engem Single-threaded Server moossen. Loosst eis zwee Terminaler opmaachen: an engem lafe mir ./http_server, an enger anerer - wrk. No enger Minutt ginn déi folgend Statistiken am zweeten Terminal ugewisen:
Eise Single-threaded Server konnt iwwer 11 Milliounen Ufroe pro Minutt veraarbecht, déi aus 100 Verbindungen stamen. Net e schlecht Resultat, awer kann et verbessert ginn?
Multithreaded Server
Wéi uewen erwähnt, kann den I/O Reaktor a getrennten Threads erstallt ginn, an doduerch all CPU Cores benotzen. Loosst eis dës Approche an d'Praxis ëmsetzen:
Maacht weg datt d'Funktioun Argument new_server() gitt true. Dëst bedeit datt mir d'Optioun un de Server Socket zouginn SO_REUSEPORTet an engem Multi-threaded Ëmfeld ze benotzen. Dir kënnt méi Detailer liesen hei.
Zweet Laf
Loosst eis elo d'Performance vun engem Multi-threaded Server moossen:
D'Zuel vun den Ufroen, déi an 1 Minutt veraarbecht ginn ass ëm ~3.28 Mol eropgaang! Awer mir waren nëmmen ~ XNUMX Millioune knapp vun der Ronn Zuel, also loosst eis probéieren dat ze fixéieren.
Als éischt kucke mer d'Statistiken déi generéiert ginn perfekt:
Benotzt CPU Affinitéit, Zesummesetzung mat -march=native, PGO, eng Erhéijung vun der Zuel vun Hits cache, Erhéijung MAX_EVENTS a benotzen EPOLLET huet keng bedeitend Erhéijung vun der Leeschtung ginn. Awer wat geschitt wann Dir d'Zuel vun de simultane Verbindungen erhéicht?
Dat gewënschte Resultat gouf kritt, an domat eng interessant Grafik, déi d'Ofhängegkeet vun der Unzuel vun de veraarbechten Ufroen an 1 Minutt vun der Unzuel vun de Verbindungen weist:
Mir gesinn datt no e puer honnert Verbindungen d'Zuel vun de veraarbechten Ufroe fir béid Server staark erofgeet (an der Multi-threaded Versioun ass dëst méi bemierkbar). Ass dëst Zesummenhang mat der Linux TCP / IP Stack Implementatioun? Fillt Iech gratis Är Viraussetzungen iwwer dëst Verhalen vun der Grafik an Optimisatiounen fir Multi-threaded an Single-threaded Optiounen an de Kommentaren ze schreiwen.
wéi bemierkt an de Kommentaren weist dësen Performance Test net d'Behuele vum I/O Reaktor ënner reale Lasten, well bal ëmmer de Server mat der Datebank interagéiert, Logbicher ausgëtt, benotzt Kryptografie mat TLS etc., als Resultat vun deem d'Laascht net eenheetlech gëtt (dynamesch). Tester zesumme mat Drëtt-Partei Komponente ginn am Artikel iwwer den I/O Proactor duerchgefouert.
Nodeeler vun ech / O Reakter
Dir musst verstoen datt den I/O Reaktor net ouni seng Nodeeler ass, nämlech:
En I/O Reaktor an engem Multi-threaded Ëmfeld ze benotzen ass e bësse méi schwéier, well Dir musst d'Flëss manuell managen.
D'Praxis weist datt d'Laascht an de meeschte Fäll net eenheetlech ass, wat zu engem Fuedemprotokoll kann féieren, während en aneren mat der Aarbecht beschäftegt ass.
Wann een Event-Handler e Fuedem blockéiert, blockéiert de Systemselektor selwer och, wat zu schwéier fonnte Bugs féiere kann.
Léist dës Problemer I/O Proaktor, déi dacks e Scheduler huet, deen d'Laascht gläichméisseg op e Pool vu Threads verdeelt, an och e méi prakteschen API huet. Mir wäerte méi spéit doriwwer schwätzen, a mengem aneren Artikel.
Konklusioun
Dëst ass wou eis Rees vun der Theorie direkt an de Profiler Auspuff op en Enn komm ass.
Dir sollt net op dëst wunnen, well et vill aner gläich interessant Approche fir Netzwierksoftware mat verschiddene Komfort a Geschwindegkeet ze schreiwen. Interessant, menger Meenung no, Linken ginn ënnendrënner.