
Hyrje
(njëthë një ) — është një model për shkruajtur softuer 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, nga një zbatim me më pak se 200 rreshta kod dhe do të bëjmë një server HTTP të thjeshtë që e trajton mbi 40 milion kërkesa/min.
Parathënie
- Artikulli është shkruar me qëllim për të ndihmuar në kuptimin e funksionimit të reaktorit I/O, dhe ndonjëherë për të kuptuar rreziqet e përdorimit të tij.
- Për të kuptuar artikullin kërkohet njohuri bazë dhe një përvojë të vogël në zhvillimin e aplikacioneve rrjetë.
- Të gjithë kodet janë shkruar në gjuhën C në përputhje me (kujdes: PDF i gjatë) për Linux dhe janë në dispozicion në .
Pse është e nevojshme?
Me rritjen e popullaritetit të Internetit, serverat web duhet të trajtojnë një numër të madh lidhjesh në të njëjtën kohë, për çfarë janë provuar dy qasje: I/O bllokuese në një numër të madh të proceseve të OS dhe I/O jo bllokuese në kombinim me një sistem njoftimi për ngjarje, gjithashtu të quajtur 'selezionuesi i sistemit' (///etc).
Qasja e parë nënkupton krijimin e një procesi të ri OS për çdo lidhje hyrëse. Disavantazhi i saj është shkallëzimi i dobët: sistemi operativ do të duhet të bëjë shumë dhe . Këto janë operacione të shtrenjta dhe mund të çojnë në mungesë të RAM-it të lirë në rastin e një numri të konsiderueshëm lidhjesh.
Një version i modifikuar ndan (puli i proceseve), duke mos lejuar që sistemi të ndalojë papritmas ekzekutimin, por njëkohësisht sjell një problem të ri: nëse për momentin pulat e proceseve bllokojnë operacione të zgjatura të leximit, atëherë socket-e të tjera, të cilat janë tashmë në gjendje të pranojnë të dhëna, nuk do të jenë në gjendje ta bëjnë këtë.
Qasja e dytë përdor (selezionuesi i sistemit) që ofrohet nga OS. Në këtë artikull shqyrtohet forma më e zakonshme e selektorit të sistemit, e bazuar në njoftime (ngjarje, njoftimet) për gatishmërinë për operacione I/O, përkundrazi nga . Një shembull i përmbledhur i përdorimit të tij mund të paraqitet në këtë diagram të thjeshtë:

Dallimi midis këtyre qasjeve është si në vijim:
- Operacioneve I/O bllokuese ndalojnë rrjedhën e përdoruesit Ky mungesë kulture mbështetjeje, së bashku me parimin "le të thyejmë, për ta bërë më të bukur", i largon zhvilluesit nga ata., derisa OS-ja nuk e paketave në një rrjedhë bajtësh (, marrja e të dhënave) ose nuk lirohet hapësirë e mjaftueshme në buffer-at e brendshëm të shkrimit për dërgesat (dërgimi i të dhënave).
- Selekstori sistemor pas një kohe njofton programin se OS tashmë ka defragmentuar paketat IP (TCP, marrja e të dhënave) ose ka hapësirë të mjaftueshme në buffer-at e brendshëm të shkrimit tashmë në dispozicion (dërgimi i të dhënave).
Për të përmbledhur, rezervimi i rrjedhës së OS për çdo I/O është një shpenzim i kotë i fuqisë llogaritëse, sepse në të vërtetë, rrjedhat nuk janë të angazhuara në punë të dobishme (këtu përfshihet termi ). Selekstori sistemor zgjidh këtë problem duke lejuar programin përdorues të shpenzojë burimet e CPU-së në mënyrë më efektive.
Modeli i reaktorit I/O
Reaktori I/O shërben si një ndërmjetës midis selektorit sistemor dhe kodit përdorues. Parimi i tij i funksionimit përshkruhet nga diagrami i mëposhtëm:

- Për të kujtuar, një ngjarje është një njoftim se një soket i caktuar është në gjendje për të kryer një operacion I/O jo bllokues.
- Përgjegjësi i ngjarjeve është një funksion, i cili thirret nga reaktori I/O kur merr një ngjarje, e cila më pas kryen një operacion I/O jo bllokues.
Është e rëndësishme të theksohet se reaktori I/O përkufizohet si një njëzë, por nuk ka asgjë që e ndalon përdorimin e konceptit në një ambient shumëzuar në lidhje me 1 rrjedhë: 1 reaktor, duke shfrytëzuar kështu të gjitha bërthamat e CPU-së.
Implementimi
Ne do ta vendosim ndërfaqen publike në skedarin , dhe implementimin në . reactor.h do të përbëhet nga këto shpallje:
Shfaq shpalljet në reactor.h
typedef struct reactor Reactor;
/*
* Pika për funksionin që do të thirret nga reaktori I/O kur ndodh një
* ngjarje nga selektori sistemor.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Kthen `NULL` në rast të gabimit, pointer jo-`NULL` në `Reactor`
* në rast tjetër.
*/
Reactor *reactor_new(void);
/*
* Çliron selektorin sistemor, të gjitha soketet e regjistruara në këtë moment
* dhe vetë reaktorin I/O.
*
* Funksionet e mëposhtme kthejnë -1 në rast të gabimit, 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);
/*
* Nis ciklin e ngjarjeve me kufirin `timeout`.
*
* Kjo funksion do të transferojë kontrollin në kodin e thirrjes nëse koha e rezervuar ka skaduar
* ose/edhe në mungesë të soketeve të regjistruara.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Struktura e reaktorit I/O përbëhet nga selektori dhe , e cila përputh çdo socket me CallbackData (strukturë nga menaxheri i ngjarjeve dhe argumenti i përdoruesit për të).
Shiko Strukturën e Reaktorit dhe CallbackData
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Vërejtje, kemi përdorur mundësinë e trajtimit të përmes pointerit. Në reactor.h ne deklarojmë strukturën reactor, ndërsa në reactor.c e cila e përcakton, duke mos lejuar përdoruesin që të ndryshojë qartë fushat e saj. Kjo është një nga modelet , që përputhet me semantikën C.
Funksionet reactor_register, reactor_deregister dhe reactor_reregister përditësojnë listën e soketeve të interesit dhe menaxherëve të ngjarjeve në selektorin sistemor dhe në tabelën hash.
Shiko 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 kapjes së ngjarjes nga reaktori I/O me deshifruesin fd, ai thërret menaxherin përkatës të ngjarjes, duke i kaluar fd, të ngjarjeve të gjeneruara dhe pointerin e përdoruesit në void.
Shiko 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) {
// Ошибка
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Время вышло
case 0:
result = 0;
goto cleanup;
// Успешная операция
default:
// Вызвать обработчиков событий
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Në përfundim, zinxhiri i thirrjeve të funksioneve në kodin e përdoruesit do të marrë formën e mëposhtme:

Server një-fibrash
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ë.
Informacione të shkurtra mbi protokollin HTTP
— është një protokoll , kryesisht i përdorur për komunikimin ndërmjet serverit dhe shfletuesit.
HTTP mund të përdoret lehtë mbi protokollit , duke dërguar dhe pranuar mesazhe në formatin e përcaktuar nga .
Formati i kërkesës
CRLF
CRLF
CRLF
CRLF CRLFCRLF— është një sekuencë e dy simboleve:rdhen, që ndan rreshtin e parë të kërkesës, headers dhe të dhënat.<КОМАНДА>— njëra ngaCONNECT,FSHI,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Shfletuesi do t'i dërgojë serverit tonë komandënGET, 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ë faqes.<ВЕРСИЯ HTTP>— versioni i protokollit HTTP në formatinHTTP/X.Y. Versioni më i përdorur sot ështëHTTP/1.1.<ЗАГОЛОВОК N>— është një çift çelës-vlerë në formatin:, që dërgohet serverit për analizë të mëtejshme.<ДАННЫЕ>— të dhënat e nevojshme për serverin për të kryer operacionin. Shpesh është thjesht ose ndonjë format tjetër.
Formati i përgjigjes
CRLF
CRLF
CRLF
CRLF CRLF
<КОД СТАТУСА>— është një numër që përfaqëson rezultatin e operacionit. Serveri ynë do të kthejë gjithmonë statusin 200 (operacion i suksesshëm).— një përfaqësim në formë string të kodit të statusit. Për kodin e statusit 200 — kjo ështëOK.<ЗАГОЛОВОК N>— një kokë e po atij formati si në kërkesë. Ne do të kthejmë kokatContent-Length(madhësia e skedarit) dheContent-Type: text/html(tipi i të dhënave që kthehen).<ДАННЫЕ>— të dhënat e kërkuara nga përdoruesi. Në rastin tonë, kjo është rruga drejt images në .
Skeda (server i njëmbledhës) përfshin skedarin , i cili përmban prototipet e funksioneve të mëposhtme:
Shfaq 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 makrosi funksional SAFE_CALL() dhe është e definuar funksioni fail(). Makrosi krahasohet me vlerën e shprehjes me gabimin, dhe nëse kushti është 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 dhëna në terminal (si ) dhe ndalon punën e programit 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ë deshifrim skedari për 'socketin' e serverit, të krijuar me thirrjet sistemore , dhe dhe është e aftë të pranojë lidhje të ardhshme në modin jo-bllokues.
Shfaq 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 socketi fillimisht krijohet në modin jo-bllokues me flagun
SOCK_NONBLOCK, në mënyrë që në funksioninon_accept()(lexoni më tej) thirrja sistemoreaccept()nuk ndalon ekzekutimin e thread-it. - Nëse
reuse_portështëtrue, atëherë ky funksion do ta konfigurojë socketin me opsionin përmes , për të përdorur të njëjtin port në një mjedis shumë-threadesh (shih seksionin 'Server i shumë-threadit').
Menaxheri i ngjarjeve on_accept() thirret pasi OS të gjenerojë një ngjarje EPOLLIN, në këtë rast që nënkupton se një lidhje e re mund të pranohet. on_accept() pranon një lidhje të re, e kalon në modin jo-bllokues dhe regjistron me menaxherin e ngjarjes on_recv() në reaktorin I/O.
Shfaq 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);
}Menaxheri i ngjarjeve on_recv() thirret pasi OS të gjenerojë një ngjarje EPOLLIN, që nënkupton se lidhja e regjistruar on_accept(), është gati për të pranuar të dhëna.
on_recv() lexon të dhëna nga lidhja derisa kërkesa HTTP të jetë e plotë, pastaj regjistron trajtuesin on_send() për të dërguar përgjigjen HTTP. Nëse klienti ndërpret lidhjen, socket-i de-regjistrohet dhe mbyllet përmes .
Trego funksionin on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Pranojem të dhënat 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 tjetër përveç gabimit që do ta 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ë trajtuesin
// 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);
}
}Menaxheri i ngjarjeve on_send() thirret pasi OS të gjenerojë një ngjarje EPOLLOUT, që nënkupton se lidhja e regjistruar on_recv(), është gati për të dërguar të dhëna. Ky funksion dërgon përgjigjen HTTP, që përmban HTML me një imazh, klientit, dhe më pas ndryshon përsëri trajtuesin 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 në fund, në skedarin http_server.c, në funksionin main() ne krijojmë një reaktor I/O përmes reactor_new(), krijojmë një socket serveri dhe e regjistrojmë, e fillojmë reaktorin përmes reactor_run() pikërisht për një minutë, dhe më pas lirojmë burimet dhe dalim nga programi.
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);
}Le t'i kontrollojmë se të gjitha funksionon siç duhet. E kompulojmë (chmod a+x compile.sh && ./compile.sh në rrënjën e projektit) dhe nisëm serverin tonë të shkruar vetë, hapim në shfletues dhe vëzhgojmë atë që prisnim:

Matja e performancës
Trego karakteristikat e makinës sime
$ 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 një serveri një-në-një. Do ta hapim dy terminale: në njërin do të ekzekutojmë ./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 ynë një-në-një arriti të përpunojë mbi 11 milion kërkesa në minutë nga 100 lidhje. Një rezultat i këndshëm, por a mund ta përmirësojmë atë?
Serveri shumë-në-një
Siç u tha më lart, reaktori I/O mund të krijohet në procese të veçanta, duke shfrytëzuar të gjithë bërthamat e CPU-së. 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 si argumenti i funksionit. Kjo do të thotë se po i japim socket-it të serverit mundësinë new_server() të ardhmen do të dalin true, për ta përdorur atë në një mjedis shumë-në-një. Mund të lexoni më shumë Tani do të masim performancën e serverit shumë-në-një: .
Qasja e dytë
$ 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.14MB
Numri i kërkesave të përpunuara në 1 minutë u rrit në ~3.28 herë! Por na munguan vetëm ~dy milion për një numër të rrethuar, le të përpiqemi ta rregullojmë këtë.Së pari, le të shohim statistikat e gjeneruara
Së pari, le të shikojmë statistikën e gjeneruar :
$ 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 numrave për '. /http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPU-t e shfrytëzuar
1 813 074 kalimet e kontekstit # 0,007 M/sec
4 689 migrimet e CPU-së # 0,019 K/sec
254 gabimet e faqeve # 0,001 K/sec
895 324 830 170 ciklet # 3,693 GHz
621 378 066 808 instruksyonet # 0,69 instruksione për cikël
119 926 709 370 degët # 494,653 M/sec
3 227 095 669 humbjet e degëve # 2,69% e të gjitha degëve
808 664 humbjet e caches
60,604330670 sekondë kohë e kaluar, kompilimi me -march=native, , rritja e numrit të goditjeve në , rritja MAX_EVENTS dhe përdorimi EPOLLET nuk solli një rritje të konsiderueshme në performancë. Po, çfarë do ndodhte nëse rritnim numrin e lidhjeve të njëkohshme?
Statistika me 352 lidhje të njëkohshme:
$ 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 e realizuar testin për 1m @ http://127.0.0.1:18470
8 threads dhe 352 lidhje
Statistikat e Fijeve Avg Stdev Max +/ Stdev
Vonesa 2.12ms 3.79ms 68.23ms 87.49%
Req/Sek 83.78k 12.69k 169.81k 83.59%
40006142 kërkesa në 1.00m, 5.48GB lexuar
Kërkesa/sec: 665789.26
Transfer/sec: 93.34MBRezultati i dëshiruar është arritur, dhe bashkë me të një grafik interesant që tregon varësinë e numrit të kërkesave të procesuar gjatë 1 minute nga numri i lidhjeve:

E shohim se pas disa qindra lidhjesh, numri i kërkesave të procesuara për të dy serverët bie ndjeshëm (në versionin me shumë fije më duket edhe më e dukshme). A ka të bëjë kjo me implementimin e stack-ut TCP/IP në Linux? Shprehni mendimet tuaja lidhur me këtë sjellje të grafikët dhe optimizimet e versioneve me shumë dhe një fije në komente.
Si në komente, ky test performancës nuk tregon sjelljen e reaktorit I/O nën ngarkesa reale, pasi pothuajse gjithmonë serveri interakton me DB, shfaq log-et, përdor kriptografi me etj., duke e bërë ngarkesën të pauniformë (dinamike). Testet së bashku me komponentët e jashtëm do të zhvillohen në një artikull mbi reaktorin I/O.
Disavantazhet e reaktorit I/O
Duhet të kuptoni se reaktori I/O nuk është pa disavantazhe, pra:
- Të përdorësh reaktorin I/O në një mjedis me shumë fije është disi më e komplikuar, pasi do të duhet të menaxhosh manualisht fijet.
- Praktika tregon se në shumicën e rasteve ngarkesa është e pabarabartë, gjë që mund të çojë në situata ku një rrjedhë do të punojë, ndërkohë që një tjetër do të jetë e ngarkuar me punë.
- Nëse një trajtues eventi bllokon rrjedhën, atëherë do të bllokohet gjithashtu dhe selektori sistematik, që mund të çojë në defekte të vështira për t'u kapur.
Këto probleme zgjidhen nga , shpesh me një planifikues që shpërndan ngarkesën në mënyrë të barabartë në grupin e rrjedhave, dhe gjithashtu me një API më të lehtë për t'u përdorur. Për atë do të flasim më vonë, në artikullin tim tjetër.
Përfundim
Kështu, udhëtimi ynë nga teoria drejtpërdrejt në rezultatin e profiler-it përfundoi.
Nuk duhet të ndalemi këtu, sepse ka shumë qasje të tjera po aq interesante për shkrimin e softuerit rrjetor me nivele të ndryshme lehtësie dhe shpejtësie. Linket interesante, sipas mendimit tim, janë të dhëna më poshtë.
Në takim të ardhshëm!
Projekte interesante
- C
Çfarë tjetër të lexoni?
Burimi: habr.com
