
Einführung
(Einzelgewinde ) ist ein Muster zum Schreiben von Hochlastsoftware, das in vielen gängigen Lösungen verwendet wird:
- ...
In diesem Artikel werden wir uns mit den Besonderheiten eines I/O-Reaktors und seiner Funktionsweise befassen, eine Implementierung in weniger als 200 Codezeilen schreiben und einen einfachen HTTP-Server dazu bringen, über 40 Millionen Anfragen/Minute zu verarbeiten.
Vorwort
- Der Artikel wurde geschrieben, um zu helfen, die Funktionsweise des I/O-Reaktors zu verstehen und damit die Risiken bei seiner Verwendung zu verstehen.
- Zum Verständnis des Artikels sind Grundkenntnisse erforderlich. und etwas Erfahrung in der Entwicklung von Netzwerkanwendungen.
- Der gesamte Code ist in C-Sprache geschrieben, streng nach (Achtung: langes PDF) für Linux und verfügbar auf .
Warum das?
Mit der wachsenden Beliebtheit des Internets mussten Webserver zunehmend eine große Anzahl von Verbindungen gleichzeitig verarbeiten, weshalb zwei Ansätze ausprobiert wurden: das Blockieren von E/A auf einer großen Anzahl von Betriebssystem-Threads und das nicht-blockierende E/A in Kombination mit ein Ereignisbenachrichtigungssystem, auch „Systemselektor“ genannt (///usw).
Der erste Ansatz bestand darin, für jede eingehende Verbindung einen neuen Betriebssystem-Thread zu erstellen. Sein Nachteil ist die schlechte Skalierbarkeit: Das Betriebssystem muss viele implementieren и . Dies sind teure Vorgänge und können bei einer beeindruckenden Anzahl von Verbindungen zu einem Mangel an freiem RAM führen.
Die modifizierte Version hebt hervor (Thread-Pool), wodurch verhindert wird, dass das System die Ausführung abbricht, aber gleichzeitig ein neues Problem entsteht: Wenn ein Thread-Pool derzeit durch lange Lesevorgänge blockiert ist, können andere Sockets, die bereits Daten empfangen können, dies nicht tun Sie dies.
Der zweite Ansatz verwendet (Systemselektor), der vom Betriebssystem bereitgestellt wird. In diesem Artikel wird der gebräuchlichste Typ von Systemselektoren erläutert, der auf Warnungen (Ereignissen, Benachrichtigungen) über die Bereitschaft für E/A-Vorgänge basiert und nicht auf . Ein vereinfachtes Anwendungsbeispiel kann durch das folgende Blockdiagramm dargestellt werden:

Der Unterschied zwischen diesen Ansätzen ist wie folgt:
- Blockieren von E/A-Vorgängen aussetzen Benutzerfluss bis dahinbis das Betriebssystem ordnungsgemäß funktioniert eingehend zum Byte-Stream (, Empfangen von Daten) oder es ist nicht genügend Platz in den internen Schreibpuffern für das anschließende Senden über verfügbar (Senden von Daten).
- Systemauswahl im Laufe der Zeit Benachrichtigt das Programm, dass das Betriebssystem bereits defragmentierte IP-Pakete (TCP, Datenempfang) oder genügend Platz in internen Schreibpuffern bereits verfügbar (Senden von Daten).
Zusammenfassend lässt sich sagen, dass das Reservieren eines Betriebssystem-Threads für jede E/A eine Verschwendung von Rechenleistung ist, da die Threads in Wirklichkeit keine nützliche Arbeit leisten (daher kommt der Begriff). ). Der Systemselektor löst dieses Problem und ermöglicht es dem Benutzerprogramm, CPU-Ressourcen wesentlich sparsamer zu nutzen.
I/O-Reaktormodell
Der I/O-Reaktor fungiert als Schicht zwischen dem Systemselektor und dem Benutzercode. Das Funktionsprinzip wird durch das folgende Blockdiagramm beschrieben:

