
Hyrje
(me një reçipent) ) — është një model për zhvillimin e softuerit me ngarkesë të lartë, i përdorur në shumë zgjidhje të njohura:
- …
Në këtë artikull ne do të shqyrtojmë thelbin e reaktorit I/O dhe parimin e funksionimit të tij, do të shkruajmë një implementim në më pak se 200 linja kodi dhe do të bëjmë që një server HTTP i thjeshtë të përpunojë mbi 40 milion kërkesa/në minutë.
Parathënie
- Ky artikull është shkruar me qëllimin për t'ju ndihmuar të kuptoni funksionimin e reaktorit I/O, dhe kështu të kuptoni rrezikun e përdorimit të tij.
- Për të kuptuar artikullin kërkohet njohuri themelore dhe një përvojë të vogël në zhvillimin e aplikacioneve rrjet.
- I gjithë kodi është shkruar në gjuhën C në përputhje me (kujdes: PDF i gjatë) për Linux dhe është në dispozicion në .
Përse është kjo e nevojshme?
Me rritjen e popullaritetit të Internetit, serverët web filluan të kishin nevojë për të përpunuar një numër të madh lidhjesh në të njëjtën kohë, për këtë arsye u provuan dy qasje: I/O i bllokuar me numër të madh të proceseve dhe I/O jo-bllokues në kombinim me një sistem njoftimi për ngjarjet, i quajtur gjithashtu «selektores sistemi» (///etc).
Qasja e parë përfshinte krijimin e një rrjedhe të re të OS për çdo lidhje që hyn. Disavantazhi i tij është se ka një shkallë të dobët të shkallëzimit: sistemi operacional do të duhet të kryejë shumë dhe . Ato janë operacione të shtrenjta dhe mund të shkaktojnë mungesë të RAM-it të lirë kur numri i lidhjeve është i konsiderueshëm.
Versioni i modifikuar shpërndan (baza e rrjedhave), duke e bërë kështu të pamundur që sistemi të ndalojë ekzekutimin papritur, por gjithashtu sjell një problem të ri: nëse në atë moment bazën e rrjedhave e bllokojnë operacionet e shëndoshë të leximit, atëherë soketat e tjera, që janë tashmë në gjendje të pranojnë të dhëna, nuk do të mund ta bëjnë këtë.
Qasja e dytë përdor (selektoni sistemor), të cilin e ofron OS. Në këtë artikull do të trajtohet lloji më i zakonshëm i selektorit sistemor, i bazuar në njoftime (ngjarje, njoftime) për gatishmërinë për operacione I/O, sesa mbi . Një shembull i përmbledhur i përdorimit të tij mund të paraqitet me diagramin e mëposhtëm:

Dallimi ndërmjet këtyre qasjeve qëndron në si vijon:
- Operacionet I/O bllokuese ndalojnë rrjedhën e përdoruesit deri sasistemi operativ të paketat në një rrjedhë bajtash (, marrja e të dhënave) ose të lirohet mjaftueshëm hapësirë në tamponët e brendshëm të shkrimit për dërgimin e mëtejshëm përmes (dërgimi i të dhënave).
- Selektoni sistemin pas një kohe njofton programin se sistemi operativ janë ka defragmentuar paketat IP (TCP, marrja e të dhënave) ose është e mjaftueshme hapësira në tamponët e brendshëm të shkrimit janë disponohet (dërgimi i të dhënave).
Duke përmbledhur, rezervimi i rrjedhës së sistemit operativ për çdo I/O është një shpërdorim i fuqive llogaritëse, pasi në të vërtetë, rrjedhat nuk janë të angazhuara në punë të dobishme (këtu fillon termi ). Selektoni sistemin zgjidh këtë problem, duke lejuar programin e përdoruesit të shpenzojë burimet e CPU-së shumë më ekonomike.
Modeli I/O reaktori
I/O reaktori vepron si një shtresë midis selektorit sistemik dhe kodit të përdoruesit. Parimi i funksionimit të tij përshkruhet nga skema e mëposhtme:

- Të kujtoj se eventi është një njoftim që një socket i caktuar është në gjendje të kryejë një operacion I/O jo bllokues.
- Punonjësi i ngjarjeve është një funksion, i cili thirret nga reaktori I/O kur merr një ngjarje dhe më pas kryen një operacion I/O jo bllokues.
Është e rëndësishme të theksohet se reaktori I/O është në mënyrë definicionale një线程, por asgjë nuk e pengon përdorimin e konceptit në një mjedis multithread në lidhje me 1线程: 1 reaktor, duke shfrytëzuar kështu të gjitha thellësitë e CPU-së.
Realizimi
Ne do ta vendosim ndërfaqen publike në skedarin , dhe realizimin në . reactor.h do të përbëhet nga këto shpallje:
Trego shpalljet në reactor.h
typedef struct reactor Reactor;
/*
* Pika e funksionit që do të thirret nga I/O reaktori kur ndodh
* një ngjarje nga selektori i sistemit.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Ktheni `NULL` në rast gabimi, një tregues i pa-`NULL` në `Reactor` në
* përndryshe.
*/
Reactor *reactor_new(void);
/*
* Çliron selektorin e sistemit, të gjitha soketat e regjistruara në këtë moment
* dhe vetë I/O reaktorin.
*
* Funksionet e mëposhtme kthejnë -1 në rast gabimi, 0 në rast suksesi.
*/
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);
/*
* Nisin ciklin e ngjarjeve me një kohë-marrje `timeout`.
*
* Kjo funksion do të kalojë kontrollin tek kodi që thirret nëse koha e caktuar ka kaluar
* ose/në mungesë të soketeve të regjistruara.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Struktura e I/O reaktorit përbëhet nga selektori dhe , e cila lidh çdo soket me CallbackData (strukturë e menaxherit të ngjarjeve dhe argumentit të përdoruesit për të).
Shfaq Reactor dhe CallbackData
struktura reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Kujdesi, ne kemi përdorur mundësinë e adresimit me përmes treguesit. Në reactor.h ne shpallim strukturën reactor, ndërsa në reactor.c e definojmë, duke e bërë që përdoruesi të mos ketë mundësi ta përmirësojë drejtpërdrejt fushat e saj. Kjo është një nga modelet , e cila përshtatet bukur në semantikën e C.
Funksionet reactor_register, reactor_deregister dhe reactor_reregister përditësojnë listën e sockets që interesojnë dhe përkatësit e ngjarjeve në selektorin sistemik dhe në tabelën e hashes.
Trego funksionet e regjistrimit
#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;
}Pas që I/O reaktori ka kapur një ngjarje me descriptorin fd, ai thërret përkatësin e ngjarjes, në të cilin kalon fd, të ngjarjeve të gjeneruara dhe treguesin personal në void.
Trego funksionin 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) {
// Gabim
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Koha ka skaduar
case 0:
result = 0;
goto cleanup;
// Operacioni i suksesshëm
default:
// Thirrja e trajtuesve të ngjarjeve
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Dhe në përfundim, zinxhiri i thirrjeve të funksioneve në kodin e përdoruesit do të marrë këtë formë:

Server një-fije
Për të testuar reaktorin I/O nën ngarkesë të lartë, do të shkruajmë një server HTTP të thjeshtë që përgjigjet me një imazh për çdo kërkesë.
Kërkesë e shkurtër për protokollin HTTP
— ky është protokolli , kryesisht që përdoret për ndërveprimin e serverit me shfletuesin.
HTTP mund të përdoret lehtësisht mbi protokolli , duke dërgon dhe merr mesazhe formati, i caktuar .
Formati i kërkesës
CRLF
CRLF
CRLF
CRLF CRLF CRLF— është një shkronjë e dy simboleve:rdhen, e cila ndan rreshtin e parë të kërkesës, titujt dhe të dhënat.<КОМАНДА>— një ngaCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Shfletuesi do t'i dërgojë serverit tonë komandenGET, që do të thotë "Më dërgo përmbajtjen e skedarit."<URI>— . Për shembull, nëse URI =/index.html, klienti kërkon faqen kryesore të uebit.<ВЕРСИЯ HTTP>— versioni i protokollit HTTP në formatinHTTP/X.Y. Versioni më i përdorur sot ështëHTTP/1.1.— është një çift çelës-vlerë në formatin:, i dërguar serverit për analizë të mëtejshme.<ДАННЫЕ>— të dhënat e kërkuara nga serveri për të realizuar operacionin. Shpesh, ato janë thjesht ose çdo format tjetër.
Formati i përgjigjes
CRLF
CRLF
CRLF
CRLF CRLF
<КОД СТАТУСА>— është një numër që paraqet rezultatin e operacionit. Serveri ynë gjithmonë do të kthejë status 200 (operacion i suksesshëm).— një paraqitje string e kodit të statusit. Për kodin e statusit 200 — kjo ështëOK.— një krye në të njëjtin format si në kërkesë. Ne do të kthejmë kryeContent-Length(madhësia e skedarit) dheContent-Type: text/html(lloji i të dhënave të kthyer).<ДАННЫЕ>— të dhënat e kërkuara nga përdoruesi. Në rastin tonë, kjo është rruga e imazhit në .
Skedari (server një-rrjedhësor) përfshin skedarin , i cili përmban prototipet e funksioneve të mëposhtme:
Trego prototipet e funksioneve në common.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);Gjithashtu është përshkruar makro funksionale SAFE_CALL() dhe është përcaktuar funksioni fail(). Makro krahason vlerën e shprehjes me gabimin, dhe nëse kushti është i plotësuar, thërret funksionin fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funksioni fail() printon argumentet e kaluara në terminal (si ) dhe e ndalon programin me kodin 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);
}Funksioni new_server() kthen një deshifrant skedar 'socket' të krijuar me thirrjet sistemore , dhe dhe në gjendje të pranojë lidhje të ardhshme në modin jo bllokues.
Trego funksionin new_server()
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;
}- Vini re se soketi krijohet fillimisht në modin jo bllokues me flamurin
SOCK_NONBLOCK, për të siguruar që thirrja sistemikeon_accept()(lexoni më tej) nuk ndalon ekzekutimin e thread-it.accept()reuse_port - Nëse
reuse_portështëe vërtetë, atëherë ky funksion do ta konfigurojë soketin me opsionin përmes , për të përdorur të njëjtin port në një mjedis me shumë threade (shih seksionin 'Server me shumë threade').
Trajtuesi i ngjarjeve on_accept() thirret pasi OPS gjeneron një ngjarje EPOLLIN, në këtë rast do të thotë se një lidhje e re mund të pranohet. on_accept() pranon një lidhje të re, e kalon atë në modalitetin jo-bllokues dhe e regjistron me një trajtues ngjarjesh on_recv() në reaktorin I/O.
Trego funksionin on_accept()
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);
}Trajtuesi i ngjarjeve on_recv() thirret pasi OPS gjeneron një ngjarje EPOLLIN, në këtë rast do të thotë se lidhja, e regjistruar on_accept(), është gati për të pranuar të dhëna.
on_recv() lexon të dhënat nga lidhja deri sa të plotësohet kërkesa HTTP, pastaj regjistron trajtuesin on_send() për dërgimin e përgjigjes HTTP. Nëse klienti ka ndërprerë lidhjen, atëherë soketi hiqet nga regjistrimi dhe mbyllet përmes .
Trego funksionin on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Pranojmë të dhënat e hyrjes derisa recv të kthejë 0 ose një gabim
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// Klienti ndërpreu lidhjen
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read ktheu një gabim, ndryshe nga gabimi që do të bllokonte
// rrjedhën
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Kemi marrë një kërkesë të plotë HTTP nga klienti. Tani regjistrojmë menaxherin
// e ngjarjeve për dërgimin e të dhënave
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}Trajtuesi i ngjarjeve on_send() thirret pasi OPS gjeneron një ngjarje EPOLLOUT, që do të thotë se lidhja e regjistruar on_recv(), është e gatshme për të dërguar të dhëna. Ky funksion dërgon një përgjigje HTTP, që përmban HTML me një imazh, klientit, dhe pastaj ndryshon përsëri menaxherin e ngjarjeve në on_recv().
Trego funksionin on_send()
static void on_send(void *arg, int fd, uint32_t events) {
const char *content = "<img "
"src="https://habrastorage.org/webt/oh/wl/23/"
"ohwl23va3b-dioerobq_mbx4xaw.jpeg">";
char response[1024];
sprintf(response,
"HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "
"text/html" DOUBLE_CRLF "%s",
strlen(content), content);
SAFE_CALL(send(fd, response, strlen(response), 0), -1);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);
}Dhe përfundimisht, në skedarin http_server.c, në funksionin main() krijojmë një reaktor I/O përmes reactor_new(), krijojmë një socket serveri dhe e regjistrojmë atë, duke nisur reaktorin me reactor_run() për një minutë të plotë, pastaj çlironi burimet dhe dilni nga programa.
Trego http_server.c
#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);
}Të kontrollojmë se gjithçka funksionon siç duhet. Kompilojmë (chmod a+x compile.sh && ./compile.sh në rrënjën e projektit) dhe nisëm serverin e shkruar nga vetë, hapim në shfletues dhe vëzhgojmë atë që pritej:

Masa e performancës
Trego karakteristikat e makinës time
$ 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
.MMMMMMMMMMMMMMMMMMMDo të masim performancën e serverit me një farë. Do të hapim dy terminale: në një do të nisnim ./http_server, në tjetrin — . Pas një minute në terminalin e dytë do të shfaqet statistika e mëposhtme:
$ 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.19MBServeri ynjëkanalësh arriti të përpunojë mbi 11 milion kërkesa në minutë nga 100 lidhje. Një rezultat i mirë, por a është e mundur ta përmirësojmë atë?
Server multiniveli
Siç u përmend më lart, reaktori I/O mund të krijohet në procese të veçanta, duke shfrytëzuar kështu të gjitha bërthamat e CPU-së. Le të aplikojmë këtë qasje në praktikë:
Shfaq 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);
}
}Tani secili proces :
static Reactor *reactor;
#pragma omp threadprivate(reactor)Vini re se argumenti i funksionit new_server() në këtë rast e vërtetë. Kjo do të thotë se po i japim socketit të serverit opsionin , për ta përdorur atë në një ambient multiniveli. Më shumë mund të lexoni .
Të dyta
Tani do të matim performancën e serverit multiniveli:
$ 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"
Duke test 1m në http://127.0.0.1:18470
8 threads dhe 100 connections
Statistikat e Thread Avg Stdev Max +/- Stdev
Latenca 1.14ms 2.53ms 40.73ms 89.98%
Kërkesa/Sek 79.98k 18.07k 154.64k 78.65%
38208400 kërkesa në 1.00m, 5.23GB të lexuara
Kërkesa/sek: 635876.41
Transferim/sek: 89.14MBNumri i kërkesave të përpunuara për 1 minutë u rrit në ~3.28 herë! Por nuk arriti numrin e plotë për afërsisht ~dy milion, le të përpiqemi ta rregullojmë këtë.
Së pari, le të shohim statistikat e gjeneruara :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded
Statistikat e numëruesve të performancës për './http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPU të përdorur
1 813 074 kalime kontekstesh # 0,007 M/sec
4 689 migrime CPU # 0,019 K/sec
254 gabime faqeje # 0,001 K/sec
895 324 830 170 cikle # 3,693 GHz
621 378 066 808 instruksione # 0,69 instruksion për cikël
119 926 709 370 degë # 494,653 M/sec
3 227 095 669 humbje të degëve # 2,69% e të gjitha degëve
808 664 humbje cache
60,604330670 sekonda koha e kaluar, kompaktim me -march=native, , rritja e numrit të goditjeve në , rritje MAX_EVENTS dhe përdorimi EPOLLET nuk solli një rritje të konsiderueshme në performancë. Por çfarë do ndodhë nëse rritet numri i lidhjeve në të njëjtën kohë?
Statistikat në 352 lidhje ndihmëse:
$ 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"
Duke ekzekutuar testin për 1m @ http://127.0.0.1:18470
8 threads dhe 352 lidhje
Statistikët e Threadit Mesatar Stdev Maks +/- Stdev
Vonesa 2.12ms 3.79ms 68.23ms 87.49%
Req/Sec 83.78k 12.69k 169.81k 83.59%
40006142 kërkesa në 1.00m, 5.48GB të lexuara
Kërkesa/sekond: 665789.26
Transferim/sekond: 93.34MBRezultati i dëshiruar është arritur, së bashku me një grafik interesant që tregon varësinë e numrit të kërkesave të përpunuara për 1 minutë nga numri i lidhjeve:

Shohim se pas disa qindra lidhjeve numri i kërkesave të përpunuara bie ndjeshëm për të dy serverët (në variantin me shumë procese kjo është më e dukshme). A është kjo e lidhur me realizimin e TCP/IP stack të Linux? Nëse keni supozime rreth këtij sjellje të grafikës dhe optimizimeve të varianteve me shumë procese dhe një procesi, mos hezitoni të shkruani në komentet.
Si n komentet, testi i këtij performancë nuk tregon sjelljen e reaktorit I/O nën ngarkesa reale, sepse pothuajse gjithmonë serveri ndërvepron me DB, nxjerr logje, përdor kriptografi me etj., për shkak të së cilës ngarkesa bëhet e ndryshueshme (dinamike). Testet së bashku me komponentët e jashtëm do të kryhen në artikullin për reaktorin I/O.
Disvantazhet e reaktorit I/O
Duhet të kuptohet se reaktori I/O nuk është pa disavantazhe, sidomos:
- Përdorimi i reaktorit I/O në një mjedis me shumëThreads është disi më i komplikuar, sepse do të duhet të menaxhohen manualisht rrjedhat.
- Praktika tregon se në shumicën e rasteve ngarkesa është e ndryshueshme, gjë që mund të çojë në situatën ku një rrjedhë do të presë, ndërsa tjetra është e ngarkuar me punë.
- Nëse një trajtues ngjarjeje bllokon një rrjedhë, atëherë do të bllokohet edhe selektori sistemor, gjë që mund të çojë në defekte të vështira për t'u kapur.
Këto probleme zgjidhen nga , shpesh duke pasur një planifikues që shpërndan ngarkesën në mënyrë të barabartë në grupin e rrjedhave dhe gjithashtu ka një API më të përshtatshëm. Rreth tij do të flas më vonë, në artikullin tim tjetër.
Përfundimi
Këtu përfundon udhëtimi ynë nga teoria në profilin e daljes.
Nuk duhet të ndaleni këtu, pasi ekzistojnë shumë qasje të tjera po aq interesante për të shkruar software rrjetesh me nivele të ndryshme komoditeti dhe shpejtësie. Linket interesante, sipas mendimit tim, janë të listuara më poshtë.
Hasta la vista!
Projekte interesante
- C
Çfarë tjetër të lexoni?
Burimi: habr.com
