
Wprowadzenie
(jedno wątkowy ) — to wzorzec do pisania oprogramowania o dużym obciążeniu, wykorzystywany w wielu popularnych rozwiązaniach:
- …
W artykule tym przyjrzymy się podsystemowi reaktora I/O oraz zasadzie jego działania, napiszemy implementację w mniej niż 200 linijkach kodu i zmusimy prosty serwer HTTP do obsługi ponad 40 milionów żądań na minutę.
Przedmowa
- Artykuł został napisany w celu pomocy w zrozumieniu działania reaktora I/O, a co za tym idzie, uświadomienia sobie ryzyk związanych z jego używaniem.
- Aby zrozumieć artykuł, wymagane jest podstawowe znawstwo i niewielkie doświadczenie w tworzeniu aplikacji sieciowych.
- Cały kod napisany jest w języku C ściśle wedługuwaga: długi PDF) dla systemu Linux i jest dostępny na .
Po co to potrzebne?
Wraz ze wzrostem popularności Internetu serwery WWW musiały obsługiwać wiele połączeń jednocześnie, w związku z czym wypróbowano dwa podejścia: blokujące I/O przy dużej liczbie wątków OS oraz nieblokujące I/O w połączeniu z systemem powiadamiania o zdarzeniach, nazywanym także "selekcjonowaniem systemowym" (///etc).
Pierwsze podejście zakładało stworzenie nowego wątku systemu operacyjnego dla każdego przychodzącego połączenia. Jego wadą jest zła skalowalność: system operacyjny będzie musiał wykonać wiele и . Są to operacje kosztowne i mogą prowadzić do braku dostępnej pamięci RAM przy znacznej liczbie połączeń.
Zmodyfikowana wersja przydziela (puli wątków), co zapobiega awaryjnemu zakończeniu działania systemu, ale wprowadza nowy problem: jeśli w danym momencie pula wątków blokuje długotrwałe operacje odczytu, inne gniazda, które mogą już przyjąć dane, nie będą mogły tego zrobić.
Drugie podejście wykorzystuje (selekcja systemowa), który jest dostarczany przez system operacyjny. W tym artykule omówiony zostanie najczęściej spotykany typ selektora systemowego, oparty na powiadomieniach (zdarzeniach, notyfikacjach) o gotowości do operacji I/O, a nie na . Uproszczony przykład jego zastosowania 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 nie przychodzące do strumienia bajtów (, odbieranie danych) lub nie zwolni się wystarczająco dużo miejsca w wewnętrznych buforach zapisu na wysyłanie przez (wysyłanie danych).
- Selektor systemowy po pewnym czasie informuje program, że system operacyjny już zdefragmentował pakiety IP (TCP, odbieranie danych) lub dostępne jest wystarczająco dużo miejsca w wewnętrznych buforach zapisu już (wysyłanie danych).
Podsumowując, rezerwacja strumienia systemu operacyjnego dla każdej operacji I/O to marnotrawstwo mocy obliczeniowej, ponieważ w rzeczywistości strumienie nie są zaangażowane w użyteczną pracę (stąd termin ). Selektor systemowy rozwiązuje ten problem, umożliwiając programowi użytkownika dużo bardziej oszczędne zużycie zasobów CPU.
Model reaktora I/O
Reaktor I/O działa jako warstwa między selektorem systemowym a kodem użytkownika. Zasada jego działania opisana jest w poniższej schemacie blokowym:

- Przypominam, że zdarzenie to powiadomienie, że dany gniazdo jest w stanie wykonać nieblokującą operację I/O.
- Handler zdarzeń to funkcja wywoływana przez reaktor I/O w momencie otrzymania zdarzenia, która następnie dokonuje nieblokującej operacji I/O.
Warto zaznaczyć, że reaktor I/O z definicji działa w jednym wątku, jednak nic nie stoi na przeszkodzie, aby wykorzystać ten koncept w środowisku wielowątkowym, stosując 1 wątek: 1 reaktor, co umożliwia wykorzystanie wszystkich rdzeni CPU.
Implementacja
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 na funkcję, która będzie wywoływana przez reaktor I/O, gdy pojawi się
* zdarzenie od systemowego selektora.
*/
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 wypadku.
*/
Reactor *reactor_new(void);
/*
* Zwolnij systemowy selektor, 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 cykl zdarzeń z czasem oczekiwania `timeout`.
*
* Ta funkcja przekaże kontrolę kodowi wywołującemu, gdy czas upłynie
* lub/i gdy nie ma zarejestrowanych gniazd.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Struktura reaktora I/O składa się z selekora и , która przypisuje każde gniazdo do CallbackData (struktura zawierająca obsługę zdarzenia oraz 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;Zauważ, że aktywowaliśmy możliwość obsługi przez wskaźnik. W reactor.h deklarujemy strukturę reactor, a w reactor.c definiujemy ją, uniemożliwiając użytkownikowi jawne modyfikowanie jej pól. To jeden z wzorców , elegancko wpisujący się w semantykę C.
Funkcje reactor_register, reactor_deregister и reactor_reregister aktualizują listę interesujących gniazd i odpowiednich obsługujących zdarzenia w systemowym selektorze oraz w tabeli 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 deskryptorem fd, wywołuje odpowiedniego obsługującego zdarzenie, przekazując mu fd, wygenerowanych zdarzeń oraz wskaźnik użytkownika na 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łaj obsługiwacze 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 będzie wyglądał następująco:

Serwer jednowątkowy
Aby przetestować reaktor I/O pod dużym obciążeniem, napiszemy prosty serwer HTTP, który odpowiada obrazem na każde zapytanie.
Krótki przewodnik po protokole HTTP
— to protokół , najczęściej używany do komunikacji serwera z przeglądarką.
HTTP można łatwo używać nad протокола , wysyłając i odbierając wiadomości w formacie określonym .
Format zapytania
CRLF
CRLF
CRLF
CRLF CRLFCRLF— to ciąg dwóch znaków:rиn, oddzielający pierwszą linię zapytania, nagłówki i dane.<КОМАНДА>— jedna zCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Przeglądarka wysyła do naszego serwera polecenieGET, co oznacza „Wyślij mi zawartość pliku”.<URI>— . Na przykład, jeśli URI =/index.html, klient żąda strony głównej witryny.<ВЕРСИЯ HTTP>— wersja protokołu HTTP w formacieHTTP/X.Y. Najczęściej używaną wersją w dzisiejszych czasach jestHTTP/1.1.<ЗАГОЛОВОК N>— to para klucz-wartość w formacie:, wysyłana do serwera w celu dalszej analizy.<ДАННЫЕ>— dane wymagane przez serwer do wykonania operacji. Często jest to po prostu lub jakikolwiek inny format.
Format odpowiedzi
CRLF
CRLF
CRLF
CRLF CRLF<КОД СТАТУСА>— to liczba, która reprezentuje wynik operacji. Nasz serwer zawsze będzie zwracał status 200 (operacja zakończona sukcesem).<ОПИСАНИЕ СТАТУСА>— tekstowa reprezentacja kodu statusu. Dla kodu statusu 200 — jest toOK.<ЗАГОЛОВОК N>— nagłówek w tym samym formacie, co w żądaniu. Będziemy zwracać nagłówkiContent-Length(rozmiar pliku) iContent-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ż makro funkcji SAFE_CALL() i zdefiniowaną 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)Funkcja fail() drukuje przekazane argumenty w 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);
}Funkcja new_server() zwraca deskryptor pliku „serwera” gniazda, utworzonego przez wywołania systemowe , и i jest w stanie przyjmować przychodzące połączenia w trybie bezblokują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 bezblokującym z użyciem flagi
SOCK_NONBLOCK, aby w funkcjion_accept()(czytaj dalej) wywołanie systemoweaccept()nie zatrzymało wykonania wątku. - Jeśli
reuse_portwynositrue, ta funkcja skonfiguruje gniazdo z opcją przez , aby używać tego samego portu w środowisku wielowątkowym (patrz sekcja „Serwer wielowątkowy”).
Handler zdarzeń on_accept() jest wywoływany po tym, jak system operacyjny wygeneruje zdarzenie EPOLLIN, w tym przypadku oznaczające, że nowe połączenie może być przyjęte. on_accept() przyjmuje nowe połączenie, przełącza je w tryb nieblokujący i rejestruje z obsługą zdarzeń on_recv() 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);
}Handler zdarzeń on_recv() jest wywoływany po tym, jak system operacyjny wygeneruje zdarzenie EPOLLIN, w tym przypadku oznaczające, że połączenie zarejestrowane on_accept(), jest gotowe do otrzymania danych.
on_recv() odczytuje dane z połączenia, aż cały żądanie HTTP zostanie odebrane, następnie rejestruje obsługę on_send() do wysłania odpowiedzi HTTP. Jeśli klient przerwie połączenie, gniazdo zostaje wykreślone i zamknięte za pomocą .
Pokaż funkcję on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Odbieramy dane wejściowe dopóki recv nie zwróci 0 lub błędu
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, który blokuje wywołanie
// wątku
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Otrzymano kompletną prośbę HTTP od klienta. Teraz rejestrujemy
// obsługę zdarzeń na wysyłanie danych
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}Handler zdarzeń on_send() jest wywoływany po tym, jak system operacyjny wygeneruje zdarzenie EPOLLOUT, oznaczającego, że połączenie zarejestrowane on_recv(), jest gotowe do wysyłania danych. Ta funkcja wysyła odpowiedź HTTP zawierającą HTML z obrazem do klienta, a następnie zmienia obsługę zdarzeń ponownie na on_recv().
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);
}A na koniec, w pliku http_server.c, w funkcji main() tworzymy reaktor I/O za pomocą reactor_new(), tworzymy gniazdo serwera i rejestrujemy je, uruchamiamy reaktor za pomocą reactor_run() dokładnie na jedną 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);
}Sprawdzimy, czy wszystko działa prawidłowo. Kompilujemy (chmod a+x compile.sh && ./compile.sh w katalogu głównym projektu) i uruchamiamy serwer, otwieramy w przeglądarce i widzimy to, czego się spodziewaliśmy:

Pomiar wydajności
Pokaż specyfikacje mojego sprzętu
$ 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
.MMMMMMMMMMMMMMMMMMMZmierzmy wydajność serwera jednowątkowego. Otworzymy dwa terminale: w jednym uruchomimy ./http_server, w drugim — . Po minucie w drugim terminalu pojawią się następujące statystyki:
$ 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"
Trwa test przez 1 minutę @ http://127.0.0.1:18470
8 wątków i 100 połączeń
Statystyki wątków Średnia Odchylenie Maks. +/- Odchylenie
Opóźnienie 493.52µs 76.70µs 17.31ms 89.57%
Żądania/sek 24.37k 1.81k 29.34k 68.13%
11657769 żądań w 1.00m, 1.60GB odczytano
Żądania/sek: 193974.70
Transfer/sek: 27.19MBNasz serwer jednowątkowy zdołał obsłużyć ponad 11 milionów żądań na minutę, pochodzących z 100 połączeń. To niezły wynik, ale czy można go poprawić?
Serwer wielowątkowy
Jak wspomniano wcześniej, reaktor I/O można tworzyć w oddzielnych wątkach, tym samym wykorzystując wszystkie rdzenie CPU. Zastosujmy to podejście w praktyce:
Pokaż 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 środowisku wielowątkowym. Więcej na ten temat można przeczytać .
Druga część
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: pl-PL,pl;q=0.5" -H "Connection: keep-alive"
Uruchamianie testu przez 1 minutę na http://127.0.0.1:18470
8 wątków i 100 połączeń
Statystyki wątków Śr Odch. standardowe Max +/– Odch. standardowe
Opóźnienie 1.14ms 2.53ms 40.73ms 89.98%
Żądań/sek 79.98k 18.07k 154.64k 78.65%
38208400 żądań w 1.00m, 5.23GB odczytanych
Żądań/sek: 635876.41
Transfer/sek: 89.14MBLiczba przetworzonych żądań w ciągu 1 minuty wzrosła o ~3.28 razy! Brakowało ~dwóch milionów, spróbujmy to poprawić.
Najpierw przyjrzymy się statystykom, które wygenerowano :
$ 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 zegar-zadań (ms) # 4,000 CPU wykorzystane
1 813 074 przełączenia kontekstu # 0,007 M/sek
4 689 migracje cpu # 0,019 K/sek
254 błędy strony # 0,001 K/sek
895 324 830 170 cykle # 3,693 GHz
621 378 066 808 instrukcje # 0,69 insn na cykl
119 926 709 370 gałęzie # 494,653 M/sek
3 227 095 669 błędy gałęzi # 2,69% wszystkich gałęzi
808 664 błędy pamięci cache
60,604330670 sekundy czasu minęło, kompilacja z -march=native, , zwiększenie liczby trafień w , wzrost MAX_EVENTS i zastosowanie EPOLLET nie przyniosło znaczącego wzrostu wydajności. Co się jednak stanie, jeśli zwiększymy liczbę jednoczesnych połączeń?
Statystyki 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"
Przeprowadzanie testu przez 1 min @ http://127.0.0.1:18470
8 wątków i 352 połączenia
Statystyki wątków Średnia Odchylenie standardowe Max +/− Odchylenie
Opóźnienie 2.12ms 3.79ms 68.23ms 87.49%
Req/sec 83.78k 12.69k 169.81k 83.59%
40006142 żądań w 1.00m, 5.48GB przeczytane
Żądania/sec: 665789.26
Transfer/sec: 93.34MBPożądany wynik uzyskany, a wraz z nim ciekawy wykres, który pokazuje 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 znacznie spada (w przypadku wersji wielowątkowej jest to bardziej zauważalne). Czy jest to związane z implementacją stosu TCP/IP w systemie Linux? Z przyjemnością przyjmujemy wasze spostrzeżenia na temat tego zachowania wykresu i optymalizacji w wersjach wielowątkowej i jednowątkowej w komentarzach.
Jak w komentarzach, ten test wydajności nie pokazuje zachowania I/O reaktora pod realnymi obciążeniami, ponieważ prawie zawsze serwer współdziała z bazą danych, generuje logi, korzysta z kryptografii z itd., co sprawia, że obciążenie staje się niejednorodne (dynamiczne). Testy wraz z zewnętrznymi komponentami będą przeprowadzone w artykule o I/O proaktorze.
Wady I/O reaktora
Należy rozumieć, że I/O reaktor ma swoje wady, a mianowicie:
- Korzystanie z I/O reaktora w środowisku wielowątkowym jest nieco bardziej skomplikowane, ponieważ trzeba ręcznie zarządzać wątkami.
- Praktyka pokazuje, że w większości przypadków obciążenie jest niejednorodne, co może prowadzić do sytuacji, w której jeden wątek będzie działał, podczas gdy inny będzie obciążony pracą.
- Jeśli jeden handler zdarzenia zablokuje wątek, zablokuje się również sam systemowy selektor, co może prowadzić do trudnych do zdiagnozowania błędów.
Te problemy rozwiązuje , który często ma harmonogram, który równomiernie rozdziela obciążenie w puli wątków oraz posiada bardziej przyjazne API. O tym będzie mowa później, w moim innym artykule.
Podsumowanie
Na tym kończymy naszą podróż z teorii prosto do wyjścia profanera.
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 są podane poniżej.
Do zobaczenia!
Interesujące projekty
- C
Co jeszcze przeczytać?
Źródło: habr.com