- Ich möchte Sie daran erinnern, dass ein Ereignis eine Benachrichtigung ist, dass ein bestimmter Socket einen nicht blockierenden E/A-Vorgang ausführen kann.
- Ein Event-Handler ist eine Funktion, die vom I/O-Reaktor aufgerufen wird, wenn ein Ereignis empfangen wird, die dann einen nicht blockierenden I/O-Vorgang ausführt.
Es ist wichtig zu beachten, dass ein I/O-Reaktor per Definition ein Single-Thread-Reaktor ist, aber nichts hindert das Konzept daran, das Konzept in einer Multi-Thread-Umgebung mit einem Verhältnis von 1 Thread: 1 Reaktor zu verwenden, wodurch alle CPU-Kerne recycelt werden.
Implementierung
Wir werden die öffentliche Schnittstelle in einer Datei ablegen , und Umsetzung - in . reactor.h wird aus folgenden Ankündigungen bestehen:
Deklarationen in „reactor.h“ anzeigen
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);Die I/O-Reaktorstruktur besteht aus Wähler и , der jeden Socket zuordnet CallbackData (Struktur eines Event-Handlers und eines Benutzerarguments dafür).
Reaktor- 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 zur Bearbeitung aktiviert haben laut Index. IN reactor.h Wir deklarieren die Struktur reactorUnd in reactor.c Wir definieren es und verhindern so, dass der Benutzer seine Felder explizit ändert. Dies ist eines der Muster , was prägnant in die C-Semantik passt.
Funktionen reactor_register, reactor_deregister и reactor_reregister Aktualisieren Sie die Liste der relevanten Sockets und der entsprechenden Ereignishandler im Systemselektor und in der Hash-Tabelle.
Registrierungsfunktionen 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 das Ereignis mit dem Deskriptor abgefangen hat fd, ruft es den entsprechenden Event-Handler auf, an den es übergeben wird fd, generierte Ereignisse und einen Benutzerzeiger auf void.
Funktion „reaktor_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) {
// Ошибка
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;
}Zusammenfassend sieht die Kette von Funktionsaufrufen im Benutzercode wie folgt aus:

