
Einführung
(einzelner Prozess) ) — ein Muster zur Erstellung hochbelastbarer Software, das in vielen beliebten Lösungen verwendet wird:
- …
In diesem Artikel werden wir die Grundlagen des I/O Reaktors und dessen Funktionsweise erläutern, eine Implementierung mit weniger als 200 Codezeilen erstellen und einen einfachen HTTP-Server dazu bringen, über 40 Millionen Anfragen pro Minute zu verarbeiten.
Vorwort
- Der Artikel ist geschrieben, um zu helfen, das Funktionieren des I/O Reaktors zu verstehen und somit die Risiken seiner Nutzung zu erkennen.
- Für das Verständnis des Artikels sind Grundlagenkenntnisse der und ein wenig Erfahrung in der Entwicklung von Netzwerkapplikationen erforderlich.
- Der gesamte Code ist in der Programmiersprache C gemäß demAchtung: langer PDF) für Linux verfasst und ist verfügbar auf .
Warum ist das wichtig?
Mit der zunehmenden Popularität des Internets müssen Webserver eine große Anzahl von Verbindungen gleichzeitig verarbeiten. Daher wurden zwei Ansätze getestet: blockierendes I/O mit einer Vielzahl von Betriebssystem-Threads und nicht-blockierendes I/O in Kombination mit einem Ereignisbenachrichtigungssystem, auch bekannt als "System-Selector" (///etc).
Der erste Ansatz sah vor, für jede eingehende Verbindung einen neuen OS-Thread zu erstellen. Sein Nachteil ist die schlechte Skalierbarkeit: Das Betriebssystem muss viele und . Diese sind kostspielige Vorgänge und können bei einer hohen Anzahl von Verbindungen zu einem Mangel an freiem RAM führen.
Die modifizierte Version weist (Thread-Pool) auf, wodurch das System nicht mehr abstürzen kann. Gleichzeitig bringt dies ein neues Problem mit sich: Wenn der Thread-Pool zurzeit durch langwierige Lesevorgänge blockiert ist, können andere Sockets, die bereit sind, Daten zu empfangen, dies nicht tun.
Der zweite Ansatz nutzt (System-Selector), das das Betriebssystem bereitstellt. In diesem Artikel wird die am häufigsten vorkommende Art des Systemselectors behandelt, die auf Benachrichtigungen (Ereignisse, Notifications) über die Bereitschaft zu I/O-Operationen basiert, und nicht auf . Ein vereinfachtes Beispiel für seine Verwendung könnte durch das folgende Flussdiagramm dargestellt werden:

Der Unterschied zwischen diesen Ansätzen liegt in Folgendem:
- Blockierende I/O-Operationen unterbrechen den Benutzerfluss bis AWS das Gleiche tut und ein erfolgreiches Geschäft darum aufbaut, und wenn die Kunden buchstäblich dasselbe verlangen. Allerdings sind gewisse Anstrengungen erforderlich, um Google dazu zu bringen, etwas zu unterstützen., bis das Betriebssystem die der eintreffenden in einen Byte-Stream (, Datenempfang) oder bis ausreichend Platz in den internen Schreibpuffern für die anschließende Übertragung (Datenversand) freigegeben ist.
- Der System-Selector informiert das Programm nach einer gewissen Zeit darüber, dass das Betriebssystem die IP-Pakete defragmentiert hat (TCP, Datenempfang) oder ausreichend Platz in den internen Schreibpuffern bereits verfügbar ist (Datenversand). bereits Zusammenfassend lässt sich sagen, dass die Reservierung des Betriebssystemflusses für jede I/O eine Verschwendung von Rechenleistung ist, da die Ströme tatsächlich nicht mit nützlicher Arbeit beschäftigt sind (hierher stammt der Begriff
"programmierte Unterbrechung" Das I/O-Reaktor-Modell
Der I/O-Reaktor fungiert als Schicht zwischen dem System-Selector und dem Benutzer-Code. Das Funktionsprinzip ist in folgendem Blockdiagramm dargestellt:
Der I/O-Reaktor fungiert als Vermittlungsschicht zwischen dem Systemauswähler und dem Benutzercode. Das Funktionsprinzip ist in folgendem Flussdiagramm dargestellt:

- Ich erinnere daran, dass ein Ereignis eine Benachrichtigung darüber ist, dass ein bestimmter Socket bereit ist, eine nicht-blockierende I/O-Operation durchzuführen.
- Der Ereignishandler ist eine Funktion, die vom I/O-Reaktor aufgerufen wird, wenn ein Ereignis empfangen wird, und die anschließend eine nicht-blockierende I/O-Operation ausführt.
Es ist wichtig zu beachten, dass der I/O-Reaktor per Definition einseitig ist, aber es steht nichts im Wege, das Konzept in einer mehrschichtigen Umgebung mit dem Verhältnis 1 Thread: 1 Reaktor zu verwenden, wodurch alle CPU-Kerne optimal genutzt werden.
Implementierung
Wir werden die öffentliche Schnittstelle in die Datei , und die Implementierung in . reactor.h bestehen aus den folgenden Deklarationen:
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 System-Selektor eintritt.
*/
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 System-Selektor, alle zum aktuellen Zeitpunkt 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 dem Timeout `timeout`.
*
* Diese Funktion übergibt die Kontrolle an den aufrufenden Code, wenn die
* zugewiesene Zeit abgelaufen ist oder/und 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 (Struktur aus dem Ereignis-Handler und dem dazugehörigen Benutzer-Argument) zuordnet.
Reactor und CallbackData anzeigen
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 Handhabung von über einen Zeiger. In reactor.h definieren wir die Struktur reactor, sondern in reactor.c und definieren diese, wodurch der Benutzer daran gehindert wird, ihre Felder direkt zu ändern. Dies ist eines der Muster , das sich nahtlos in die Semantik von C einfügt.
Funktionen reactor_register, reactor_deregister und reactor_reregister aktualisieren die Liste der interessierenden Sockets und die entsprechenden Ereignishandler im System-Selector und in der Hashtabelle.
Funktionen zur Registrierung anzeigen
#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 fderfasst hat, ruft er den entsprechenden Ereignishandler auf, dem er fd, der generierten Ereignisse und einen Benutzerzeiger auf void.
zeigt die Funktion 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) {
// Fehler
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Zeitüberschreitung
case 0:
result = 0;
goto cleanup;
// Erfolgreiche Operation
default:
// Ereignishandler aufrufen
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Zusammenfassend wird die Aufrufkette der Funktionen im Benutzercode wie folgt aussehen:

Einzelner Thread-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.
Kurzer Überblick über das HTTP-Protokoll
— ist ein Protokoll , das hauptsächlich für die Interaktion zwischen Server und Browser verwendet wird.
HTTP kann problemlos über entwickelten Protokolls , indem Sie Nachrichten im festgelegten Format senden und empfangen. .
Anfrageformat
CRLF
CRLF
CRLF
CRLF CRLF CRLF— ist eine Zeichenfolge aus zwei Symbolen:rundn, die die erste Zeile der Anfrage, die Header und die Daten trennt.<КОМАНДА>— ist eines derCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,eine,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, fordert der Client die Hauptseite der Webseite an.<ВЕРСИЯ HTTP>— die Version des HTTP-Protokolls im FormatHTTP/X.Y. Die heute am häufigsten verwendete Version istHTTP/1.1.— ist ein Paar aus Schlüssel-Wert im Format:, das an den Server gesendet wird, um weiter analysiert zu werden.— die Daten, die der Server benötigt, um die Operation auszuführen. Oft handelt es sich einfach um oder ein anderes Format.
Antwortformat
CRLF
CRLF
CRLF
CRLF CRLF <КОД СТАТУСА>— ist eine Zahl, die das Ergebnis der Operation darstellt. Unser Server wird immer den Status 200 zurückgeben (erfolgreiche Operation).<ОПИСАНИЕ СТАТУСА>— eine stringbasierte Darstellung des Statuscodes. Für den Statuscode 200 ist diesOK.— ein Header im selben Format wie in der Anfrage. Wir werden die HeaderContent-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 Thread-Server) umfasst die Datei , die folgende 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);Ebenso wird das Funktionsmakro SAFE_CALL() und die Funktion fail()definiert. Das Makro vergleicht den Wert des Ausdrucks mit einem Fehler und ruft die Funktion auf, wenn die Bedingung erfüllt ist, fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Die Funktion fail() gibt 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);
}Die Funktion new_server() gibt einen Dateideskriptor für das 'Server'-Socket zurück, das durch Systemaufrufe erstellt wurde , und und in der Lage, 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_NONBLOCKerstellt wird, damit der Systemaufrufon_accept()(weiterlesen) nicht die Ausführung des Threads stoppt.accept()reuse_port - Wenn
, wird diese Funktion den Socket mit der Optiongleich isttrueSO_REUSEPORT setsockopt() Der Ereignishandler
wird aufgerufen, nachdem das Betriebssystem ein Ereignis on_accept() EPOLLIN generiert hat, was in diesem Fall bedeutet, dass eine neue Verbindung akzeptiert werden kann.akzeptiert eine neue Verbindung, wechselt sie in den nicht blockierenden Modus und registriert sie bei dem Ereignishandler. on_accept() nimmt eine neue Verbindung an, schaltet sie in den nicht-blockierenden Modus und registriert sie mit dem Ereignis-Handler on_recv() 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 on_recv() EPOLLIN generiert hat, was in diesem Fall bedeutet, dass eine neue Verbindung akzeptiert werden kann., was in diesem Fall bedeutet, dass die registrierte Verbindung on_accept(), bereit ist, Daten zu empfangen.
on_recv() liest Daten aus der Verbindung, bis die HTTP-Anfrage vollständig empfangen ist, dann registriert sie den Handler on_send() zum Senden der HTTP-Antwort. Wenn der Client die Verbindung trennt, wird der Socket deregistriert und durch .
Zeige die Funktion on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Wir empfangen 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 unterbrochen
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read hat einen Fehler zurückgegeben, der nicht die Blockierungsbedingung ist
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Ein vollständiger HTTP-Antrag vom Client wurde empfangen. Jetzt registrieren wir den Handler
// für Ereignisse zur Datenübertragung
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 on_send() EPOLLIN EPOLLOUT, was bedeutet, dass die registrierte Verbindung on_recv(), bereit ist, Daten zu senden. Diese Funktion sendet die HTTP-Antwort, die HTML mit Bildern enthält, an den Client und ändert dann den Ereignis-Handler zurück zu on_recv().
zeige die Funktion on_send()
statische 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 im Datei http_server.c, in der Funktion main() erstellen wir den I/O-Reaktor mithilfe von reactor_new(), erstellen einen Serversocket und registrieren ihn, starten den Reaktor mit Hilfe von reactor_run() genau für eine Minute, dann geben wir die Ressourcen frei und beenden das Programm.
http_server.c anzeigen
#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 ordnungsgemäß funktioniert. Wir kompilieren (chmod a+x compile.sh && ./compile.sh im Wurzelverzeichnis des Projekts) und starten unseren Server, öffnen im Browser und beobachten das, was wir erwartet haben:

Leistungsbenchmark
Die Eigenschaften meines Systems anzeigen
$ 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: Pakete: 2217
ddddMMh dMM :hNMNMNhNMNMNh: `NMm Shell: bash 4.4.20
NMm dMM .NMN/-+MMM+-/NMN` dMM Auflösung: 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-Thema: Mint-Y-Dark (Mint-Y)
NMm dMM` ..` ... ydm. dMM GTK-Thema: Mint-Y [GTK2/3]
hMM- +MMd/-------...-:sdds dMM Icon-Thema: Mint-Y
-NMm- :hNMNNNmdddddddddy/` dMM Schriftart: Noto Sans 9
-dMNs-``-::::-------.`` dMM CPU: Intel Core i7-6700 @ 8x 4GHz [52.0°C]
`/dMNmy+/:-------------:/yMMM GPU: NV136
./ydNMMMMMMMMMMMMMMMMMMMMM RAM: 2544MiB / 7926MiB
.MMMMMMMMMMMMMMMMMMMWir messen die Leistung des Single-Thread-Servers. Öffnen wir zwei Terminals: in einem starten wir ./http_server, im anderen — . Nach einer Minute werden im zweiten Terminal die folgenden Statistiken angezeigt:
$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: de-DE,de;q=0.5" -H "Connection: keep-alive"
Eine 1-minütige Testlauf bei http://127.0.0.1:18470
8 Threads und 100 Verbindungen
Thread-Statistiken Durchschnitt Abstand Maximal +/- Abstand
Latenz 493.52µs 76.70µs 17.31ms 89.57%
Anfragen/Sek. 24.37k 1.81k 29.34k 68.13%
11657769 Anfragen in 1.00m, 1.60GB gelesen
Anfragen/Sek.: 193974.70
Übertragung/Sek.: 27.19MBUnser einthreadiger Server konnte über 11 Millionen Anfragen pro Minute aus 100 Verbindungen verarbeiten. Ein nicht schlechter Wert, aber können wir ihn noch verbessern?
Multithread-Server
Wie bereits erwähnt, kann ein I/O-Reaktor in separaten Threads erstellt werden, wodurch gleichzeitig alle CPU-Kerne genutzt werden. Lassen Sie uns diesen Ansatz in der Praxis anwenden:
http_server_multithreaded.c anzeigen
#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 besitzt jeder Thread Reaktor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Bitte beachten Sie, dass das Argument der Funktion new_server() wird unterstützt von truebedeutet, dass wir dem Server-Socket die Option zuweisen , um ihn in einer multithreaded Umgebung zu verwenden. Weitere Informationen finden Sie .
Zweite Runde
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: de-DE,de;q=0.5" -H "Connection: keep-alive"
Führe 1m Test @ http://127.0.0.1:18470 aus
8 Threads und 100 Verbindungen
Thread Statistiken Ø Std Max +/- Std
Latenz 1,14ms 2,53ms 40,73ms 89,98%
Anfragen/Sek 79,98k 18,07k 154,64k 78,65%
38208400 Anfragen in 1,00m, 5,23GB gelesen
Anfragen/Sek: 635876,41
Übertragung/Sek: 89,14MBDie Anzahl der verarbeiteten Anfragen pro Minute ist um etwa 3,28 Mal gestiegen! Es fehlten nur etwa zwei Millionen, wir werden versuchen, dies zu beheben.
Zuerst schauen wir uns die generierte Statistik an :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded
Leistungszähler-Statistiken für './http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPUs genutzt
1 813 074 Kontextwechsel # 0,007 M/Sek
4 689 CPU-Migrationen # 0,019 K/Sek
254 Seitenfehler # 0,001 K/Sek
895 324 830 170 Zyklen # 3,693 GHz
621 378 066 808 Anweisungen # 0,69 Instruktionen pro Zyklus
119 926 709 370 Zweige # 494,653 M/Sek
3 227 095 669 Zweig-Fehler # 2,69% aller Zweige
808 664 Cache-Fehler
60,604330670 Sekunden vergangene Zeit, kompilieren mit -march=native, , die Anzahl der Treffer erhöht sich in , eine Steigerung MAX_EVENTS und Nutzung von EPOLLET brachte keinen signifikanten Leistungszuwachs. Doch was passiert, wenn wir die Anzahl der gleichzeitigen Verbindungen erhöhen?
Statistik 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"
Testlauf von 1 Minute @ http://127.0.0.1:18470
8 Threads und 352 Verbindungen
Thread-Statistik Durchschnitt Stdabweichung Max +/- Stdabweichung
Latenz 2,12ms 3,79ms 68,23ms 87,49%
Anfragen/Sek. 83,78k 12,69k 169,81k 83,59%
40006142 Anfragen in 1,00m, 5,48GB gelesen
Anfragen/Sek.: 665789,26
Übertragungsrate/Sek.: 93,34MBDas gewünschte Ergebnis wurde erzielt, und damit ein interessanter Graph, der die Abhängigkeit der verarbeiteten Anfragen pro Minute von der Anzahl der Verbindungen zeigt:

Wir sehen, dass die Anzahl der verarbeiteten Anfragen bei beiden Servern nach ein paar Hundert Verbindungen drastisch abnimmt (bei der Multithread-Variante ist dies deutlicher). Hängt dies mit der Implementierung des TCP/IP-Stacks unter Linux zusammen? Teilen Sie Ihre Vermutungen über das Verhalten des Graphen und Optimierungen der Multithread- und Einzelthread-Varianten gerne in den Kommentaren.
Wie In den Kommentaren zeigt dieser Leistungstest nicht das Verhalten des I/O-Reactors unter realen Bedingungen, da der Server fast immer mit der Datenbank interagiert, Protokolle ausgibt und Kryptografie verwendet mit usw., was dazu führt, dass die Last ungleichmäßig (dynamisch) wird. Tests zusammen mit Drittanbieterkomponenten werden in einem Artikel über den I/O-Operator durchgeführt.
Nachteile des I/O-Reactors
Es ist wichtig zu verstehen, dass der I/O-Reactor nicht ohne Mängel ist, nämlich:
- Die Nutzung des I/O-Reactors in einer mehrseitigen Umgebung ist etwas komplizierter, da man die Threads manuell verwalten muss.
- Die Praxis zeigt, dass in den meisten Fällen die Last ungleichmäßig ist, was dazu führen kann, dass ein Thread aktiv ist, während ein anderer mit Arbeit beschäftigt ist.
- Wenn ein Ereignis-Handler einen Thread blockiert, wird auch der System-Selector blockiert, was zu schwer nachvollziehbaren Bugs führen kann.
Diese Probleme werden gelöst durch , der oft einen Scheduler hat, der die Last gleichmäßig auf einen Thread-Pool verteilt und außerdem eine benutzerfreundlichere API bietet. Darüber wird später in einem meiner anderen Artikel gesprochen.
Fazit
Damit endet unsere Reise von der Theorie direkt zur Profilergebnis-Ausgabe.
Aber das ist nur der Anfang, denn es gibt viele andere interessante Ansätze zur Entwicklung von Netzwerksoftware mit unterschiedlichen Komfort- und Geschwindigkeitsniveaus. Einige interessante Links finden Sie unten.
Bis zum nächsten Mal!
Interessante Projekte
- C
Was gibt es sonst zu lesen?
Quelle: habr.com
