Linux Adventskalender Tag 10 Artikel.
Im Bereich Betrieb sowie Forschung und Entwicklung gibt es meines Erachtens viele Möglichkeiten, die Kommunikationsgeschwindigkeit zwischen Computern mit Benchmark-Tools oder eigenen Anwendungen für Softwareexperimente oder Gerätetests und -auswahl zu messen. Andererseits variieren diese Messergebnisse in neueren Hochgeschwindigkeitsnetzwerken wie 10 Gbit / s und 40 Gbit / s stark in Abhängigkeit von der Implementierung des Kommunikations-API-Teils der Anwendung, den Kernelparametern oder den Kompilierungsoptionen. Stellen Sie sie daher für eine genaue Messung richtig ein. / Muss verstehen. Dieser Artikel soll Ihnen einen Überblick über die Funktionsweise des Kernels und der Anwendungen in Ihrem Netzwerk und die wichtigsten Punkte darin geben.
Lassen Sie uns zunächst einen kurzen Blick darauf werfen, wie die heutigen Serverprogramme, die TCP verwenden, erstellt werden. Das Serverprogramm wartet grundsätzlich auf die Anforderung vom Client, verarbeitet die Anforderung, gibt die Antwort an den Client zurück, und das Clientprogramm generiert die Anforderung, sendet die Anforderung und antwortet vom Server. Der Prozess ist zu warten, und obwohl die Reihenfolge unterschiedlich ist, ist die Struktur des Programms fast gleich. Sowohl der Client als auch der Server müssen Anforderungen (Antworten im Fall von Clients) auf mehreren TCP-Verbindungen verarbeiten, die asynchron arbeiten. Verwenden Sie daher ein vom Betriebssystem bereitgestelltes Ereignisüberwachungsframework wie epoll. Das Bild des Programms mit epoll_ * sieht folgendermaßen aus. Den vollständigen Code finden Sie beispielsweise unter hier.
int lfd, epfd, newfd, nevts = 1000;
struct epoll_event ev;
struct epoll_event evts[1000]; /*Erhalten Sie bis zu 1000 Anfragen gleichzeitig*/
struct sockaddr_in sin = {.sin_port = htons(50000), .sin_addr = INADDR_ANY};
char buf[10000]; /*Erhalten Sie 10 KB/Puffer senden*/
/*Warten Sie auf eine neue Verbindung(listen)Steckdose für*/
lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bind(lfd, &sin, sizeof(sin));
listen(listen_fd);
/*Ereignisdateideskriptor zum Warten auf Ereignisse mit mehreren Sockets*/
epfd = epoll_create1(EPOLL_CLOEXE);
/*Registrieren Sie den Listen-Socket im Ereignisdateideskriptor*/
bzero(&ev, sizeof(ev));
ev.events = POLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev);
for (;;) {
int i, nfds;
/*Blockieren Sie bis zu 1000 ms und warten Sie auf ein Ereignis*/
nfds = epoll_wait(epfd, evts, nevts, 1000);
for (i = 0; i < nfds; i++) {
int fd = evts[i].data.fd;
if (fd == lfd) { /*Neue TCP-Verbindung*/
bzero(&ev, sizeof(ev));
newfd = accept(fd);
ev.events = POLLIN;
ev.data.fd = newfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &ev);
} else { /*Anforderungen für vorhandene TCP-Verbindungen*/
read(fd, buf);
/*Verarbeiten Sie die in buf eingelesene Anfrage und bereiten Sie die Antwort auf buf vor*/
write(fd, buf); /*Antwort senden*/
}
}
}
In diesem Programm werden ein Dateideskriptor epfd für die Ereignisüberwachung, ein Socket lfd zum Abhören einer neuen Verbindung und ein Socket für jede TCP-Verbindung angezeigt, für die eine Verbindung hergestellt wurde. epoll_wait () ruft die Ereignisse vom Kernel ab und verarbeitet sie einzeln (für Schleifen, die die Variable i inkrementieren). Neue Verbindungsanforderungen werden an den Listen-Socket (lfd) benachrichtigt, und Anforderungen an vorhandene Verbindungen werden an die Sockets vorhandener Verbindungen benachrichtigt. Für das vorherige Ereignis erstellt der Systemaufruf accept () einen Socket, der der neuen Verbindung entspricht, und registriert ihn in der Liste der von epoll (epoll_ctl ()) behandelten Deskriptoren. Für das letztere Ereignis liest read () die Anforderung für den Zieldeskriptor, unternimmt etwas und schreibt dann () die Antwort. Um mehrere CPU-Kerne zu verwenden, führen Sie diese Ereignisschleife epoll_wait () in einem separaten Thread auf jedem Kern aus.
Serveranwendungen und Bibliotheken wie nginx, memcached, Redis und libuv haben fast dasselbe Programmiermodell. Wenn Sie also ein Programm ausführen, das mehrere TCP-Verbindungen verarbeitet, sollten Sie einen Überblick über dieses Verhalten behalten. Ich denke es ist gut.
Lassen Sie uns als nächstes einen kurzen Blick auf die Funktionsweise des Netzwerkstapels unter Linux werfen. Wenn ein Paket bei der Netzwerkkarte ankommt, leitet die Netzwerkkarte das Paket in den Speicher weiter und unterbricht die CPU. Dadurch wird der aktuell auf dieser CPU ausgeführte Thread vorübergehend gestoppt und stattdessen eine Funktion ausgeführt, die als Interrupt-Handler bezeichnet wird. Genau genommen ist es in Hardware-Interrupts und Software-Interrupts unterteilt, aber hier werden diese Prozesse zusammen als Interrupt-Handler bezeichnet. Der Interrupt-Handler verschiebt beispielsweise das Paket auf den Netzwerkstapel (IP oder TCP) und verarbeitet den Header. Während dieses Prozesses wird bestimmt, ob das Paket an die Anwendung benachrichtigt werden muss (ACK-Paket für SYN / ACK, das eine neue Verbindung darstellt, Paket, das Daten enthält). Wenn die Anwendung für die Paketverarbeitung benachrichtigt werden muss und die Anwendung sich registriert und auf den Deskriptor in epoll_wait () gewartet hat (dh im Fall des obigen Codes), wird die Epoll-Warteschlange (über dem Codebeispiel) Registrieren Sie den Deskriptor und den Ereignisinhalt (POLLIN für den Empfang) in (entsprechend epfd). Auf diese Weise registrierte Ereignisse werden gemeinsam benachrichtigt, wenn epoll_wait (), das von der Anwendung blockiert wird, zurückgegeben wird.
Bisher haben wir kurz gesehen, wie der Kernel ein Paket empfängt und wann die Anwendung die Daten empfängt, aber es gibt viele Parameter, die daran beteiligt sind. In diesem Abschnitt werden anhand von Experimenten die Auswirkungen dieser Parameter auf die Netzwerkleistung erläutert.
Der Server ist Intel Xeon Silver 4110 (2,1 GHz), der Client ist Xeon E5-2690v4 (2,6 GHz) und er ist über eine Intel X540 10 GbE-Netzwerkkarte verbunden. Details werden später beschrieben, aber sofern nicht anders als Grundkonfiguration angegeben, sind Turbo-Boost und Hyper-Threading, CPU-Ruhemodus und Netzfilter deaktiviert, die Anzahl der NIC-Warteschlangen ist eins und in epoll. Die Blockierzeit wird auf Null gesetzt. Als Benchmark-Software funktioniert der Server genauso wie der obige Code Experimentelles Serverprogramm, der Client ist Verwenden Sie das beliebte HTTP-Benchmarking-Tool wrk. wrk stellt weiterhin Anforderungen an den Server und empfängt Antworten über die angegebene Anzahl von TCP-Verbindungen für den angegebenen Zeitraum. Die TCP-Verbindung bleibt gespannt, und die Größe ohne die ausgetauschten HTTP-GET- und OK-TCP / IP / Ethernet-Header beträgt 44 Byte bzw. 151 Byte. Da beide Daten klein sind und in ein Paket passen, haben NIC-Offload-Funktionen wie TSO, LRO und Prüfsummen-Offload keine Auswirkung und sind deaktiviert.
In der vorherigen Diskussion haben wir erwähnt, dass die Netzwerkkarte die CPU unterbricht, aber in schnellen Netzwerken kann die Paketempfangsrate Millionen oder sogar mehrere zehn Millionen Pakete pro Sekunde betragen. Da die Interrupt-Verarbeitung für die aktuelle Anwendung unterbrochen wird, wird bei zu hoher Paketempfangsrate die meiste CPU-Zeit für die Interrupt-Verarbeitung verwendet, oder die Anwendungs- oder Kernel-Verarbeitung wird häufig ausgeführt. Es gibt ein Problem, dass es unterbrochen wird. Dieses Problem wird allgemein als Viehzucht bezeichnet. Da die Verarbeitung eines empfangenen Pakets zehn bis hundert ns dauert, wird berechnet, dass ein CPU-Zyklus von mehreren GHz nur zur Verarbeitung des gesamten Interrupts verwendet wird.
Daher verfügt die Netzwerkkarte über einen Mechanismus, um die Häufigkeit der Unterbrechung der CPU zu verringern. Standardmäßig wird es häufig auf ungefähr einmal pro 1us eingestellt. Das einfache Erhöhen dieses Werts bedeutet jedoch nicht, dass die Anzahl der Interrupts verringert werden kann, und selbst wenn ein neues Paket eintrifft, wird der Interrupt für eine Weile nicht generiert, sodass das Problem besteht, dass die Verzögerung bei niedriger Last zunimmt. Aufgrund des als NAPI bezeichneten Mechanismus deaktiviert der Kernel den Interrupt von der Netzwerkkarte selbst entsprechend der Anzahl der nicht verarbeiteten empfangenen Pakete. Selbst wenn dieser Wert erhöht wird, erhöht sich der Durchsatz nicht einmal und der Empfang neuer Pakete wird verzögert. Es kann auch eine Situation sein wie.
Hier messen wir die Zeit, die benötigt wird, um eine Anfrage über HTTP zu senden und eine Antwort aufgrund der unterschiedlichen Interrupt-Rate zu erhalten. Stellen Sie zunächst nur eine TCP-Verbindung zum Server her und wiederholen Sie dann das Senden von HTTP GET und das Empfangen von HTTP OK. Nachfolgend sind die Befehle und Ergebnisse aufgeführt, wobei die Interrupt-Verzögerung auf der Serverseite auf Null gesetzt ist. "-d 3" repräsentiert die Zeit des Experiments und "-c 1" und "-t 1" repräsentieren eine Verbindung bzw. einen Thread.
root@client:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 24.56us 2.11us 268.00us 97.76%
Req/Sec 38.43k 231.73 38.85k 64.52%
118515 requests in 3.10s, 17.07MB read
Requests/sec: 38238.90
Transfer/sec: 5.51MB
Das Folgende ist das Ergebnis, wenn die Interrupt-Verzögerung auf der Serverseite auf 1us eingestellt ist.
root@client:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 29.16us 2.59us 314.00us 99.03%
Req/Sec 32.53k 193.15 32.82k 74.19%
100352 requests in 3.10s, 14.45MB read
Requests/sec: 32379.67
Transfer/sec: 4.66MB
Wie Sie sehen können, ist das Ergebnis ganz anders.
Versuchen Sie als Nächstes, mehrere parallele TCP-Verbindungen zu verwenden. Mit dem folgenden Befehl stellt der Client 100 TCP-Verbindungen her, sendet eine Anforderung für jede Verbindung mit 100 Threads und empfängt eine Antwort.
Das Folgende ist ohne Unterbrechungsverzögerung der Fall
root@client:~# wrk -d 3 -c 100 -t 100 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 245.83us 41.77us 1.22ms 84.60%
Req/Sec 4.05k 152.00 5.45k 88.90%
1248585 requests in 3.10s, 179.80MB read
Requests/sec: 402774.94
Transfer/sec: 58.00MB
Unten ist die 1us Interrupt-Verzögerung.
root@client:~# wrk -d 3 -c 100 -t 100 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 247.22us 41.20us 1.16ms 84.70%
Req/Sec 4.03k 137.84 5.43k 90.80%
1241477 requests in 3.10s, 178.78MB read
Requests/sec: 400575.95
Transfer/sec: 57.68MB
Die Interrupt-Verzögerung hat nur geringe Auswirkungen. Dies liegt jedoch an der Fähigkeit der Netzwerkkarte, Interrupts selbst zu deaktivieren. Da viele Pakete gleichzeitig ankommen (bis zu 100), deaktiviert der Kernel vorübergehend Interrupts von der Netzwerkkarte, selbst wenn die Interruptfrequenz hoch eingestellt ist.
Ein weiterer Punkt, der hier zu beachten ist, ist, dass die Verzögerung um eine Größenordnung größer ist als wenn keine parallele Verbindung besteht. Dies liegt daran, dass mehrere Ereignisse (eingehende Anforderungen) einzeln in der Ereignisschleife der Anwendung verarbeitet werden. Stellen Sie sich vor, das obige Serverprogramm empfängt bis zu 100 Ereignisse gleichzeitig (bis zu 100 von epoll_wait () zurückgegebene nfds).
Die NIC-Interrupt-Rate ist für den ixgbe-Treiber (Intel X520 und X540 10GbE NIC) und den i40e-Treiber (Intel X710 / XXV710 / XL710 10/25/40 GbE NIC) unterschiedlich.
root@server:~# ethtool -C enp23s0f0 rx-usecs 0 #Keine Interrupt-Verzögerung
Es kann wie folgt eingestellt werden.
Überprüfen Sie beim Messen der Netzwerkverzögerung die NIC-Interrupt-Einstellungen.
Diese Funktion erhöht den Takt eines anderen CPU-Kerns mit hoher Last, wenn die Last einiger CPU-Kerne niedrig ist. Dies ist eigentlich eine sehr ärgerliche Funktion und sollte für Leistungsmessungen deaktiviert werden. Der Grund dafür ist, dass bei der Durchführung eines Skalierbarkeitsexperiments mit der Anzahl der CPU-Kerne der Durchsatz linear von 1 auf 3 von 6 Kernen skaliert wird, dann aber plötzlich die Skalierung stoppt. geschehen. Dies lag natürlich nicht daran, dass das Programm oder der Kernel schlecht war, sondern daran, dass bei einer Anzahl von 1 bis 3 die verbleibenden Kerne im Leerlauf waren und die aktiven Kerne durch Turbo-Boost getaktet wurden. Es gibt viele.
Um den Turbo-Boost zu deaktivieren, können Sie ihn entweder in den BIOS-Einstellungen deaktivieren oder folgende Schritte ausführen:
root@server:~# sh -c "echo 1 >> /sys/devices/system/cpu/intel_pstate/no_turbo"
Wenn Sie Probleme mit der Multi-Core-Skalierbarkeit haben, sollten Sie auch Ihre Turbo-Boost-Einstellungen überprüfen.
Schalten wir es aus, ohne nachzudenken
Um Strom zu sparen, werden moderne CPUs je nach Auslastung in mehrere Ruhephasen versetzt, und die Leistung der Software kann aufgrund der Bewegung zwischen den Ruhezuständen instabil werden. Als scheinbar mysteriöses Phänomen, das beispielsweise im Code wie dem obigen Serverprogramm verursacht wird, steigt die Last moderat an, wenn Sie mehrere parallele Verbindungen anstatt einer einzelnen Verbindung verarbeiten, und die CPU geht nicht in den Ruhezustand. Es kommt vor, dass die vom Client beobachtete Verzögerung kürzer wird. In Abbildung 2 von diesem Dokument ist die Verzögerung für 5 Verbindungen geringfügig geringer als für 1 Verbindung, was versehentlich geschieht. Ich habe vergessen, den Ruhezustand für diese CPU zu deaktivieren.
Um zu verhindern, dass die CPU in den Ruhezustand wechselt, empfiehlt es sich, intel_idle.max_cstate = 0 process.max_cstate = 1 in den Kernel-Boot-Parametern festzulegen (angegeben in Dateien wie grub.cfg und pxelinux.cfg / default). Unten ist ein Auszug aus meiner pxelinux.cfg / default
APPEND ip=::::::dhcp console=ttyS0,57600n8 intel_idle.max_cstate=0 processor.max_cstate=1
Moderne Netzwerkkarten verfügen über mehrere Paketpufferwarteschlangen, von denen jede eine separate CPU unterbrechen kann. Daher hat die Anzahl der Warteschlangen in der Netzwerkkarte einen großen Einfluss auf die Behandlung von Interrupts im Kernel und sollte sorgfältig überprüft werden.
root@c307:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 25.22us 2.03us 259.00us 97.68%
Req/Sec 37.50k 271.83 38.40k 74.19%
115595 requests in 3.10s, 16.65MB read
Requests/sec: 37299.11
Transfer/sec: 5.37MB
Das obige ist das gleiche Experiment wie das, bei dem die Interrupt-Verzögerung im Abschnitt NIC-Interrupt-Verzögerung zuvor auf Null gesetzt wurde, aber die Anzahl der Warteschlangen auf der Server-NIC wurde auf 8 und die Anzahl der Threads und Kerne festgelegt Ist auf 8 eingestellt. Stellen Sie sich als Bild die Ereignisschleife epoll_wait des obigen Serverprogramms vor, die in einem separaten Thread auf jedem Kern ausgeführt wird. Der Grund, warum sich der Durchsatz nicht verbessert, besteht darin, dass in diesem Experiment nur eine Verbindung verwendet wird und die Netzwerkkarte die zu unterbrechende Warteschlange / CPU anhand des Hashwerts des Ports oder der Adresse der Verbindung bestimmt, sodass die gesamte Verarbeitung dieselbe CPU ist. Weil es in einem Thread gemacht wird. Darüber hinaus ist die Verzögerung im Vergleich zum Experiment [Interrupt Delay Section](#NIC Interrupt Delay) mit 1 Thread und 1 Warteschlange (24,56-> 25,22) geringfügig erhöht. Dies zeigt, dass das Aktivieren mehrerer Warteschlangen einen leichten Overhead verursacht. Je nach Experiment ist es daher möglicherweise besser, die Anzahl der Warteschlangen auf eins zu reduzieren.
Wie Sie unten sehen können, werden bei 100 Verbindungen Pakete, die zu verschiedenen Verbindungen gehören, auf mehrere Warteschlangen verteilt, was zu einem skalierten, wenn nicht perfekten Durchsatz führt.
root@client:~# wrk -d 3 -c 100 -t 100 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
100 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 65.68us 120.17us 17.26ms 99.53%
Req/Sec 15.52k 1.56k 28.00k 81.43%
4780372 requests in 3.10s, 688.40MB read
Requests/sec: 1542345.05
Transfer/sec: 222.11MB
Die Anzahl der NIC-Warteschlangen beträgt
ethtool -L enp23s0f0 combined 8 #Anzahl der NIC-Warteschlangen bis 8
Es kann wie folgt eingestellt werden.
Zusammenfassend sollte die NIC-Warteschlange grundsätzlich der Anzahl der von der Anwendung verwendeten Kerne entsprechen. In einigen Fällen kann dies jedoch zu einem Overhead führen. Ändern Sie sie daher nach Bedarf. Beachten Sie natürlich, dass Ihre Anwendung auch so programmiert oder konfiguriert sein muss, dass sie auf mehreren Kernen ausgeführt werden kann.
Der Linux-Kernel bietet einen Mechanismus (Netzfilter) zum Einbinden von Paketen an verschiedenen Stellen im Netzwerkstapel. Diese Hooks werden abhängig von iptables usw. dynamisch aktiviert. Dieser Mechanismus selbst wirkt sich jedoch häufig auf die Leistung aus, unabhängig davon, ob einzelne Hooks aktiviert oder deaktiviert sind. Wenn Sie die Leistung genau messen möchten, deaktivieren Sie CONFIG_NETFILTER und CONFIG_RETPOLINE in Ihrer Kernelkonfiguration, sofern dies nicht erforderlich ist.
Wie im obigen Serverprogramm zu sehen ist, blockieren (Sleep) Anwendungen normalerweise mit epoll_wait () und warten auf Ereignisse. Sie können epoll_wait () einen Wert übergeben, der die Blockierungszeit in ms-Einheiten angibt. Wie oben erwähnt, wird der Interrupt-Handler jedoch ausgeführt, indem der Kontext des laufenden Threads ausgeliehen wird. Wenn also kein laufender Thread vorhanden ist, wenn die CPU einen Interrupt von der Netzwerkkarte empfängt, wird zuerst der schlafende Thread ausgewählt. Sie müssen es aufwecken. Die Operation, die diesen Thread aufweckt, ist mit erheblichem Aufwand verbunden. Das Folgende ist das Ergebnis der Verwendung von epoll_wait () zum Blockieren von 1000 ms (NIC-Interrupt-Verzögerung ist Null).
root@client:~# wrk -d 3 -c 1 -t 1 http://192.168.11.3:60000/
Running 3s test @ http://192.168.11.3:60000/
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 33.56us 3.28us 282.00us 95.50%
Req/Sec 28.42k 192.11 28.76k 54.84%
87598 requests in 3.10s, 12.61MB read
Requests/sec: 28257.46
Transfer/sec: 4.07MB
Im Experiment im Abschnitt NIC-Interrupt-Verzögerung war es 24,56us, sodass wir sehen können, dass die Verzögerung um fast 10us zugenommen hat. Um sicherzustellen, dass bei jeder Unterbrechung der CPU ein Thread ausgeführt wird, ist es in Ordnung, epoll_wait () die Blockierungszeit Null zu übergeben.
In diesem Artikel haben wir verschiedene Parameter vorgestellt, die sich auf die Netzwerkleistung auswirken. Wir hoffen, dass es für Experimente von Serveradministratoren, Anwendungsentwicklern und solchen nützlich sein wird, die Netzwerkstacks und (Bibliotheks-) Betriebssysteme (planen).
Recommended Posts