Single-Threaded-Server
Um den I/O-Reaktor unter hoher Last zu testen, schreiben wir einen einfachen HTTP-Webserver, der auf jede Anfrage mit einem Bild antwortet.
Eine kurze Referenz zum HTTP-Protokoll
- Das ist das Protokoll , wird hauptsächlich für die Server-Browser-Interaktion verwendet.
HTTP kann problemlos verwendet werden Protokoll , Senden und Empfangen von Nachrichten in einem angegebenen Format .
Anfrageformat
<КОМАНДА> <URI> <ВЕРСИЯ HTTP>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ>CRLFist eine Folge von zwei Zeichen:rиn, wobei die erste Zeile der Anfrage, Header und Daten getrennt werden.<КОМАНДА>- einer vonCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Der Browser sendet einen Befehl an unseren ServerGET, was „Senden Sie mir den Inhalt der Datei“ bedeutet.<URI>- . Wenn beispielsweise URI =/index.html, dann fordert der Client die Hauptseite der Site an.<ВЕРСИЯ HTTP>— Version des HTTP-Protokolls im FormatHTTP/X.Y. Die heute am häufigsten verwendete Version istHTTP/1.1.<ЗАГОЛОВОК N>ist ein Schlüssel-Wert-Paar im Format<КЛЮЧ>: <ЗНАЧЕНИЕ>, zur weiteren Analyse an den Server gesendet.<ДАННЫЕ>– Daten, die der Server zur Ausführung des Vorgangs benötigt. Oft ist es einfach oder ein anderes Format.
Antwortformat
<ВЕРСИЯ HTTP> <КОД СТАТУСА> <ОПИСАНИЕ СТАТУСА>CRLF
<ЗАГОЛОВОК 1>CRLF
<ЗАГОЛОВОК 2>CRLF
<ЗАГОЛОВОК N>CRLF CRLF
<ДАННЫЕ><КОД СТАТУСА>ist eine Zahl, die das Ergebnis der Operation darstellt. Unser Server wird immer den Status 200 (erfolgreicher Vorgang) zurückgeben.<ОПИСАНИЕ СТАТУСА>— String-Darstellung des Statuscodes. Für den Statuscode 200 ist dies der FallOK.<ЗАГОЛОВОК N>— Header im gleichen Format wie in der Anfrage. Wir werden die Titel zurückgebenContent-Length(Dateigröße) undContent-Type: text/html(Rückgabedatentyp).<ДАННЫЕ>— Vom Benutzer angeforderte Daten. In unserem Fall ist dies der Pfad zum Bild in .
Datei (Single-Threaded-Server) enthält Datei , das 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);Das Funktionsmakro wird ebenfalls beschrieben SAFE_CALL() und die Funktion ist definiert fail(). Das Makro vergleicht den Wert des Ausdrucks mit dem Fehler und ruft die Funktion auf, wenn die Bedingung wahr ist fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funktion fail() gibt die übergebenen Argumente an das 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 den Dateideskriptor des durch Systemaufrufe erstellten „Server“-Sockets zurück , и und in der Lage, eingehende Verbindungen in einem nicht blockierenden Modus zu akzeptieren.
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;
}- Beachten Sie, dass der Socket zunächst im nicht blockierenden Modus mithilfe des Flags erstellt wird
SOCK_NONBLOCKdamit in der Funktionon_accept()(weiterlesen) Systemaufrufaccept()hat die Thread-Ausführung nicht gestoppt. - wenn
reuse_portisttrue, dann konfiguriert diese Funktion den Socket mit der Option mittels um denselben Port in einer Multithread-Umgebung zu verwenden (siehe Abschnitt „Multithread-Server“).
Ereignishandler on_accept() Wird aufgerufen, nachdem das Betriebssystem ein Ereignis generiert hat EPOLLIN, was in diesem Fall bedeutet, dass die neue Verbindung angenommen werden kann. on_accept() akzeptiert eine neue Verbindung, schaltet sie in den nicht blockierenden Modus und registriert sich bei einem Event-Handler on_recv() in einem I/O-Reaktor.
Funktion on_accept() anzeigen
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);
}Ereignishandler on_recv() Wird aufgerufen, nachdem das Betriebssystem ein Ereignis generiert hat EPOLLIN, was in diesem Fall bedeutet, dass die Verbindung registriert wurde on_accept(), bereit zum Datenempfang.
on_recv() liest Daten aus der Verbindung, bis die HTTP-Anfrage vollständig empfangen wurde, und registriert dann einen Handler on_send() um eine HTTP-Antwort zu senden. Wenn der Client die Verbindung unterbricht, wird der Socket deregistriert und mit geschlossen .
Funktion on_recv() anzeigen
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);
}
}Ereignishandler on_send() Wird aufgerufen, nachdem das Betriebssystem ein Ereignis generiert hat EPOLLOUT, was bedeutet, dass die Verbindung registriert wurde on_recv(), bereit zum Senden von Daten. Diese Funktion sendet eine HTTP-Antwort, die HTML mit einem Bild enthält, an den Client und ändert dann den Ereignishandler wieder in on_recv().
Funktion on_send() anzeigen
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 in der Akte http_server.c, in Funktion main() Wir erstellen einen I/O-Reaktor mit reactor_new(), erstellen Sie einen Server-Socket, registrieren Sie ihn und starten Sie den Reaktor mit reactor_run() für genau eine Minute, dann geben wir 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 wie erwartet funktioniert. Kompilieren (chmod a+x compile.sh && ./compile.sh im Projektstamm) und starten Sie den selbstgeschriebenen Server, öffnen Sie ihn im Browser und sehen Sie, was wir erwartet haben:

Leistungsmessung
Zeigen Sie meine Fahrzeugspezifikationen an
$ 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
.MMMMMMMMMMMMMMMMMMMLassen Sie uns die Leistung eines Single-Threaded-Servers messen. Lassen Sie uns zwei Terminals öffnen: In einem werden wir laufen ./http_server, in einem anderen - . Nach einer Minute werden im zweiten Terminal folgende Statistiken angezeigt:
$ 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-Threaded-Server konnte über 11 Millionen Anfragen pro Minute verarbeiten, die von 100 Verbindungen stammten. Kein schlechtes Ergebnis, aber kann es verbessert werden?
Multithread-Server
Wie oben erwähnt, kann der I/O-Reaktor in separaten Threads erstellt werden, wodurch alle CPU-Kerne genutzt werden. Lassen Sie uns diesen Ansatz in die Praxis umsetzen:
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 jeder Thread Reaktor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Bitte beachten Sie, dass das Funktionsargument new_server() Fürsprecher true. Das bedeutet, dass wir die Option dem Server-Socket zuweisen um es in einer Multithread-Umgebung zu verwenden. Weitere Einzelheiten können Sie nachlesen .
Zweiter Lauf
Lassen Sie uns nun die Leistung eines Multithread-Servers messen:
$ 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 in einer Minute verarbeiteten Anfragen stieg um das ~1-fache! Aber wir lagen nur etwa 3.28 Millionen unter der runden Zahl, also versuchen wir, das zu beheben.
Schauen wir uns zunächst die generierten Statistiken an :
$ 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, Zusammenstellung mit -march=native, , eine Erhöhung der Anzahl der Treffer , Zunahme MAX_EVENTS und verwenden EPOLLET brachte keine nennenswerte Leistungssteigerung. Aber was passiert, wenn Sie die Anzahl gleichzeitiger Verbindungen erhöhen?
Statistik für 352 gleichzeitige 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"
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.34MBDas gewünschte Ergebnis wurde erzielt und damit ein interessantes Diagramm, das die Abhängigkeit der Anzahl der verarbeiteten Anfragen in 1 Minute von der Anzahl der Verbindungen zeigt:

Wir sehen, dass nach ein paar hundert Verbindungen die Anzahl der verarbeiteten Anfragen für beide Server stark abnimmt (in der Multithread-Version ist dies deutlicher spürbar). Hängt das mit der Linux TCP/IP-Stack-Implementierung zusammen? Schreiben Sie Ihre Annahmen zu diesem Verhalten des Diagramms und zu Optimierungen für Multithread- und Single-Thread-Optionen gerne in die Kommentare.
Как In den Kommentaren zeigt dieser Leistungstest nicht das Verhalten des I/O-Reaktors unter realer Belastung, da der Server fast immer mit der Datenbank interagiert, Protokolle ausgibt und Kryptographie verwendet usw., wodurch die Belastung ungleichmäßig (dynamisch) wird. Tests zusammen mit Komponenten von Drittanbietern werden im Artikel zum I/O-Proaktor durchgeführt.
Nachteile des I/O-Reaktors
Sie müssen verstehen, dass der I/O-Reaktor nicht ohne Nachteile ist, nämlich:
- Die Verwendung eines I/O-Reaktors in einer Multithread-Umgebung ist etwas schwieriger, weil Sie müssen die Flüsse manuell verwalten.
- Die Praxis zeigt, dass die Auslastung in den meisten Fällen ungleichmäßig ist, was dazu führen kann, dass ein Thread protokolliert, während ein anderer mit der Arbeit beschäftigt ist.
- Wenn ein Event-Handler einen Thread blockiert, blockiert auch der Systemselektor selbst, was zu schwer zu findenden Fehlern führen kann.
Löst diese Probleme , das oft über einen Scheduler verfügt, der die Last gleichmäßig auf einen Thread-Pool verteilt, und außerdem über eine praktischere API verfügt. Wir werden später in meinem anderen Artikel darüber sprechen.
Fazit
Hier endet unsere Reise von der Theorie direkt in den Profiler-Auspuff.
Darüber sollten Sie sich nicht aufhalten, denn es gibt viele andere ebenso interessante Ansätze zum Schreiben von Netzwerksoftware mit unterschiedlichem Komfort und Geschwindigkeit. Interessante Links sind meiner Meinung nach unten aufgeführt.
Bis wir uns wieder treffen!
Interessante Projekte
- selbst
Was soll ich sonst noch lesen?
Source: habr.com
