
Bevezetés
(egyszálú ) — ez egy mintázat nagy terhelésű szoftverek írására, amelyet számos népszerű megoldásban használnak:
- …
A cikk célja, hogy bemutassa az I/O reaktor működését, megvalósítunk egy implementációt kevesebb mint 200 sorban, és lehetővé tesszük, hogy egy egyszerű HTTP szerver több mint 40 millió kérést dolgozzon fel percenként.
Bevezető
- A cikket azzal a céllal írtuk, hogy segítsen megérteni az I/O reaktor működését és ezzel együtt tudatosítani a használatával kapcsolatos kockázatokat.
- A cikk elsajátításához alapfokú tudásra van szükség és némi hálózati alkalmazásfejlesztési tapasztalatra.
- Minden kód C nyelven íródott szigorúan a (vigyázat: hosszú PDF) Linuxra, és elérhető a .
Miért van szükség erre?
Az internet népszerűsödésével a webszervereknek meg kellett oldaniuk a nagyszámú egyidejű kapcsolat kezelését, emiatt két megközelítést próbáltak ki: blokkoló I/O-t sok OS szállítószálon, és nem blokkoló I/O-t eseményértesítési rendszerrel kombinálva, amit "rendszerválasztónak" neveznek (///etc).
Az első megközelítés új OS szálat hozott létre minden bejövő kapcsolat esetén. Ennek hátránya a rossz skálázhatóság: a operációs rendszernek számos és kell végrehajtania. Ezek költséges műveletek, és a kapcsolataink magas számához kellhet elegendő szabad RAM.
A módosított változat rögzített számú szálat A második megközelítés eseményértesítési rendszert
(rendszerválasztó) használ, amit az operációs rendszer biztosít. Ebben a cikkben a leggyakrabban előforduló eseményértesítési rendszert vizsgáljuk, amely az I/O műveletekhez való érés értesítéseken (események, értesítések) alapul, nem pedig az azok befejezéséről szóló értesítéseken. A két megközelítés közötti különbség a következő: felfüggesztik

a felhasználói szálat.
- a felhasználói szálat. blokkoló I/O műveletek felhasználói szálat. Ez a támogatási kultúra hiánya, együtt a „törjük össze, hogy szebbé tegyük” elvvel, eltávolítja a fejlesztőket tőlük., amíg az operációs rendszer megfelelően a beérkező byte-áramokba (, adatátvitel) vagy amíg nem szabadul fel elegendő hely a belső cache-ben a következő (adatküldés).
- A rendszerlegedő egy kis idő elteltével értesíti a programot arról, hogy az operációs rendszer már defragmentálta az IP csomagokat (TCP, adatátvitel), vagy elegendő hely áll rendelkezésre a belső cache-ben már az adatküldéshez.
Összegzésül, az operációs rendszer áramának fenntartása minden I/O-hoz értelmetlen számítási teljesítménypazarlás, hiszen valójában az áramok nem végeznek hasznos munkát (innen ered a ). A rendszerlegedő megoldja ezt a problémát, lehetővé téve a felhasználói program számára, hogy a CPU erőforrásait sokkal hatékonyabban hasznosítsa.
Az I/O reaktor modell
Az I/O reaktor a rendszerlegedő és a felhasználói kód közötti köztes rétegként működik. A működési elvét az alábbi blokkdiagram írja le:

- Emlékeztetem, hogy az esemény egy értesítés arról, hogy egy bizonyos socket készen áll egy nem blokkoló I/O művelet végrehajtására.
- Az eseménykezelő egy olyan függvény, amelyet az I/O reaktor hív meg esemény megérkezésekor, és amely ezután végrehajt egy nem blokkoló I/O műveletet.
Fontos megjegyezni, hogy az I/O reaktor alapértelmezés szerint egy szálú, de semmi sem akadályozza meg a fogalom használatát többszálú környezetben, 1 szál: 1 reaktor arányban, így kihasználva az összes CPU magot.
Megvalósítás
A nyilvános interfészt az fájlba helyezzük, a megvalósítást pedig a . reactor.h a következő deklarációkból fog állni:
Deklarációk megjelenítése a reactor.h-ban
typedef struct reactor Reactor;
/*
* A függvény mutató, amelyet az I/O reaktor hív meg, amikor esemény érkezik
* a rendszerszelvényből.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* NULL-t ad vissza hibás esetben, nem-NULL mutatót a `Reactor`-ra
* egyébként.
*/
Reactor *reactor_new(void);
/*
* Felszabadítja a rendszerszelvényt, minden éppen regisztrált socketet
* és magát az I/O reaktort.
*
* A következő függvények -1-et térnek vissza hibás esetben, 0-t a siker esetén.
*/
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);
/*
* Elindít egy eseményciklust a `timeout` időtartammal.
*
* Ez a függvény átadja a vezérlést a hívó kódnak, ha az allokált idő letelt
* vagy ha nincsenek regisztrált socketek.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Az I/O reaktor struktúrája a következőkből áll szelvény és , amely minden socketet párosít a CallbackData (az eseménykezelő és a felhasználói argumentumot tartalmazó struktúra).
Mutasd a Reactor-t és a CallbackData-t
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Kérjük, vegye figyelembe, hogy lehetőséget adtunk nem teljes típusú * mutatókkal való bánásmódra. A * struktúra bejelentésével * a `reactor`-t, definiálva ezzel megakadályozzuk, hogy a felhasználó közvetlenül módosítsa a mezőit. Ez az adatok elrejtésének egyik mintája, * amely szervesen illeszkedik a C nyelv szematájába. ahol reactor.h a struktúra deklarálva van reactor, míg a reactor.c definiálásával megakadályozza, hogy a felhasználó közvetlenül megváltoztassa a mezőit. Ez egyike az adatok elrejtésének mintáinak, amely * összhangban van a C nyelv szematájával. , amely szervesen illeszkedik a C nyelv szematájába.
Funkciók reactor_register, reactor_deregister és reactor_reregister frissítik az érdeklődő socketek listáját és a megfelelő eseménykezelőket a rendszerszelvényben és a hash-táblázatban.
Mutasd be a regisztráló funkciókat
#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;
}Miután az I/O reaktor észlelt egy eseményt a deskriptorral, fda megfelelő eseménykezelőt hívja, amely megkapja fd, Mutasd be a reactor_run() függvényt 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) {
// Hiba
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Idő lejárt
case 0:
result = 0;
goto cleanup;
// Sikeres művelet
default:
// Eseménykezelők hívása
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Összegezve, a felhasználói kódban a függvényhívások lánca a következőképpen fog kinézni:

Egyszálú szerver
Az I/O reaktor magas terhelés alatt történő teszteléséhez egyszerű HTTP webszervert írunk, amely bármilyen kérésre képet válaszol.
Rövid áttekintés az HTTP protokollról
— ez egy protokoll , amely elsősorban a szerver és a böngésző közötti interakcióra használatos.
Az HTTP-t könnyen lehet használni protocol , üzeneteket küldve és fogadva a megadott formátumban .
Kérési formátum
CRLF
CRLF
CRLF
CRLF CRLF
CRLF— ez a két karakterből álló sorozat:résn, amely elválasztja a kérdés első sorát, a fejlécet és az adatokat.<КОМАНДА>— az egyikCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST http://10.132.1.202:8181/restconf/config/network-topology:network-topology/topology/topology-netconf/node/jun01/yang-ext:mount/junos-conf-root:configuration/junos-conf-protocols:protocols,PUT,TRACE.A böngésző a szerverünknek küldeni fogja a parancsotGET, ami azt jelenti, hogy "Kérlek küldd el a fájl tartalmát".<URI>— . Például, ha az URI =/index.html, akkor az ügyfél a webhely főoldalát kéri.<ВЕРСИЯ HTTP>— a HTTP protokoll verziója a következő formátumbanHTTP/X.Y.A leggyakrabban használt verzió napjainkban —HTTP/1.1.— ez egy kulcs-érték pár a következő formátumban:, amelyet a szervernek küldenek további elemzés céljából.— az adat, amelyre a szervernek szüksége van a művelet végrehajtásához. Gyakran ez egyszerűen vagy bármely más formátum.
Válasz formátuma
CRLF
CRLF
CRLF
CRLF CRLF
<КОД СТАТУСА>— ez a szám az operáció eredményét jelenti. A szerverünk mindig 200-as státuszt fog visszaadni (sikeres művelet).<ОПИСАНИЕ СТАТУСА>— a státuszkód szöveges reprezentációja. A 200-as státuszkód esetén ezOK.— a kéréshez hasonló formátumú fejléc. A következő fejlécet fogjuk visszaadni:Content-Length(a fájl mérete) ésContent-Type: text/html(visszaadott adatok típusa).— a felhasználó által kért adatok. Ebben az esetben ez a kép elérési útja a .
Fájl (egyszálú szerver), amely tartalmazza a fájlt , amely a következő függvények prototípusait tartalmazza:
Függvény prototípusok megjelenítése a common.h fájlban
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов принять новое соединение.
*/
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);Továbbá le van írva a funkcionális makró SAFE_CALL() és a fail()függvény definiálva van. A makró az kifejezés értékét összehasonlítja a hibával, és ha a feltétel teljesült, meghívja a fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funkció fail() végrehajtja a megadott argumentumokat a terminálra (mint ) és leállítja a programot a 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);
}Funkció new_server() visszaadja a "szerver" socket fájl leíróját, amelyet a rendszerhívások hoznak létre , és és képes fogadni a bejövő kapcsolódásokat nem blokkoló módban.
new_server() függvény megjelenítése
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;
}- Vegye figyelembe, hogy a socket kezdetben nem blokkoló módban jön létre a
SOCK_NONBLOCKflag segítségével, hogy aon_accept()(olvasson tovább) rendszerhívásaccept()ne állítsa le a szál végrehajtását. - Ha
reuse_portegyenlőigaz, akkor a függvény beállítja a socketet a opcióval , hogy ugyanazt a portot használhassa a több szálú környezetben (lásd a "Többszálú szerver" szakaszt).
Eseménykezelő on_accept() akkor hívódik meg, amikor az OS eseményt generál EPOLLIN, ez esetben azt jelenti, hogy új kapcsolatot lehet fogadni. on_accept() új kapcsolatot fogad, átkapcsolja nem blokkoló módba és regisztrálja az eseménykezelőhöz on_recv() az I/O reaktorban.
on_accept() függvény megjelenítése
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);
}Eseménykezelő on_recv() akkor hívódik meg, amikor az OS eseményt generál EPOLLIN, ami azt jelenti, hogy a regisztrált kapcsolat on_accept(), készen áll az adatok fogadására.
on_recv() olvasza az adatokat a kapcsolaton, amíg a HTTP kérés teljesen el nem készül, majd regisztrálja a feldolgozó on_send() a HTTP válasz küldésére. Ha az ügyfél megszakítja a kapcsolatot, a socket törlésre kerül és lezárásra kerül a .
Funkció megjelenítése on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Fogadjuk a bejövő adatokat, amíg a recv 0-t vagy hibát nem ad vissza
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// Az ügyfél megszakította a kapcsolatot
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// a read hiba történt, ami nem a blokkoló hiba
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// A teljes HTTP kérés megérkezett az ügyféltől. Most regisztráljuk az eseménykezelőt
// az adatok küldésére
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}Eseménykezelő on_send() akkor hívódik meg, amikor az OS eseményt generál EPOLLOUT, ami azt jelenti, hogy a regisztrált kapcsolat on_recv(), készen áll az adatok küldésére. Ez a függvény HTTP választ küld az ügyfélnek, ami HTML-t tartalmaz képpel, majd ismét megváltoztatja az eseménykezelőt on_recv().
Funkció megjelenítése on_send()
statikus üres on_send(void *arg, int fd, uint32_t események) {\n const char *tartalom = "<img "
"src="https://habrastorage.org/webt/oh/wl/23/"
"ohwl23va3b-dioerobq_mbx4xaw.jpeg">";\n char válasz[1024];\n sprintf(válasz,\n "HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "\n "text/html" DOUBLE_CRLF "%s",\n strlen(tartalom), tartalom);\n\n SAFE_CALL(send(fd, válasz, strlen(válasz), 0), -1);\n SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);\n}Végül a fájlban http_server.c, a funkcióban main() a I/O reaktort a következő módon hozunk létre: reactor_new(), létrehozzuk a szerver socketet és regisztráljuk, elindítjuk a reaktort a következő módon: reactor_run() egészen egy percig, majd felszabadítjuk az erőforrásokat és kilépünk a programból.
http_server.c megjelenítése
#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);
}Ellenőrizzük, hogy minden jól működik. Fordítsuk le (chmod a+x compile.sh && ./compile.sh a projekt gyökerében) és indítsuk el az elkészített szervert, nyissuk meg a böngészőben, és nézzük meg, amit vártunk:

Teljesítménymérés
Mutasd a számítógépem jellemzőit
$ 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
.MMMMMMMMMMMMMMMMMMMMérjük meg az egyszálú szerver teljesítményét. Nyissunk meg két terminált: az egyiken indítsuk el ./http_server, a másikon pedig . Egy perc múlva a második terminálban az alábbi statisztika fog megjelenni:
$ 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.19MBEgyszálú szerverünk több mint 11 millió kérést tudott kezelni egy perc alatt, 100 kapcsolatból. Nem rossz eredmény, de lehet ezt javítani?
Többszálú szerver
Ahogy korábban említettük, az I/O reaktort külön szálakban hozhatjuk létre, ezzel maximálisan kihasználva a CPU összes magját. Alkalmazzuk ezt a gyakorlatban:
Megmutatni 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);
}
}Mostantól minden szál reaktort birtokol:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Figyeljen arra, hogy a függvény argumentumaként new_server() növelni kívánja igaz. Ez azt jelenti, hogy a szerver sockethez beállítjuk az opciót , hogy használhassuk azt több szálú környezetben. Részleteket olvashat .
Második kör
Most mérjük meg a többszálú szerver teljesítményét:
$ 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.14MBAz egy perc alatt feldolgozott kérések száma ~3.28-szorosára nőtt! De a kerek számhoz még ~két millió hiányzott, próbáljuk meg javítani.
Először nézzük meg az elkészített statisztikát :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses .\/http_server_multithreaded
Performancia számláló statisztikák a '.\/http_server_multithreaded'-hez:
242446,314933 task-clock (msec) # 4,000 CPU hasznosítás
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% az összes branch-ből
808 664 cache-misses
60,604330670 másodperc eltelt idő, lefordítás -march=native, , a találatok számának növelése a , növelés MAX_EVENTS és a használat EPOLLET nem hozott jelentős teljesítménynövekedést. De mi történik, ha növeljük a párhuzamos kapcsolatok számát?
Statisztika 352 párhuzamos kapcsolattal:
$ 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"
Elindult 1 perces teszt @ http://127.0.0.1:18470
8 szál és 352 kapcsolat
Szál Statisztika Átlag Stdev Max +/ - Stdev
Késlekedés 2.12ms 3.79ms 68.23ms 87.49%
Req/Sec 83.78k 12.69k 169.81k 83.59%
40006142 kérés 1.00m, 5.48GB olvasva
Kérés/másodperc: 665789.26
Átvitel/másodperc: 93.34MBA kívánt eredmény megvan, és vele egy érdekes grafikon, amely szemlélteti az egy perc alatt feldolgozott kérések számának függését a kapcsolatok számától:

Látjuk, hogy néhány száz kapcsolat után a feldolgozott kérések száma mindkét server esetében jelentősen csökken (a több szálon futó verziónál ez észlelhetőbb). Ez összefüggésben állhat a Linux TCP/IP stack megvalósításával? Kérjük, ossza meg véleményét a grafikon ilyen viselkedéséről és a több szálas illetve egyszálas változatok optimalizálásáról a megjegyzésekben.
Mint a megjegyzésekben, ez a teljesítményteszt nem mutatja az I/O reaktor viselkedését a valós terheléseken, mivel a server szinte mindig interakcióba lép az adatbázissal, naplókat ír ki, kriptográfiát használ stb., emiatt a terhelés heterogén (dinamikus) lesz. A teszteket harmadik féltől származó komponensekkel együtt a következő I/O reaktor cikkben fogjuk elvégezni.
I/O reaktor hátrányai
Meg kell értenünk, hogy az I/O reaktor nem mentes a hátrányaitól, mégpedig:
- Az I/O reaktor használata több szálas környezetben kissé bonyolultabb, mivel kézzel kell kezelni a szálakat.
- A gyakorlat azt mutatja, hogy a legtöbb esetben a terhelés nem egyenletes, ami azt eredményezheti, hogy az egyik szál folyamatosan dolgozik, míg a másik a munka miatt le van terhelve.
- Ha egy eseménykezelő blokkolja a szálat, akkor a rendszer kiválasztója is blokkolva lesz, ami nehezen észlelhető hibákhoz vezethet.
Ezeket a problémákat megoldja , amely gyakran tartalmaz ütemezőt, amely egyenletesen elosztja a terhelést a szálak pooljában, és emellett egy kényelmesebb API-val rendelkezik. Erről később beszélek, egy másik cikkemben.
Zárás
Ezzel a gyakorlatunk elérkezett a professzionális profilozo kimenethez.
Nem érdemes itt megállni, hiszen számos más, szintén érdekes megközelítés létezik a hálózati szoftverek írására, különböző kényelmi és sebességi szintekkel. Az alábbiakban érdekesnek tartott hivatkozásokat találsz.
Viszontlátásra!
Érdekes projektek
- C
Mit olvassak még?
Forrás: habr.com
