
Wprowadzenie
(jednowątkowy ) — to wzorzec do pisania oprogramowania o wysokiej wydajności, stosowany w wielu popularnych rozwiązaniach:
- …
W tym artykule przyjrzymy się zasadzie działania reaktora I/O, napiszemy implementację w mniej niż 200 linijkach kodu i sprawimy, że prosty serwer HTTP będzie obsługiwał ponad 40 milionów żądań/min.
Wstęp
- Artykuł napisano, aby pomóc zrozumieć działanie reaktora I/O, a tym samym uświadomić sobie ryzyka związane z jego używaniem.
- Do zrozumienia artykułu wymagana jest znajomość podstaw i niewielkie doświadczenie w tworzeniu aplikacji sieciowych.
- Cały kod został napisany w języku C zgodnie z (uwaga: długi PDF) dla Linuxa i jest dostępny na .
Po co to potrzebne?
Wraz z rosnącą popularnością Internetu serwery WWW zaczęły wymagać obsługi dużej liczby połączeń jednocześnie, dlatego wypróbowano dwa podejścia: blokujące I/O na dużej liczbie wątków systemu operacyjnego oraz nieblokujące I/O w połączeniu z systemem powiadamiania o zdarzeniach, zwanym również «selektywnym systemem» (///etc).
Pierwsze podejście polegało na tworzeniu nowego wątku systemu operacyjnego dla każdego przychodzącego połączenia. Jego wadą jest słaba skalowalność: system operacyjny musi przeprowadzać wiele i . Są to kosztowne operacje i mogą prowadzić do niedoboru wolnej pamięci RAM przy dużej liczbie połączeń.
Zmodyfikowana wersja przydziela (pula wątków), co zapobiega nagłemu zakończeniu wykonywania przez system, ale jednocześnie wprowadza nowy problem: jeśli w danym momencie pula wątków blokuje długotrwałe operacje odczytu, inne gniazda, które już są gotowe do przyjęcia danych, nie będą mogły tego zrobić.
Drugie podejście używa (selektywny system), który zapewnia system operacyjny. W tym artykule omówiono najczęściej spotykaną wersję selektywnego systemu, opartą na powiadomieniach (zdarzeniach, informacjach) o gotowości do operacji I/O, a nie na . Uproszczony przykład jego użycia można przedstawić w następującej schemacie blokowym:

Różnica między tymi podejściami polega na następujących kwestiach:
- Operacje I/O blokujące wstrzymują strumień użytkownika dopókisystem operacyjny odpowiednio przychodzących do strumienia bajtów (, odbieranie danych) lub nie zwolni się wystarczająco dużo miejsca w buforach zapisu dla późniejszej wysyłki przez (wysyłanie danych).
- Selektor systemowy po pewnym czasie informuje program, że system operacyjny już zdefragmentował pakiety IP (TCP, odbieranie danych) lub wystarczająco dużo miejsca w buforach zapisu już jest dostępne (wysyłanie danych).
Podsumowując, rezerwacja strumienia systemu operacyjnego dla każdego I/O to marnotrawstwo mocy obliczeniowej, ponieważ w rzeczywistości strumienie nie są zajęte użyteczną pracą (stąd pochodzi termin ). Selektor systemowy rozwiązuje ten problem, pozwalając programowi użytkownika znacznie efektywniej wykorzystać zasoby CPU.
Model reaktora I/O
Reaktor I/O działa jako pośrednik między selektorem systemowym a kodem użytkownika. Zasada jego działania jest opisana przez poniższy diagram blokowy:

- Przypomnę, że zdarzenie to powiadomienie, że dany gniazdo jest gotowe do wykonania nieblokującej operacji I/O.
- Handler zdarzeń to funkcja, która jest wywoływana przez reaktor I/O po otrzymaniu zdarzenia, która następnie wykonuje nieblokującą operację I/O.
Warto zauważyć, że reaktor I/O z definicji jest jednowątkowy, ale nic nie stoi na przeszkodzie, aby używać tej koncepcji w środowisku wielowątkowym w konfiguracji 1 wątek: 1 reaktor, tym samym wykorzystując wszystkie rdzenie CPU.
Realizacja
Publiczny interfejs umieścimy w pliku , a implementację w . reactor.h będzie składać się z następujących deklaracji:
Pokaż deklaracje w reactor.h
typedef struct reactor Reactor;
/*
* Wskaźnik do funkcji, która będzie wywoływana przez reaktor I/O po otrzymaniu
* zdarzenia od selektora systemowego.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Zwraca `NULL` w przypadku błędu, wskaźnik różny od `NULL` na `Reactor` w
* przeciwnym razie.
*/
Reactor *reactor_new(void);
/*
* Zwalnia selektor systemowy, wszystkie zarejestrowane gniazda w danym momencie
* oraz sam reaktor I/O.
*
* Następujące funkcje zwracają -1 w przypadku błędu, 0 w przypadku sukcesu.
*/
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);
/*
* Uruchamia pętlę zdarzeń z czasem oczekiwania `timeout`.
*
* Ta funkcja przekaże kontrolę kodowi wywołującemu, jeśli przydzielony czas minął
* lub/lub przy braku zarejestrowanych gniazd.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Struktura reaktora I/O składa się z selektera i , która łączy każde gniazdo z CallbackData (struktura zawierająca obsługę zdarzenia i argument użytkownika dla niej).
Pokaż Reactor i CallbackData
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Zwróć uwagę, że wykorzystaliśmy możliwość odwołania się do za pomocą wskaźnika. W reactor.h ogłaszamy strukturę reaktora, a w reactor.c definiując ją w ten sposób, uniemożliwiając użytkownikowi jawne modyfikowanie jej pól. To jeden z wzorców , zgrabnie wpasowujący się w semantykę C.
Funkcje reactor_register, reactor_deregister i reactor_reregister aktualizują listę interesujących gniazd oraz odpowiadające im obsługi zdarzeń w selektorze systemowym i w tablicy haszującej.
Pokaż funkcje rejestracji
#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;
}Po tym jak reaktor I/O przechwyci zdarzenie z deskryptora fd, wywołuje odpowiednią obsługę zdarzenia, do której przekazuje fd, wygenerowanych zdarzeń oraz wskaźnik użytkownika do void.
Pokaż funkcję 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) {
// Błąd
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Czas minął
case 0:
result = 0;
goto cleanup;
// Operacja zakończona sukcesem
default:
// Wywołanie obsługiwaczy zdarzeń
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Podsumowując, łańcuch wywołań funkcji w kodzie użytkownika przyjmie następującą formę:

Serwer jednowątkowy
Aby przetestować reaktywator I/O pod dużym obciążeniem, napiszemy prosty serwer WWW, który odpowiada obrazem na każde zapytanie.
Krótki przewodnik po protokole HTTP
— to protokół , głównie używany do komunikacji między serwerem a przeglądarką.
HTTP można łatwo używać na warstwie protokołu , wysyłając i odbierając wiadomości w formacie określonym przez .
Format zapytania
CRLF
CRLF
CRLF
CRLF CRLFCRLF— to sekwencja dwóch znaków:rin, oddzielająca pierwszą linię zapytania, nagłówki i dane.<КОМАНДА>— jedna zCONNECT,USUŃ,i login/hasło: admin/admin.,HEAD,OPTIONS,PATCH,POST,Żądanie PUT:,TRACE. Przeglądarka wysyła do naszego serwera komendęi login/hasło: admin/admin., oznaczającą „Wyślij mi zawartość pliku”.<URI>— . Na przykład, jeśli URI =/index.html, to klient żąda strony głównej witryny.<ВЕРСИЯ HTTP>— wersja protokołu HTTP w formacieHTTP/X.Y. Najczęściej używana wersja to obecnie —HTTP/1.1.<ЗАГОЛОВОК N>— to para klucz-wartość w formacie:, wysyłana do serwera w celu dalszej analizy.<ДАННЫЕ>— dane, które serwer potrzebuje do wykonania operacji. Często są to po prostu lub inny format.
Format odpowiedzi
CRLF
CRLF
CRLF
CRLF CRLF<КОД СТАТУСА>— to liczba, która reprezentuje wynik operacji. Nasz serwer zawsze zwróci status 200 (operacja zakończona sukcesem).<ОПИСАНИЕ СТАТУСА>— tekstowa reprezentacja kodu statusu. Dla kodu statusu 200 — toOK.<ЗАГОЛОВОК N>— nagłówek w tym samym formacie, co w żądaniu. Będziemy zwracać nagłówkiContent-Length(rozmiar pliku) orazContent-Type: text/html(typ zwracanych danych).<ДАННЫЕ>— dane żądane przez użytkownika. W naszym przypadku jest to ścieżka do obrazu w .
Plik (serwer jednowątkowy) zawiera plik , który zawiera następujące prototypy funkcji:
Pokaż prototypy funkcji w 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);Zdefiniowano również funkcjonalny makros SAFE_CALL() i zdefiniowano funkcję fail(). Makro porównuje wartość wyrażenia z błędem, a jeśli warunek został spełniony, wywołuje funkcję fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)veth_xdp_flush_bq() fail() drukuje przekazane argumenty na terminalu (jak ) i kończy działanie programu z kodem 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);
}veth_xdp_flush_bq() new_server() zwraca deskryptor pliku 'serwera' gniazda, utworzonego za pomocą wywołań systemowych , i i mogącego przyjmować przychodzące połączenia w trybie nieblokującym.
Pokaż funkcję 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;
}- Zauważ, że gniazdo jest początkowo tworzone w trybie nieblokującym przy użyciu flagi
SOCK_NONBLOCK, aby funkcjaon_accept()(czytaj dalej) nie zatrzymała wykonywania wątku.accept()reuse_port - Jeśli
jest równy, więc ta funkcja skonfiguruje gniazdo z opcjątrueSO_REUSEPORT setsockopt() Obsługiwacz zdarzeń
jest wywoływany po tym, jak system operacyjny wygeneruje zdarzenie on_accept() EPOLLIN , w tym przypadku oznaczające, że nowe połączenie może być zaakceptowane.przyjmuje nowe połączenie, zmienia je w tryb nieblokujący i rejestruje z obsługiwaczem zdarzenia on_accept() on_recv() w reaktorze I/O. w reaktorze I/O.
Pokaż funkcję 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);
}jest wywoływany po tym, jak system operacyjny wygeneruje zdarzenie w reaktorze I/O. EPOLLIN , w tym przypadku oznaczające, że nowe połączenie może być zaakceptowane., co w tym przypadku oznacza, że połączenie zostało zarejestrowane on_accept(), gotowe do przyjęcia danych.
w reaktorze I/O. odczytuje dane z połączenia, dopóki cały HTTP request nie zostanie odebrany, następnie rejestruje handler on_send() do wysłania odpowiedzi HTTP. Jeśli klient przerwie połączenie, gniazdo jest deregisterowane i zamykane za pomocą .
Pokaż funkcję on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Odbieramy dane do momentu, gdy recv zwróci 0 lub wystąpi błąd
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// Klient przerwał połączenie
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read zwrócił błąd inny niż błąd, w którym wywołanie zablokuje
// wątek
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Otrzymano pełny HTTP request od klienta. Teraz rejestrujemy handler
// zdarzeń, aby wysłać dane
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}jest wywoływany po tym, jak system operacyjny wygeneruje zdarzenie on_send() EPOLLIN EPOLLOUT, co oznacza, że połączenie zostało zarejestrowane w reaktorze I/O., gotowe do wysłania danych. Ta funkcja wysyła odpowiedź HTTP zawierającą HTML z obrazem do klienta, a następnie zmienia handler zdarzeń z powrotem na w reaktorze I/O..
Pokaż funkcję 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);
}I w końcu, w pliku http_server.c, w funkcji main() tworzymy reaktor I/O za pomocą reactor_new(), tworzymy gniazdo serwerowe i rejestrujemy je, uruchamiamy reaktor przy pomocy reactor_run() dokładnie na minutę, a następnie zwalniamy zasoby i wychodzimy z programu.
Pokaż 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);
}Sprawdźmy, czy wszystko działa jak powinno. Kompilujemy (chmod a+x compile.sh && ./compile.sh w głównym katalogu projektu) i uruchamiamy serwer własnej roboty, otwieramy w przeglądarce i obserwujemy to, czego się spodziewaliśmy:

Pomiar wydajności
Pokaż specyfikacje mojego komputera
$ 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
.MMMMMMMMMMMMMMMMMMMMierzymy wydajność serwera jednowątkowego. Otworzymy dwa terminale: w jednym uruchomimy ./http_server, w drugim — . Po minucie w drugim terminalu wyświetli się następująca statystyka:
$ 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.19MBNasz serwer jednowątkowy był w stanie obsłużyć ponad 11 milionów zapytań na minutę, pochodzących z 100 połączeń. Niezły wynik, ale czy można go poprawić?
Serwer wielowątkowy
Jak już wspomniano, reaktor I/O można tworzyć w osobnych wątkach, tym samym wykorzystując wszystkie rdzenie CPU. Zastosujmy to podejście w praktyce:
Zobacz 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);
}
}Teraz każdy wątek reaktor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Zauważ, że argumentem funkcji new_server() jest true. To oznacza, że przypisujemy opcję do gniazda serwera, aby używać go w wielowątkowym środowisku. Szczegóły można przeczytać .
Drugie podejście
Teraz zmierzmy wydajność serwera wielowątkowego:
$ 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.14MBLiczba przetworzonych zapytań w ciągu 1 minuty wzrosła o ~3.28 razy! Ale do okrągłej liczby brakowało tylko ~dwa miliony, spróbujmy to naprawić.
Najpierw spojrzimy na statystykę wygenerowaną :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded
Statystyki liczników wydajności dla './http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPU wykorzystane
1 813 074 przełączenia kontekstu # 0,007 M/s
4 689 migracje CPU # 0,019 K/s
254 błędy stron # 0,001 K/s
895 324 830 170 cykle # 3,693 GHz
621 378 066 808 instrukcje # 0,69 instrukcji na cykl
119 926 709 370 rozgałęzienia # 494,653 M/s
3 227 095 669 błędy rozgałęzień # 2,69% wszystkich rozgałęzień
808 664 błędy pamięci podręcznej
60,604330670 sekund czasu upłynęło, kompilacja z -march=native, , zwiększenie liczby trafień w , zwiększenie MAX_EVENTS i użycie EPOLLET nie przyniosło znaczącego wzrostu wydajności. Ale co się stanie, jeśli zwiększymy liczbę jednoczesnych połączeń?
Statystyka przy 352 jednoczesnych połączeniach:
$ 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"
Uruchamianie testu 1m @ http://127.0.0.1:18470
8 wątków i 352 połączenia
Statystyki wątku Średnia Odch. standardowe Maks. +/- Odch. standardowe
Latencja 2.12ms 3.79ms 68.23ms 87.49%
Żądania/Sek 83.78k 12.69k 169.81k 83.59%
40006142 żądań w 1.00m, 5.48GB odczytane
Żądania/s: 665789.26
Transfer/s: 93.34MBOczekiwany rezultat osiągnięty, a wraz z nim interesujący wykres, pokazujący zależność liczby obsłużonych żądań w ciągu 1 minuty od liczby połączeń:

Widzimy, że po kilku setkach połączeń liczba obsłużonych żądań na obu serwerach dramatycznie spada (w przypadku wersji wielowątkowej jest to bardziej wyraźne). Czy ma to związek z implementacją stosu TCP/IP w systemie Linux? Zachęcamy do dzielenia się swoimi spostrzeżeniami na temat takiego zachowania wykresu i optymalizacji wersji wielowątkowej i jednowątkowej w komentarzach.
Jak w komentarzach, ten test wydajności nie pokazuje zachowania I/O reaktora pod prawdziwym obciążeniem, ponieważ prawie zawsze serwer współpracuje z bazą danych, zapisuje logi, korzysta z kryptografii z itd., co sprawia, że obciążenie staje się niejednorodne (dynamiczne). Testy z innymi komponentami będą przeprowadzone w artykule o I/O reaktorze.
Wady I/O reaktora
Należy zrozumieć, że I/O reaktor nie jest pozbawiony wad, a mianowicie:
- Korzystanie z I/O reaktora w środowisku wielowątkowym jest nieco trudniejsze, ponieważ trzeba ręcznie zarządzać wątkami.
- Praktyka pokazuje, że w większości przypadków obciążenie jest nierównomierne, co może prowadzić do sytuacji, gdzie jeden strumień będzie zajęty, podczas gdy inny jest obciążony pracą.
- Jeśli jeden obsługiwacz zdarzeń zablokuje wątek, to również zablokuje sam systemowy selektor, co może prowadzić do trudnych do wykrycia błędów.
Te problemy rozwiązuje , często mający harmonogram, który równomiernie rozkłada obciążenie w puli wątków, a także oferujący bardziej przyjazne API. O nim będzie mowa później, w moim innym artykule.
Podsumowanie
Na tym nasze podróż z teorii prosto do wydechu profilera dobiegła końca.
Nie warto na tym poprzestawać, ponieważ istnieje wiele innych, równie interesujących podejść do pisania oprogramowania sieciowego o różnym poziomie wygody i szybkości. Interesujące, moim zdaniem, linki znajdują się poniżej.
Do zobaczenia!
Interesujące projekty
- C
Co jeszcze poczytać?
Źródło: habr.com
