
Paraqitje
(me një fije ) është një model për të shkruar softuer me ngarkesë të lartë, i përdorur në shumë zgjidhje të njohura:
- ...
Në këtë artikull, ne do të shikojmë të dhënat e një reaktori I/O dhe mënyrën se si funksionon, do të shkruajmë një zbatim në më pak se 200 rreshta kodi dhe do të bëjmë një proces të thjeshtë të serverit HTTP mbi 40 milionë kërkesa/min.
Parathënie libri
- Artikulli u shkrua për të ndihmuar në kuptimin e funksionimit të reaktorit I/O, dhe për këtë arsye për të kuptuar rreziqet gjatë përdorimit të tij.
- Për të kuptuar artikullin kërkohet njohja e bazave. dhe disa përvojë në zhvillimin e aplikacioneve të rrjetit.
- I gjithë kodi është shkruar në gjuhën C në mënyrë rigoroze sipas (kujdes: PDF e gjatë) për Linux dhe është i disponueshëm në .
Pse bëhet kjo?
Me rritjen e popullaritetit të internetit, serverët e uebit filluan të kenë nevojë të trajtojnë një numër të madh lidhjesh njëkohësisht, dhe për këtë arsye u provuan dy qasje: bllokimi i hyrjes/daljes në një numër të madh temash të sistemit operativ dhe mosbllokimi i hyrjes/daljes në kombinim me një sistem njoftimi për ngjarje, i quajtur gjithashtu "zgjedhësi i sistemit" (///etj).
Qasja e parë përfshinte krijimin e një filli të ri OS për çdo lidhje hyrëse. Disavantazhi i tij është shkallëzueshmëria e dobët: sistemi operativ do të duhet të zbatojë shumë и . Ato janë operacione të shtrenjta dhe mund të çojnë në mungesë të RAM-it të lirë me një numër mbresëlënës lidhjesh.
Versioni i modifikuar thekson (peshinë thread), duke parandaluar kështu sistemin nga ndërprerja e ekzekutimit, por në të njëjtën kohë duke paraqitur një problem të ri: nëse një grup thread është aktualisht i bllokuar nga operacionet e leximit të gjatë, atëherë prizat e tjera që tashmë janë në gjendje të marrin të dhëna nuk do të jenë në gjendje të bej keshtu.
Qasja e dytë përdor (zgjedhësi i sistemit) i siguruar nga OS. Ky artikull diskuton llojin më të zakonshëm të përzgjedhësit të sistemit, bazuar në sinjalizimet (ngjarjet, njoftimet) në lidhje me gatishmërinë për operacionet I/O, dhe jo në . Një shembull i thjeshtuar i përdorimit të tij mund të përfaqësohet nga bllok diagrami i mëposhtëm:

Dallimi midis këtyre qasjeve është si më poshtë:
- Bllokimi i operacioneve I/O pezullojë fluksi i përdoruesit deri saderisa OS të jetë në rregull hyrëse në rrjedhën e bajtit (, duke marrë të dhëna) ose nuk do të ketë hapësirë të mjaftueshme në buferët e brendshëm të shkrimit për dërgimin e mëvonshëm nëpërmjet (dërgimi i të dhënave).
- Zgjedhësi i sistemit me kalimin e kohës njofton programin se OS tashmë paketa IP të defragmentuara (TCP, marrja e të dhënave) ose hapësirë e mjaftueshme në buferët e brendshëm të shkrimit tashmë në dispozicion (dërgimi i të dhënave).
Për ta përmbledhur, rezervimi i një thread OS për çdo I/O është humbje e fuqisë kompjuterike, sepse në realitet, thread-ët nuk po bëjnë punë të dobishme (prandaj termi ). Përzgjedhësi i sistemit e zgjidh këtë problem, duke i lejuar programit të përdoruesit të përdorë burimet e CPU-së shumë më ekonomikisht.
Modeli i reaktorit I/O
Reaktori I/O vepron si një shtresë ndërmjet përzgjedhësit të sistemit dhe kodit të përdoruesit. Parimi i funksionimit të tij përshkruhet nga bllok diagrami i mëposhtëm:

- Më lejoni t'ju kujtoj se një ngjarje është një njoftim që një prizë e caktuar është në gjendje të kryejë një operacion I/O jo-bllokues.
- Një mbajtës i ngjarjeve është një funksion i thirrur nga reaktori I/O kur merret një ngjarje, i cili më pas kryen një operacion I/O jo-bllokues.
Është e rëndësishme të theksohet se reaktori I/O është sipas përkufizimit me një filetim të vetëm, por nuk ka asgjë që e ndalon konceptin të përdoret në një mjedis me shumë fije në një raport prej 1 fije: 1 reaktor, duke ricikluar kështu të gjitha bërthamat e CPU.
Zbatimi
Ne do të vendosim ndërfaqen publike në një skedar , dhe zbatimi - në . reactor.h do të përbëhet nga njoftimet e mëposhtme:
Trego deklaratat në reaktor.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);Struktura e reaktorit I/O përbëhet nga përzgjedhës и , e cila e lidh çdo fole në CallbackData (struktura e një mbajtësi të ngjarjeve dhe një argument i përdoruesit për të).
Shfaq të dhënat e reaktorit dhe të thirrjes
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Ju lutemi vini re se ne kemi aktivizuar aftësinë për të trajtuar sipas indeksit. NË reactor.h ne deklarojmë strukturën reactordhe në reactor.c ne e përcaktojmë atë, duke parandaluar kështu përdoruesin të ndryshojë në mënyrë eksplicite fushat e tij. Ky është një nga modelet , e cila përshtatet në mënyrë të përmbledhur në semantikën C.
Funksionet reactor_register, reactor_deregister и reactor_reregister përditësoni listën e prizave me interes dhe mbajtësit përkatës të ngjarjeve në përzgjedhësin e sistemit dhe tabelën hash.
Shfaq 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;
}Pasi reaktori I/O ka përgjuar ngjarjen me përshkruesin fd, ai thërret mbajtësin përkatës të ngjarjeve, tek i cili kalon fd, ngjarjet e krijuara dhe një tregues përdoruesi për void.
Shfaq 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 < 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;
}Për ta përmbledhur, zinxhiri i thirrjeve të funksionit në kodin e përdoruesit do të marrë formën e mëposhtme:

Server me një filetim të vetëm
Për të testuar reaktorin I/O nën ngarkesë të lartë, ne do të shkruajmë një server të thjeshtë në internet HTTP që i përgjigjet çdo kërkese me një imazh.
Një referencë e shpejtë për protokollin HTTP
- ky është protokolli , përdoret kryesisht për ndërveprimin server-shfletues.
HTTP mund të përdoret lehtësisht protokoll , dërgimi dhe marrja e mesazheve në një format të specifikuar .
Formati i Kërkesës
<КОМАНДА> <URI> <ВЕРСИЯ HTTP>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>CRLFështë një sekuencë prej dy karakteresh:rиn, duke ndarë rreshtin e parë të kërkesës, titujt dhe të dhënat.<КОМАНДА>- nje ngaCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Shfletuesi do të dërgojë një komandë në serverin tonëGET, që do të thotë "Më dërgoni përmbajtjen e skedarit."<URI>- . Për shembull, nëse URI =/index.html, atëherë klienti kërkon faqen kryesore të faqes.<ВЕРСИЯ HTTP>— versioni i protokollit HTTP në formatHTTP/X.Y. Versioni më i përdorur sot ështëHTTP/1.1.<ЗАГОЛОВОК N>është një çift çelës-vlerë në format<КЛЮЧ>: <ЗНАЧЕНИЕ>, dërguar në server për analiza të mëtejshme.<ДАННЫЕ>— të dhënat e kërkuara nga serveri për të kryer operacionin. Shpesh është e thjeshtë ose ndonjë format tjetër.
Formati i përgjigjes
<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ><КОД СТАТУСА>është një numër që përfaqëson rezultatin e operacionit. Serveri ynë gjithmonë do të kthejë statusin 200 (operim i suksesshëm).<ОПИСАНИЕ СТАТУСА>— paraqitja e vargut të kodit të statusit. Për kodin e statusit 200 kjo ështëOK.<ЗАГОЛОВОК N>- titulli i të njëjtit format si në kërkesë. Ne do t'i kthejmë titujtContent-Length(madhësia e skedarit) dheContent-Type: text/html(kthimi i llojit të të dhënave).<ДАННЫЕ>— të dhënat e kërkuara nga përdoruesi. Në rastin tonë, kjo është rruga drejt imazhit në .
skedar (server me një filetim të vetëm) përfshin skedarin , i cili përmban prototipet e mëposhtme të funksionit:
Trego prototipet e funksioneve të përbashkëta.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);Makro funksionale është përshkruar gjithashtu SAFE_CALL() dhe funksioni është i përcaktuar fail(). Makroja krahason vlerën e shprehjes me gabimin, dhe nëse kushti është i vërtetë, thërret funksionin fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funksion fail() printon argumentet e kaluara në terminal (si ) dhe përfundon 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);
}Funksion new_server() kthen përshkruesin e skedarit të prizës "server" të krijuar nga thirrjet e sistemit , и dhe të aftë për të pranuar lidhjet hyrëse në një modalitet 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 priza fillimisht krijohet në modalitetin jo-bllokues duke përdorur flamurin
SOCK_NONBLOCKnë mënyrë që në funksionon_accept()(lexo më shumë) thirrje sistemiaccept()nuk e ndaloi ekzekutimin e fillit. - Nëse
reuse_portështëtrue, atëherë ky funksion do të konfigurojë prizën me opsionin përmes për të përdorur të njëjtën portë në një mjedis me shumë fije (shih seksionin "Serveri me shumë fije").
Mbajtës i ngjarjeve on_accept() thirret pasi OS gjeneron një ngjarje EPOLLIN, në këtë rast do të thotë se lidhja e re mund të pranohet. on_accept() pranon një lidhje të re, e kalon atë në modalitetin pa bllokim dhe regjistrohet me një mbajtës të ngjarjeve on_recv() në një reaktor 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);
}Mbajtës i ngjarjeve on_recv() thirret pasi OS gjeneron një ngjarje EPOLLIN, në këtë rast do të thotë se lidhja është regjistruar on_accept(), gati për të marrë të dhëna.
on_recv() lexon të dhënat nga lidhja derisa kërkesa HTTP të merret plotësisht, pastaj regjistron një mbajtës on_send() për të dërguar një përgjigje HTTP. Nëse klienti prish lidhjen, priza çregjistrohet dhe mbyllet duke përdorur .
Shfaq funksionin 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);
}
}Mbajtës i ngjarjeve on_send() thirret pasi OS gjeneron një ngjarje EPOLLOUT, që do të thotë se lidhja është regjistruar on_recv(), gati për të dërguar të dhëna. Ky funksion dërgon një përgjigje HTTP që përmban HTML me një imazh te klienti dhe më pas ndryshon trajtuesin e ngjarjeve përsëri në on_recv().
Shfaq 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 së fundi, në dosje http_server.c, në funksion main() ne krijojmë një reaktor I/O duke përdorur reactor_new(), krijoni një prizë serveri dhe regjistrojeni atë, filloni duke përdorur reaktorin reactor_run() për saktësisht një minutë, dhe më pas lëshojmë burime dhe dalim nga programi.
Shfaq 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ë kontrollojmë që gjithçka po funksionon siç pritej. Përpilimi (chmod a+x compile.sh && ./compile.sh në rrënjën e projektit) dhe hapni serverin e vetë-shkruar në shfletues dhe shikoni se çfarë prisnim:

Matja e performancës
Trego specifikimet 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
.MMMMMMMMMMMMMMMMMMMLe të matim performancën e një serveri me një filetim të vetëm. Le të hapim dy terminale: në një do të vrapojmë ./http_server, në një tjetër - . Pas një minutë, statistikat e mëposhtme do të shfaqen në terminalin 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 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ë me një fije të vetme ishte në gjendje të përpunonte mbi 11 milionë kërkesa në minutë, me origjinë nga 100 lidhje. Nuk është një rezultat i keq, por a mund të përmirësohet?
Server me shumë fije
Siç u përmend më lart, reaktori I/O mund të krijohet në fije të veçanta, duke shfrytëzuar kështu të gjitha bërthamat e CPU. Le ta zbatojmë 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 çdo fije reaktor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Ju lutemi vini re se argumenti i funksionit new_server() aktet true. Kjo do të thotë që ne ia caktojmë opsionin prizës së serverit për ta përdorur atë në një mjedis me shumë fije. Mund të lexoni më shumë detaje .
Vrapimi i dytë
Tani le të matim performancën e një serveri me shumë fije:
$ 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.14MBNumri i kërkesave të përpunuara në 1 minutë është rritur me ~3.28 herë! Por ne kishim vetëm XNUMX milionë pak nga numri i rrumbullakët, kështu që le të përpiqemi ta rregullojmë atë.
Së pari le të shohim statistikat e krijuara :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded
Performance counter stats for './http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPUs utilized
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% of all branches
808 664 cache-misses
60,604330670 seconds time elapsed, përmbledhje me -march=native, , një rritje në numrin e goditjeve , rrit MAX_EVENTS dhe përdorni EPOLLET nuk dha një rritje të ndjeshme të performancës. Por çfarë ndodh nëse rrit numrin e lidhjeve të njëkohshme?
Statistikat për 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"
Running 1m test @ http://127.0.0.1:18470
8 threads and 352 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 2.12ms 3.79ms 68.23ms 87.49%
Req/Sec 83.78k 12.69k 169.81k 83.59%
40006142 requests in 1.00m, 5.48GB read
Requests/sec: 665789.26
Transfer/sec: 93.34MBRezultati i dëshiruar u mor dhe me të një grafik interesant që tregon varësinë e numrit të kërkesave të përpunuara në 1 minutë nga numri i lidhjeve:

Shohim që pas disa qindra lidhjeve, numri i kërkesave të përpunuara për të dy serverat bie ndjeshëm (kjo është më e dukshme në versionin me shumë fije). A lidhet kjo me implementimin? TCP/IP grumbull LinuxNdihuni të lirë të ndani mendimet tuaja mbi sjelljen e këtij grafiku dhe optimizimet për versionet me shumë fije dhe me një fije në komente.
Si në komente, ky test i performancës nuk tregon sjelljen e reaktorit I/O nën ngarkesa reale, sepse pothuajse gjithmonë serveri ndërvepron me bazën e të dhënave, nxjerr regjistrat, përdor kriptografinë me etj., si rezultat i së cilës ngarkesa bëhet jo uniforme (dinamike). Testet së bashku me komponentët e palëve të treta do të kryhen në artikullin për proaktorin I/O.
Disavantazhet e reaktorit I/O
Ju duhet të kuptoni se reaktori I/O nuk është pa të metat e tij, përkatësisht:
- Përdorimi i një reaktori I/O në një mjedis me shumë fije është disi më i vështirë, sepse do t'ju duhet të menaxhoni manualisht flukset.
- Praktika tregon se në shumicën e rasteve ngarkesa është jo uniforme, gjë që mund të çojë në prerjen e një fijeje ndërsa një tjetër është e zënë me punë.
- Nëse një mbajtës ngjarjesh bllokon një thread, vetë zgjedhësi i sistemit do të bllokojë gjithashtu, gjë që mund të çojë në gabime të vështira për t'u gjetur.
Zgjidh këto probleme , i cili shpesh ka një planifikues që shpërndan në mënyrë të barabartë ngarkesën në një grup fijesh, dhe gjithashtu ka një API më të përshtatshëm. Ne do të flasim për këtë më vonë, në artikullin tim tjetër.
Përfundim
Këtu ka përfunduar udhëtimi ynë nga teoria drejt e në shter të profilit.
Ju nuk duhet të ndaleni në këtë, sepse ka shumë qasje të tjera po aq interesante për të shkruar softuer rrjeti me nivele të ndryshme komoditeti dhe shpejtësie. Interesante, për mendimin tim, lidhjet janë dhënë më poshtë.
Deri herën tjetër!
Projekte interesante
- B
Çfarë tjetër për të lexuar?
Burimi: www.habr.com
