
Einführung
(einzelnd ) – ist ein Muster für das Schreiben von hochbelastbarer Software, das in vielen beliebten Lösungen verwendet wird:
- …
In diesem Artikel werden wir die Hintergründe des I/O-Reaktors und das Prinzip seiner Funktionsweise betrachten, eine Implementierung mit weniger als 200 Zeilen Code schreiben und einen einfachen HTTP-Server dazu bringen, über 40 Millionen Anfragen pro Minute zu verarbeiten.
Vorwort
- Der Artikel wurde geschrieben, um beim Verständnis des Funktionierens des I/O-Reaktors zu helfen und somit die Risiken bei seiner Verwendung zu erkennen.
- Für das Verständnis des Artikels sind Kenntnisse der Grundlagen und ein wenig Erfahrung in der Entwicklung von Netzwerk-Anwendungen erforderlich.
- Der gesamte Code ist in der Programmiersprache C streng nach (Vorsicht: Langer PDF) für Linux geschrieben und steht zur Verfügung auf .
Warum ist das nötig?
Mit der zunehmenden Popularität des Internets mussten Webserver eine große Anzahl von Verbindungen gleichzeitig verarbeiten, weshalb zwei Ansätze erprobt wurden: blockierendes I/O mit vielen Betriebssystem-Threads und nicht blockierendes I/O in Kombination mit einem Ereignisbenachrichtigungssystem, auch "Systemselector" genannt (///etc).
Der erste Ansatz sah vor, für jede eingehende Verbindung einen neuen Betriebssystem-Thread zu erstellen. Sein Nachteil ist eine schlechte Skalierbarkeit: Das Betriebssystem muss viele und durchführen. Diese sind kostspielige Operationen und können bei einer großen Anzahl von Verbindungen zu einem Mangel an freiem RAM führen.
Die modifizierte Version weist (Thread-Pool) zu, wodurch das System nicht in der Ausführung abgebrochen werden kann, aber gleichzeitig ein neues Problem entsteht: Wenn der Thread-Pool im Moment lange Lesevorgänge blockiert, können andere Sockets, die bereits bereit sind, Daten zu empfangen, dies nicht tun.
Der zweite Ansatz verwendet (Systemselector), das vom Betriebssystem bereitgestellt wird. In diesem Artikel betrachten wir die am häufigsten vorkommende Art des Systemselectors, die auf Benachrichtigungen (Ereignissen, Notifications) über die Bereitschaft zu I/O-Operationen basiert, statt auf . Ein vereinfachtes Beispiel seiner Verwendung könnte in folgendem Flussdiagramm dargestellt werden:

Der Unterschied zwischen den beiden Ansätzen liegt darin:
- Blockierende I/O-Operationen unterbrechen den Benutzer-Thread solange, bis das Betriebssystem ordnungsgemäß eingehenden in einen Byte-Stream (, Datenempfang) umwandelt oder genügend Platz in den internen Schreibpuffern für die spätere Übertragung über freigegeben wird (Datenübertragung).
- Der Systemselektor benachrichtigt nach einiger Zeit das Programm darüber, dass das Betriebssystem bereits die IP-Pakete (TCP, Datenempfang) defragmentiert hat oder ausreichend Platz in den internen Schreibpuffern bereits verfügbar ist (Datenübertragung).
Zusammenfassend lässt sich sagen, dass die Reservierung eines Threads durch das Betriebssystem für jede I/O-Operation eine sinnlose Verschwendung von Rechenleistung darstellt, da die Threads tatsächlich nicht mit nützlicher Arbeit beschäftigt sind (hierher stammt der Begriff ). Der Systemselektor löst dieses Problem, indem er es dem Benutzerprogramm ermöglicht, die CPU-Ressourcen deutlich effizienter zu nutzen.
Das I/O-Reaktor-Modell
Der I/O-Reaktor fungiert als Zwischenschicht zwischen dem Systemselektor und dem Benutzer-Code. Das Prinzip seiner Funktionsweise wird durch das folgende Flussdiagramm beschrieben:

- Ich erinnere daran, dass ein Ereignis eine Benachrichtigung darüber ist, dass ein bestimmter Socket in der Lage ist, eine nicht blockierende I/O-Operation auszuführen.
- Der Ereignis-Handler ist eine Funktion, die vom I/O-Reaktor beim Empfang eines Ereignisses aufgerufen wird und die dann eine nicht blockierende I/O-Operation durchführt.
Es ist wichtig zu beachten, dass der I/O-Reaktor definitionsgemäß einkabelig ist, aber es steht nichts im Wege, dieses Konzept in einer mehrthreadigen Umgebung in Bezug auf 1 Thread: 1 Reaktor zu verwenden, wodurch alle CPU-Kerne ausgelastet werden.
Implementierung
Wir werden die öffentliche Schnittstelle in eine Datei , und die Implementierung in . reactor.h bestehen aus den folgenden Deklarationen:
Die Deklarationen in reactor.h anzeigen
typedef struct reactor Reactor;
/*
* Ein Zeiger auf eine Funktion, die vom I/O-Reaktor aufgerufen wird, wenn ein
* Ereignis vom systemeigenen Selektor eintrifft.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Gibt `NULL` im Fehlerfall zurück, im anderen Fall einen nicht-`NULL` Zeiger auf `Reactor`.
*/
Reactor *reactor_new(void);
/*
* Gibt den systemeigenen Selektor, alle momentan registrierten Sockets
* und den I/O-Reaktor selbst frei.
*
* Die folgenden Funktionen geben -1 im Fehlerfall und 0 im Erfolgsfall zurück.
*/
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);
/*
* Startet die Ereignisschleife mit Timeout `timeout`.
*
* Diese Funktion übergibt die Kontrolle an den aufrufenden Code, wenn die vorgegebene
* Zeit abgelaufen ist oder wenn keine registrierten Sockets vorhanden sind.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Die Struktur des I/O-Reaktors besteht aus Selektor und , die jeden Socket mit CallbackData verknüpft (eine Struktur aus dem Ereignis-Handler und einem Argument für diesen).
Zeige Reactor und CallbackData
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Bitte beachten Sie, dass wir die Möglichkeit der Behandlung mit über einen Zeiger genutzt haben. In reactor.h deklarieren wir die Struktur reactor, sondern in reactor.c und definieren sie, wobei wir verhindern, dass der Benutzer ihre Felder explizit verändert. Dies ist ein Muster der , das sich elegant in die Semantik von C einfügt.
Funktionen reactor_register, reactor_deregister und reactor_reregister aktualisieren die Liste der interessierenden Sockets und der entsprechenden Ereignis-Handler im systemeigenen Selektor und in der Hash-Tabelle.
Zeige Registrierungsfunktionen
#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;
}Nachdem der I/O-Reaktor ein Ereignis mit dem Deskriptor abgefangen hat, ruft er den entsprechenden Ereignis-Handler auf und übergibt fdeine Bitmaske fd, Zeige die Funktion reactor_run() void.
Die Funktion reactor_run() anzeigen
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) {
// Fehler
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Zeitüberschreitung
case 0:
result = 0;
goto cleanup;
// Erfolgreiche Operation
default:
// Ereignis-Handler aufrufen
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Zusammenfassend wird die Kette der Funktionsaufrufe im Benutzer-Code wie folgt aussehen:

Einstufiger Server
Um den I/O-Reaktor unter hoher Last zu testen, werden wir einen einfachen HTTP-Webserver schreiben, der auf jede Anfrage mit einem Bild antwortet.
Kurze Einführung in das HTTP-Protokoll
— ist ein Protokoll , das hauptsächlich zur Interaktion zwischen Server und Browser verwendet wird.
HTTP kann problemlos über Protokolls , Nachrichten im festgelegten Format senden und empfangen, das durch .
Anforderungsformat
CRLF
CRLF
CRLF
CRLF CRLFCRLF— ist eine Sequenz aus zwei Zeichen:rundn, die die erste Anfragezeile, die Kopfzeilen und die Daten trennt.<КОМАНДА>— eine derCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Der Browser wird unserem Server den Befehl senden,GET, was bedeutet: „Schicke mir den Inhalt der Datei“.<URI>— . Zum Beispiel, wenn URI =/index.html, fragt der Client die Hauptseite der Website an.<ВЕРСИЯ HTTP>— die Version des HTTP-Protokolls im FormatHTTP/X.Y. Die am häufigsten verwendete Version heute istHTTP/1.1.— ist ein Schlüssel-Wert-Paar im Format:, das an den Server zur weiteren Analyse gesendet wird.— Daten, die der Server zur Ausführung der Operation benötigt. Häufig sind dies einfach oder ein anderes Format.
Antwortformat
CRLF
CRLF
CRLF
CRLF CRLF<КОД СТАТУСА>— ist die Zahl, die das Ergebnis der Operation darstellt. Unser Server wird immer den Status 200 (erfolgreiche Operation) zurückgeben.<ОПИСАНИЕ СТАТУСА>— die stringbasierte Darstellung des Statuscodes. Für den Statuscode 200 ist diesOK.— die Überschrift im gleichen Format wie in der Anfrage. Wir werden die Header zurückgebenContent-Length(Dateigröße) undContent-Type: text/html(Typ der zurückgegebenen Daten).— die vom Benutzer angeforderten Daten. In unserem Fall ist dies der Pfad zum Bild in .
Die Datei (einzelner Server) umfasst die Datei , die die folgenden Funktionsprototypen enthält:
Funktionsprototypen in common.h anzeigen
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов принять новое соединение.
*/
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);Es ist auch das funktionale Makro SAFE_CALL() und die Funktion fail(). Das Makro vergleicht den Wert des Ausdrucks mit einem Fehler, und wenn die Bedingung erfüllt ist, ruft es die Funktion auf fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funktion fail() druckt die übergebenen Argumente im Terminal aus (wie ) und beendet das Programm mit dem Code 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);
}Funktion new_server() gibt einen Dateideskriptor für den "Server"-Socket zurück, der durch Systemaufrufe erstellt wurde , und und in der Lage ist, eingehende Verbindungen im nicht-blockierenden Modus anzunehmen.
Funktion new_server() anzeigen
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;
}- Bitte beachten Sie, dass der Socket zunächst im nicht-blockierenden Modus mit dem Flag
SOCK_NONBLOCK, erstellt wird, sodass der Systemaufruf in der Funktionon_accept()nicht die Ausführung des Threads stoppt.accept()reuse_port - Wenn
ist gleich, dann wird diese Funktion den Socket mit der OptiontrueSO_REUSEPORT setsockopt() Der Ereignis-Handler
wird aufgerufen, nachdem das Betriebssystem ein Ereignis erzeugt hat on_accept() EPOLLIN , was in diesem Fall bedeutet, dass eine neue Verbindung angenommen werden kann.nimmt eine neue Verbindung an, schaltet sie in den nicht-blockierenden Modus und registriert sie beim Ereignis-Handler on_accept() on_recv() im I/O-Reaktor. im I/O-Reaktor.
Zeige die Funktion 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);
}wird aufgerufen, nachdem das Betriebssystem ein Ereignis erzeugt hat im I/O-Reaktor. EPOLLIN , was in diesem Fall bedeutet, dass eine neue Verbindung angenommen werden kann., was in diesem Fall bedeutet, dass die registrierte Verbindung on_accept(), bereit ist, Daten zu empfangen.
im I/O-Reaktor. liest Daten von der Verbindung, bis der HTTP-Anfrage vollständig empfangen ist, danach registriert sie den Handler on_send() zum Senden der HTTP-Antwort. Wenn der Client die Verbindung abbricht, wird der Socket deregistriert und schließt sich durch .
Zeige die Funktion on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Empfange Eingabedaten, solange recv 0 oder einen Fehler zurückgibt
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// Der Client hat die Verbindung abgebrochen
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read hat einen anderen Fehler als einen Fehler zurückgegeben, der den Aufruf blockiert
// den Thread
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Vollständige HTTP-Anfrage vom Client empfangen. Jetzt registrieren wir den Handler
// für Ereignisse zum Senden von Daten
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}wird aufgerufen, nachdem das Betriebssystem ein Ereignis erzeugt hat on_send() EPOLLIN EPOLLOUT, was bedeutet, dass die registrierte Verbindung im I/O-Reaktor., bereit ist, Daten zu senden. Diese Funktion sendet die HTTP-Antwort, die HTML mit einem Bild enthält, an den Client und ändert dann den Ereignishandler wieder auf im I/O-Reaktor..
Zeige die Funktion 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);
}Und schließlich erstellen wir in der Datei http_server.c, in der Funktion main() einen I/O-Reaktor mit Hilfe von reactor_new(), erstellen einen Server-Socket und registrieren ihn, starten den Reaktor mit reactor_run() genau für eine Minute, und dann geben wir die Ressourcen frei und beenden das Programm.
Zeige 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);
}Überprüfen wir, ob alles wie erwartet funktioniert. Wir kompilieren (chmod a+x compile.sh && ./compile.sh im Stammverzeichnis des Projekts) und starten den eigenen Server, öffnen das im Browser und beobachten, was wir erwartet haben:

Leistungsmessung
Zeige die Spezifikationen meines Rechners
$ 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
.MMMMMMMMMMMMMMMMMMMMessen wir die Leistung eines Single-Thread-Servers. Öffnen wir zwei Terminals: in einem starten wir ./http_server, im anderen — . Nach einer Minute erscheint im zweiten Terminal die folgende Statistik:
$ 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.19MBUnser Single-Thread-Server konnte über 11 Millionen Anfragen pro Minute verarbeiten, die von 100 Verbindungen stammen. Ein gutes Ergebnis, aber kann man es verbessern?
Multithread-Server
Wie bereits erwähnt, kann ein I/O-Reaktor in separaten Threads erstellt werden, wodurch alle CPU-Kerne genutzt werden. Lassen Sie uns diesen Ansatz in der Praxis anwenden:
Zeige 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);
}
}Jetzt hat jeder Thread Reaktor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Beachten Sie, dass das Argument der Funktion new_server() darstellt true. Das bedeutet, dass wir dem Server-Socket die Option zuweisen, um ihn in einer Multithread-Umgebung zu verwenden. Näheres können Sie lesen .
Zweiter Versuch
Jetzt messen wir die Leistung des Multithread-Servers:
$ 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.14MBDie Anzahl der verarbeiteten Anfragen pro Minute ist um ~3,28-fach gestiegen! Aber es fehlten nur ~zwei Millionen zum runden Betrag. Lassen Sie uns das versuchen zu verbessern.
Zuerst schauen wir uns die Statistik an, die von :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses .\/http_server_multithreaded\n\n Leistungszählersstats für '.\/http_server_multithreaded':\n\n 242446,314933 task-clock (msec) # 4,000 CPUs genutzt \n 1 813 074 Kontextwechsel # 0,007 M\/sec \n 4 689 CPU-Migrationen # 0,019 K\/sec \n 254 Seitenfehler # 0,001 K\/sec \n 895 324 830 170 Zyklen # 3,693 GHz \n 621 378 066 808 Anweisungen # 0,69 insn pro Zyklus \n 119 926 709 370 Zweige # 494,653 M\/sec \n 3 227 095 669 Branch-Misses # 2,69% aller Zweige \n 808 664 Cache-Misses \n\n 60,604330670 Sekunden vergangene Zeit, Kompilierung mit -march=native, , Erhöhung der Trefferquote in , Erhöhung MAX_EVENTS und Nutzung EPOLLET führte nicht zu einem signifikanten Leistungszuwachs. Aber was passiert, wenn man die Anzahl der gleichzeitigen Verbindungen erhöht?
Statistiken bei 352 gleichzeitigen Verbindungen:
$ 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"\nFühre 1m Test durch @ http:\/\/127.0.0.1:18470\n 8 Threads und 352 Verbindungen\n Thread-Stats Avg Stdev Max +\/ - Stdev\n Latenz 2.12ms 3.79ms 68.23ms 87.49%\n Req\/Sec 83.78k 12.69k 169.81k 83.59%\n 40006142 Anfragen in 1.00m, 5.48GB gelesen\nAnfragen\/sec: 665789.26\nÜbertragung\/sec: 93.34MBDas gewünschte Ergebnis wurde erzielt, und zusammen damit auch ein interessantes Diagramm, das die Abhängigkeit der verarbeiteten Anfragen pro Minute von der Anzahl der Verbindungen zeigt:

Wir sehen, dass nach ein paar Hundert Verbindungen die Anzahl der verarbeiteten Anfragen bei beiden Servern stark abfällt (bei der Multithread-Variante ist dies deutlicher). Hängt das mit der Implementierung des TCP\/IP-Stacks von Linux zusammen? Teilen Sie Ihre Vermutungen über dieses Verhalten des Diagramms und Optimierungen der Multithread- und Einfaden-Varianten gerne in den Kommentaren mit.
Wie in den Kommentaren, dieser Leistungstest zeigt nicht das Verhalten des I\/O-Reaktors unter realen Lasten, da der Server fast immer mit einer Datenbank interagiert, Protokolle ausgibt, Kryptographie verwendet mit usw., wodurch die Last uneinheitlich (dynamisch) wird. Tests zusammen mit externen Komponenten werden in einem Artikel über den I\/O-Reaktor durchgeführt.
Nachteile des I\/O-Reaktors
Es muss verstanden werden, dass der I\/O-Reaktor nicht ohne Mängel ist, nämlich:
- Die Verwendung des I\/O-Reaktors in einer Mehrfadenumgebung ist etwas komplizierter, da man die Threads manuell verwalten muss.
- Die Praxis zeigt, dass die Last in den meisten Fällen nicht homogen ist, was dazu führen kann, dass ein Thread bearbeitet wird, während ein anderer mit Arbeiten belastet ist.
- Wenn ein Ereignis-Handler den Thread blockiert, blockiert auch der Systemauswahlmechanismus selbst, was zu schwer auffindbaren Bugs führen kann.
Diese Probleme löst , der oft einen Scheduler hat, der die Last gleichmäßig auf den Thread-Pool verteilt, und zudem eine benutzerfreundlichere API bietet. Darüber wird in meinem anderen Artikel später gesprochen.
Fazit
Damit endet unsere Reise von der Theorie direkt zur Profiler-Ausgabe.
Man sollte damit nicht aufhören, denn es gibt viele andere ebenso interessante Ansätze zur Erstellung von Netzwerksoftware mit unterschiedlichem Komfort und Geschwindigkeit. Interessante Links aus meiner Sicht sind unten aufgeführt.
Bis zum nächsten Mal!
Interessante Projekte
- C
Was sonst noch zu lesen?
Quelle: habr.com
