Es ist bekannt, dass NGINX einen asynchronen, ereignisgesteuerten Ansatz zur Handhabung von Verbindungen verwendet. Dies bedeutet, dass nicht für jede Anforderung ein weiterer dedizierter Prozess oder Thread erstellt werden muss (wie bei Servern mit herkömmlicher Architektur), sondern mehrere Verbindungen und Anforderungen in einem einzigen Arbeitsprozess verarbeitet werden. Um dies zu erreichen, arbeitet NGINX mit Sockets in einem nicht blockierenden Modus und verwendet effiziente Methoden wie epoll und kqueue .
Da die Anzahl der Prozesse mit voller Gewichtung gering (normalerweise nur einer pro CPU-Kern) und konstant ist, wird viel weniger Speicher verbraucht und es werden keine CPU-Zyklen durch das Wechseln zwischen Aufgaben verschwendet. Die Vorteile eines solchen Ansatzes sind am Beispiel von NGINX selbst bekannt. Es verarbeitet erfolgreich Millionen gleichzeitiger Anfragen und ist sehr gut skalierbar.
Der asynchrone, ereignisgesteuerte Ansatz hat jedoch immer noch ein Problem. Oder, wie ich es gerne nenne, ein „Feind“. Und der Name des Feindes lautet: Blockieren . Leider verwenden viele Module von Drittanbietern blockierende Aufrufe und die Benutzer (und manchmal sogar die Entwickler der Module) sind sich der Nachteile nicht bewusst. Blockierende Vorgänge können die Leistung von NGINX beeinträchtigen und müssen um jeden Preis vermieden werden.
Selbst im aktuellen offiziellen NGINX-Code ist es nicht möglich, blockierende Vorgänge in jedem Fall zu vermeiden. Um dieses Problem zu lösen, wurde in NGINX Version 1.7.11 und NGINX Plus Release 7 der neue „Thread-Pools“-Mechanismus implementiert. Was es ist und wie es verwendet werden soll, erläutern wir später. Jetzt wollen wir unserem Feind von Angesicht zu Angesicht gegenübertreten.
Herausgeber – Eine Übersicht über NGINX Plus R7 finden Sie unter „Ankündigung von NGINX Plus R7“ in unserem Blog.
Ausführliche Erläuterungen zu weiteren neuen Funktionen in NGINX Plus R7 finden Sie in den folgenden Blogbeiträgen:
Zunächst zum besseren Verständnis der Problematik ein paar Worte zur Funktionsweise von NGINX.
Im Allgemeinen ist NGINX ein Ereignishandler, also ein Controller, der vom Kernel Informationen zu allen bei Verbindungen auftretenden Ereignissen empfängt und dann dem Betriebssystem Befehle für die auszuführenden Aktionen gibt. Tatsächlich übernimmt NGINX die ganze harte Arbeit, indem es das Betriebssystem orchestriert, während das Betriebssystem die Routinearbeit des Lesens und Sendens von Bytes übernimmt. Daher ist es für NGINX sehr wichtig, schnell und zeitnah zu reagieren.
Bei den Ereignissen kann es sich um Timeouts, Benachrichtigungen über lese- oder schreibbereite Sockets oder Benachrichtigungen über aufgetretene Fehler handeln. NGINX empfängt eine Reihe von Ereignissen, verarbeitet sie dann nacheinander und führt die erforderlichen Aktionen aus. Somit erfolgt die gesamte Verarbeitung in einer einfachen Schleife über eine Warteschlange in einem Thread. NGINX entfernt ein Ereignis aus der Warteschlange und reagiert dann darauf, indem es beispielsweise einen Socket schreibt oder liest. In den meisten Fällen geht dies extrem schnell (vielleicht sind nur ein paar CPU-Zyklen erforderlich, um einige Daten in den Speicher zu kopieren) und NGINX arbeitet alle Ereignisse in der Warteschlange augenblicklich ab.
Doch was passiert, wenn eine langwierige und schwere Operation stattgefunden hat? Der gesamte Zyklus der Ereignisverarbeitung bleibt hängen, während auf den Abschluss dieses Vorgangs gewartet wird.
Mit „blockierender Operation“ meinen wir also jede Operation, die den Zyklus der Ereignisverarbeitung für einen längeren Zeitraum unterbricht. Aus verschiedenen Gründen kann es zu Blockaden im Betrieb kommen. Beispielsweise ist NGINX möglicherweise mit langwierigen, CPU-intensiven Verarbeitungen beschäftigt oder muss auf den Zugriff auf eine Ressource warten (z. B. eine Festplatte oder einen Mutex- oder Bibliotheksfunktionsaufruf, der synchron Antworten von einer Datenbank erhält usw.). Der entscheidende Punkt ist, dass der Arbeitsprozess während der Verarbeitung solcher Vorgänge nichts anderes tun und keine anderen Ereignisse verarbeiten kann, selbst wenn weitere Systemressourcen verfügbar sind und einige Ereignisse in der Warteschlange diese Ressourcen nutzen könnten.
Stellen Sie sich einen Verkäufer in einem Geschäft vor, vor dem eine lange Warteschlange steht. Der erste Typ in der Schlange fragt nach etwas, das nicht im Laden, sondern im Lager ist. Der Verkäufer geht ins Lager, um die Waren auszuliefern. Jetzt muss die gesamte Schlange mehrere Stunden auf diese Lieferung warten und alle in der Schlange sind unzufrieden. Können Sie sich die Reaktion der Leute vorstellen? Die Wartezeit jeder Person in der Warteschlange verlängert sich zwar um diese Stunden, doch die Artikel, die sie kaufen möchte, befinden sich möglicherweise gleich dort im Laden.
Bei NGINX tritt fast die gleiche Situation auf, wenn eine Datei gelesen werden soll, die nicht im Speicher zwischengespeichert ist, sondern von der Festplatte gelesen werden muss. Festplatten sind langsam (insbesondere rotierende Festplatten) und obwohl die anderen Anfragen in der Warteschlange möglicherweise keinen Zugriff auf die Festplatte benötigen, müssen sie trotzdem warten. Die Folge ist eine Erhöhung der Latenzen und eine Nichtausnutzung der Systemressourcen.
Einige Betriebssysteme bieten eine asynchrone Schnittstelle zum Lesen und Senden von Dateien und NGINX kann diese Schnittstelle verwenden (siehe die aio- Direktive). Ein gutes Beispiel hierfür ist FreeBSD. Leider können wir das Gleiche von Linux nicht behaupten. Obwohl Linux eine Art asynchrone Schnittstelle zum Lesen von Dateien bereitstellt, weist es einige erhebliche Nachteile auf. Eine davon sind Ausrichtungsanforderungen für Dateizugriff und Puffer, aber NGINX kommt damit gut zurecht. Aber das zweite Problem ist schlimmer. Für die asynchrone Schnittstelle muss im Dateideskriptor das Flag O_DIRECT
gesetzt sein. Dies bedeutet, dass bei jedem Zugriff auf die Datei der Cache im Speicher umgangen wird und die Belastung der Festplatten zunimmt. Das macht es für viele Fälle definitiv nicht optimal.
Um insbesondere dieses Problem zu lösen, wurden in NGINX 1.7.11 und NGINX Plus Release 7 Thread-Pools eingeführt.
Sehen wir uns nun genauer an, worum es bei Thread-Pools geht und wie sie funktionieren.
Kehren wir zu unserer armen Verkäuferin zurück, die Waren aus einem weit entfernten Lager ausliefert. Aber er ist schlauer geworden (oder wurde er schlauer, nachdem er von der Menge wütender Kunden verprügelt wurde?) und hat einen Lieferdienst engagiert. Wenn jetzt jemand etwas aus dem weit entfernten Lager bestellt, muss er nicht selbst dorthin gehen, sondern gibt die Bestellung einfach bei einem Lieferdienst ab. Dieser kümmert sich um die Bestellung, während unser Verkaufsassistent weiterhin andere Kunden bedient. Somit müssen nur noch die Kunden auf die Lieferung warten, deren Ware nicht im Laden ist, während die anderen sofort bedient werden können.
In Bezug auf NGINX führt der Thread-Pool die Funktionen des Lieferdienstes aus. Es besteht aus einer Aufgabenwarteschlange und einer Anzahl von Threads, die die Warteschlange verwalten. Wenn ein Arbeitsprozess eine möglicherweise langwierige Operation ausführen muss, stellt er eine Aufgabe in die Warteschlange des Pools, statt sie selbst auszuführen. Von dort kann sie von einem beliebigen freien Thread übernommen und ausgeführt werden.
Dann haben wir anscheinend eine weitere Warteschlange. Rechts. In diesem Fall ist die Warteschlange jedoch durch eine bestimmte Ressource begrenzt. Wir können nicht schneller von einem Laufwerk lesen, als das Laufwerk Daten produzieren kann. Jetzt verzögert das Laufwerk zumindest nicht die Verarbeitung anderer Ereignisse und es warten nur die Anforderungen, die auf Dateien zugreifen müssen.
Der Vorgang „Lesen von der Festplatte“ wird oft als gängigstes Beispiel für einen blockierenden Vorgang verwendet, aber tatsächlich kann die Thread-Pool-Implementierung in NGINX für alle Aufgaben verwendet werden, die nicht für die Verarbeitung im Hauptarbeitszyklus geeignet sind.
Derzeit ist das Auslagern in Thread-Pools nur für drei wesentliche Vorgänge implementiert: den Systemaufruf read()
auf den meisten Betriebssystemen, sendfile()
unter Linux und aio_write()
unter Linux, das beim Schreiben einiger temporärer Dateien, beispielsweise der Dateien für den Cache, verwendet wird. Wir werden die Implementierung weiterhin testen und vergleichen und in zukünftigen Versionen möglicherweise andere Vorgänge auf die Thread-Pools auslagern, wenn sich daraus ein klarer Vorteil ergibt.
Editor – Unterstützung für den Systemaufruf aio_write()
wurde in NGINX 1.9.13 und NGINX Plus R9 hinzugefügt.
Es ist Zeit, von der Theorie zur Praxis überzugehen. Um die Wirkung der Verwendung von Thread-Pools zu demonstrieren, führen wir einen synthetischen Benchmark durch, der die schlechteste Mischung aus blockierenden und nicht blockierenden Vorgängen simuliert.
Es ist ein Datensatz erforderlich, der garantiert nicht in den Speicher passt. Auf einer Maschine mit 48 GB RAM haben wir 256 GB an zufälligen Daten in 4 MB-Dateien generiert und dann NGINX 1.9.0 für die Bereitstellung dieser Daten konfiguriert.
Die Konfiguration ist ziemlich einfach:
worker_processes 16;
events {
accept_mutex aus;
}
http {
include mime.types;
default_type application/octet-stream;
access_log aus;
sendfile an;
sendfile_max_chunk 512k;
server {
listen 8000;
location / {
root /storage;
}
}
}
Wie Sie sehen, wurden zur Erzielung einer besseren Leistung einige Optimierungen vorgenommen: Protokollierung
und accept_mutex
wurden deaktiviert, sendfile
wurde aktiviert und sendfile_max_chunk
wurde festgelegt. Die letzte Anweisung kann die maximale Zeit reduzieren, die zum Blockieren von sendfile()-
Aufrufen benötigt wird, da NGINX nicht versucht, die gesamte Datei auf einmal zu senden, sondern dies in 512-KB-Blöcken tut.
Die Maschine verfügt über zwei Intel Xeon E5645-Prozessoren (insgesamt 12 Kerne, 24 HT-Threads) und eine 10-Gbit/s-Netzwerkschnittstelle. Das Festplattensubsystem wird durch vier Western Digital WD1003FBYX-Festplatten dargestellt, die in einem RAID10-Array angeordnet sind. Die gesamte Hardware läuft auf Ubuntu Server 14.04.1 LTS.
Die Clients werden durch zwei Maschinen mit denselben Spezifikationen dargestellt. Auf einer dieser Maschinen erstellt wrk
mithilfe eines Lua-Skripts eine Last. Das Skript fordert über 200 parallele Verbindungen in zufälliger Reihenfolge Dateien von unserem Server an, und jede Anforderung führt wahrscheinlich zu einem Cache-Fehler und einem blockierenden Lesevorgang von der Festplatte. Nennen wir diese Last die Zufallslast .
Auf dem zweiten Client-Rechner führen wir eine weitere Kopie von wrk
aus, die über 50 parallele Verbindungen dieselbe Datei mehrmals anfordert. Da auf diese Datei häufig zugegriffen wird, bleibt sie die ganze Zeit im Speicher. Unter normalen Umständen würde NGINX diese Anfragen sehr schnell bearbeiten, die Leistung sinkt jedoch, wenn die Arbeitsprozesse durch andere Anfragen blockiert werden. Nennen wir diese Last die konstante Last.
Die Leistung wird gemessen, indem der Durchsatz des Servercomputers mit ifstat
überwacht und die WRK-
Ergebnisse vom zweiten Client abgerufen werden.
Der erste Durchlauf ohne Thread-Pools liefert keine besonders aufregenden Ergebnisse:
% ifstat -bi eth2 eth2 Kbps rein Kbps raus 5531,24 1,03e+06 4855,23 812922,7 5994,66 1,07e+06 5476,27 981529,3 6353,62 1,12e+06 5166,17 892770,3 5522,81 978540,8 6208,10 985466,7 6370,79 1,12e+06 6123,33 1,07e+06
Wie Sie sehen, kann der Server mit dieser Konfiguration insgesamt etwa 1 Gbit/s Datenverkehr erzeugen. In der Ausgabe von top
können wir sehen, dass alle Arbeitsprozesse die meiste Zeit mit blockierenden E/A-Vorgängen verbringen (sie befinden sich im Zustand D
):
oben – 10:40:47, 11 Tage, 1:32, 1 Benutzer, durchschnittliche Auslastung: 49,61, 45,77 62,89Aufgaben:375 gesamt,2 läuft,373 Schlafen,0 angehalten,0 Zombie-%CPU(s):0.0 uns,0.3 ja,0.0 nein,67.7 Ausweis,31.9 wa,0.0 Hi,0.0 ja,0.0 1. KiB-Speicher:49453440 gesamt,49149308 gebraucht,304132 frei,98780 Puffer KiB Swap:10474236 gesamt,20124 gebraucht,10454112 frei,46903412 zwischengespeicherter Speicher PID USER PR NI VIRT RES SHR S %CPU %MEM ZEIT+ BEFEHL 4639 vbart 20 0 47180 28152 496 D 0,7 0,1 0:00.17 nginx 4632 vbart 20 0 47180 28196 536 D 0,3 0,1 0:00.11 nginx 4633 vbart 20 0 47180 28324 540 D 0,3 0,1 0:00.11 nginx 4635 vbart 20 0 47180 28136 480 D 0,3 0,1 0:00.12 nginx 4636 vbart 20 0 47180 28208 536 D 0,3 0,1 0:00.14 nginx 4637 vbart 20 0 47180 28208 536 D 0,3 0,1 0:00.10 nginx 4638 vbart 20 0 47180 28204 536 D 0,3 0,1 0:00.12 nginx 4640 vbart 20 0 47180 28324 540 D 0,3 0,1 0:00.13 nginx 4641 vbart 20 0 47180 28324 540 D 0,3 0,1 0:00.13 nginx 4642 vbart 20 0 47180 28208 536 D 0,3 0,1 0:00.11 nginx 4643 vbart 20 0 47180 28276 536 D 0,3 0,1 0:00.29 nginx 4644 vbart 20 0 47180 28204 536 D 0,3 0,1 0:00.11 nginx 4645 vbart 20 0 47180 28204 536 D 0,3 0,1 0:00.17 nginx 4646 vbart 20 0 47180 28204 536 D 0,3 0,1 0:00.12 nginx 4647 vbart 20 0 47180 28208 532 D 0,3 0,1 0:00,17 nginx 4631 vbart 20 0 47180 756 252 S 0,0 0,1 0:00,00 nginx 4634 vbart 20 0 47180 28208 536 D 0,0 0,1 0:00,11 nginx< 4648 vbart 20 0 25232 1956 1160 R 0,0 0,0 0:00,08 oben 25921 vbart 20 0 121956 2232 1056 S 0,0 0,0 0:01,97 sshd 25923 vbart 20 0 40304 4160 2208 S 0,0 0,0 0:00,53 zsh
In diesem Fall wird der Durchsatz durch das Festplattensubsystem begrenzt, während die CPU die meiste Zeit im Leerlauf ist. Auch die Ergebnisse von wrk
sind sehr niedrig:
1-Minuten-Test wird ausgeführt @ http://192.0.2.1:8000/1/1/1 12 Threads und 50 Verbindungen
Thread-Statistiken Durchschnittliche Standardabweichung Max. +/- Standardabweichung
Latenz 7,42 s 5,31 s 24,41 s 74,73 %
Anf./Sek. 0,15 0,36 1,00 84,62 %
488 Anfragen in 1,01 m, 2,01 GB gelesen
Anfragen/Sek.: 8,08
Übertragung/Sek.: 34,07 MB
Und denken Sie daran, dies gilt für die Datei, die aus dem Speicher bereitgestellt werden soll! Die übermäßig großen Latenzen sind darauf zurückzuführen, dass alle Arbeitsprozesse damit beschäftigt sind, Dateien von den Laufwerken zu lesen, um die zufällige Last zu bewältigen, die durch 200 Verbindungen vom ersten Client entsteht, und unsere Anfragen nicht rechtzeitig verarbeiten können.
Es ist Zeit, unsere Thread-Pools ins Spiel zu bringen. Dazu fügen wir einfach die Direktive „aio
threads“
zum Location-
Block hinzu:
Standort / { root /storage;
aio threads;
}
und bitten Sie NGINX, seine Konfiguration neu zu laden.
Danach wiederholen wir den Test:
% ifstat -bi eth2 eth2 Kbps rein Kbps raus 60915,19 9,51e+06 59978,89 9,51e+06 60122,38 9,51e+06 61179,06 9,51e+06 61798,40 9,51e+06 57072,97 9,50e+06 56072,61 9,51e+06 61279,63 9,51e+06 61243,54 9,51e+06 59632,50 9,50e+06
Jetzt produziert unser Server 9,5 Gbit/s , im Vergleich zu ~1 Gbit/s ohne Thread-Pools!
Es könnte wahrscheinlich sogar noch mehr produzieren, hat aber bereits die praktische maximale Netzwerkkapazität erreicht, sodass NGINX in diesem Test durch die Netzwerkschnittstelle begrenzt ist. Die Arbeitsprozesse verbringen die meiste Zeit damit, zu schlafen und auf neue Ereignisse zu warten (sie befinden sich oben
im Zustand S
):
oben – 10:43:17, 11 Tage, 1:35, 1 Benutzer, durchschnittliche Auslastung: 172,71, 93,84, 77,90Aufgaben:376 gesamt,1 läuft,375 Schlafen,0 angehalten,0 Zombie-%CPU(s):0.2 uns,1.2 ja,0.0 nein,34.8 Ausweis,61.5 wa,0.0 Hi,2.3 ja,0.0 1. KiB-Speicher:49453440 gesamt,49096836 gebraucht,356604 frei,97236 Puffer KiB Swap:10474236 gesamt,22860 gebraucht,10451376 frei,46836580 zwischengespeicherter Speicher PID USER PR NI VIRT RES SHR S %CPU %MEM ZEIT+ BEFEHL 4654 vbart 20 0 309708 28844 596 S 9,0 0,1 0:08,65 nginx 4660 vbart 20 0 309748 28920 596 S 6,6 0,1 0:14,82 nginx 4658 vbart 20 0 309452 28424 520 S 4,3 0,1 0:01,40 nginx 4663 vbart 20 0 309452 28476 572 S 4,3 0,1 0:01,32 nginx 4667 vbart 20 0 309584 28712 588 S 3,7 0,1 0:05,19 nginx 4656 vbart 20 0 309452 28476 572 S 3,3 0,1 0:01,84 nginx 4664 vbart 20 0 309452 28428 524 S 3,3 0,1 0:01,29 nginx 4652 vbart 20 0 309452 28476 572 S 3,0 0,1 0:01,46 nginx 4662 vbart 20 0 309552 28700 596 S 2,7 0,1 0:05,92 nginx 4661 vbart 20 0 309464 28636 596 S 2,3 0,1 0:01,59 nginx 4653 vbart 20 0 309452 28476 572 S 1,7 0,1 0:01,70 nginx 4666 vbart 20 0 309452 28428 524 S 1,3 0,1 0:01,63 nginx 4657 vbart 20 0 309584 28696 592 S 1,0 0,1 0:00,64 nginx 4655 vbart 20 0 30958 28476 572 S 0,7 0,1 0:02.81 nginx 4659 vbart 20 0 309452 28468 564 S 0,3 0,1 0:01.20 nginx 4665 vbart 20 0 309452 28476 572 S 0,3 0,1 0:00.71 nginx 5180 vbart 20 0 25232 1952 1156 R 0,0 0,0 0:00.45 oben 4651 vbart 20 0 20032 752 252 S 0,0 0,0 0:00.00 nginx 25921 vbart 20 0 121956 2176 1000 S 0,0 0,0 0:01,98 sshd 25923 vbart 20 0 40304 3840 2208 S 0,0 0,0 0:00,54 zsh
Es sind immer noch genügend CPU-Ressourcen vorhanden.
Die Ergebnisse der Arbeit
:
1-Minuten-Test wird ausgeführt @ http://192.0.2.1:8000/1/1/1 12 Threads und 50 Verbindungen
Thread-Statistiken Durchschnittliche Standardabweichung Max. +/- Standardabweichung
Latenz 226,32 ms 392,76 ms 1,72 s 93,48 %
Anf./Sek. 20,02 10,84 59,00 65,91 %
15045 Anfragen in 1,00 m, 58,86 GB gelesen
Anfragen/Sek.: 250,57
Übertragung/Sek.: 0,98 GB
Die durchschnittliche Zeit zum Bereitstellen einer 4 MB großen Datei wurde von 7,42 Sekunden auf 226,32 Millisekunden (33 mal weniger) reduziert, und die Anzahl der Anfragen pro Sekunde hat sich um das 31-fache erhöht (250 gegenüber 8)!
Die Erklärung liegt darin, dass unsere Anfragen nicht mehr in der Ereigniswarteschlange auf ihre Verarbeitung warten, während Arbeitsprozesse beim Lesen blockiert sind, sondern von freien Threads verarbeitet werden. Solange das Festplattensubsystem seine Aufgabe bestmöglich erfüllt und unsere zufällige Last vom ersten Clientcomputer bedient, verwendet NGINX die restlichen CPU-Ressourcen und die Netzwerkkapazität, um Anforderungen des zweiten Clients aus dem Speicher zu bedienen.
Nach all unseren Befürchtungen hinsichtlich blockierender Vorgänge und einigen spannenden Ergebnissen werden die meisten von Ihnen wahrscheinlich bereits Thread-Pools auf Ihren Servern konfigurieren. Beeilen Sie sich nicht.
Die Wahrheit ist, dass glücklicherweise die meisten Lese- und Sendevorgänge auf Dateien nicht mit langsamen Festplatten zu tun haben. Wenn zum Speichern des Datensatzes ausreichend RAM vorhanden ist, ist ein Betriebssystem intelligent genug, häufig verwendete Dateien in einem sogenannten „Seitencache“ zwischenzuspeichern.
Der Seitencache funktioniert ziemlich gut und ermöglicht NGINX, in fast allen gängigen Anwendungsfällen eine hervorragende Leistung zu demonstrieren. Das Lesen aus dem Seitencache geht recht schnell und niemand kann solche Vorgänge als „Blockieren“ bezeichnen. Andererseits ist mit der Auslagerung in einen Thread-Pool ein gewisser Mehraufwand verbunden.
Wenn Sie also über eine angemessene Menge an RAM verfügen und Ihr Arbeitsdatensatz nicht sehr groß ist, funktioniert NGINX bereits ohne die Verwendung von Thread-Pools optimal.
Das Auslagern von Lesevorgängen in den Threadpool ist eine Technik, die auf sehr spezielle Aufgaben anwendbar ist. Dies ist besonders dann nützlich, wenn die Menge häufig angeforderter Inhalte nicht in den VM-Cache des Betriebssystems passt. Dies kann beispielsweise bei einem stark ausgelasteten NGINX-basierten Streaming-Media-Server der Fall sein. Dies ist die Situation, die wir in unserem Benchmark simuliert haben.
Es wäre toll, wenn wir die Auslagerung von Lesevorgängen in Thread-Pools verbessern könnten. Alles was wir brauchen, ist eine effiziente Möglichkeit, um festzustellen, ob die benötigten Dateidaten im Speicher vorhanden sind oder nicht. Nur im letzteren Fall sollte der Lesevorgang auf einen separaten Thread ausgelagert werden.
Um auf unsere Verkaufsanalogie zurückzukommen: Derzeit kann der Verkäufer nicht wissen, ob der gewünschte Artikel im Laden ist und muss alle Bestellungen entweder immer an den Lieferservice weitergeben oder immer selbst abwickeln.
Der Grund hierfür liegt darin, dass diese Funktion in den Betriebssystemen fehlt. Die ersten Versuche, es als Systemaufruf fincore()
zu Linux hinzuzufügen, fanden 2010 statt, führten jedoch nicht dazu. Später gab es mehrere Versuche, es als neuen preadv2()-
Systemaufruf mit dem Flag RWF_NONBLOCK
zu implementieren (weitere Informationen finden Sie unter „Nicht blockierende gepufferte Dateilesevorgänge“ und „Asynchrone gepufferte Lesevorgänge“ auf LWN.net). Das Schicksal all dieser Patches ist noch unklar. Das Traurige dabei ist, dass der Hauptgrund, warum diese Patches noch nicht in den Kernel aufgenommen wurden, anscheinend das anhaltende Bikeshedding ist.
Benutzer von FreeBSD müssen sich dagegen überhaupt keine Sorgen machen. FreeBSD verfügt bereits über eine ausreichend gute asynchrone Schnittstelle zum Lesen von Dateien, die Sie anstelle von Thread-Pools verwenden sollten.
Wenn Sie also sicher sind, dass Sie in Ihrem Anwendungsfall durch die Verwendung von Thread-Pools einen Nutzen erzielen können, ist es an der Zeit, tief in die Konfiguration einzutauchen.
Die Konfiguration ist recht einfach und flexibel. Als Erstes sollten Sie über NGINX Version 1.7.11 oder höher verfügen, kompiliert mit dem Argument --with-threads
für den Konfigurationsbefehl
. NGINX Plus-Benutzer benötigen Release 7 oder höher. Im einfachsten Fall sieht die Konfiguration sehr schlicht aus. Sie müssen lediglich die Direktive „aio
threads“
im entsprechenden Kontext einfügen:
# in den Kontext-Threads „http“, „Server“ oder „Standort“;
Dies ist die minimal mögliche Konfiguration von Thread-Pools. Tatsächlich handelt es sich um eine Kurzversion der folgenden Konfiguration:
# im „Haupt“-Kontextthread_pool Standard-Threads=32 max_queue=65536;
# im „http“-, „Server“- oder „Standort“-Kontext
aio-Threads=Standard;
Es definiert einen Thread-Pool namens „Standard“ mit 32 Arbeitsthreads und einer maximalen Länge für die Task-Warteschlange von 65536 Tasks. Wenn die Aufgabenwarteschlange überlastet ist, lehnt NGINX die Anforderung ab und protokolliert diesen Fehler:
Thread-Pool „ NAME “-Warteschlangenüberlauf: N Aufgaben warten
Der Fehler bedeutet, dass die Threads die Arbeit möglicherweise nicht so schnell verarbeiten können, wie sie zur Warteschlange hinzugefügt wird. Sie können versuchen, die maximale Warteschlangengröße zu erhöhen. Wenn das jedoch nicht hilft, ist Ihr System nicht in der Lage, so viele Anfragen zu verarbeiten.
Wie Sie bereits bemerkt haben, können Sie mit der Direktive thread_pool
die Anzahl der Threads, die maximale Länge der Warteschlange und den Namen eines bestimmten Thread-Pools konfigurieren. Letzteres bedeutet, dass Sie mehrere unabhängige Thread-Pools konfigurieren und diese an verschiedenen Stellen Ihrer Konfigurationsdatei für unterschiedliche Zwecke verwenden können:
# im „Haupt“-Kontext
thread_pool eins Threads=128 max_queue=0;
thread_pool zwei Threads=32;
http {
server {
location /one {
aio threads=one;
}
location /two {
aio threads=two;
}
}
# ...
}
Wenn der Parameter max_queue
nicht angegeben ist, wird standardmäßig der Wert 65536 verwendet. Wie gezeigt ist es möglich, max_queue
auf Null zu setzen. In diesem Fall kann der Thread-Pool nur so viele Aufgaben verarbeiten, wie Threads konfiguriert sind. Es warten keine Aufgaben in der Warteschlange.
Stellen Sie sich nun vor, Sie haben einen Server mit drei Festplatten und möchten, dass dieser Server als „Caching-Proxy“ fungiert, der alle Antworten Ihrer Backends zwischenspeichert. Die erwartete Menge zwischengespeicherter Daten übersteigt den verfügbaren RAM bei weitem. Es ist eigentlich ein Caching-Knoten für Ihr persönliches CDN. Das Wichtigste ist hierbei natürlich, die maximale Leistung aus den Laufwerken herauszuholen.
Eine Ihrer Möglichkeiten besteht darin, ein RAID-Array zu konfigurieren. Dieser Ansatz hat seine Vor- und Nachteile. Jetzt können Sie mit NGINX noch einen Schritt weiter gehen:
# Wir gehen davon aus, dass jede der Festplatten in einem dieser Verzeichnisse gemountet ist:# /mnt/disk1, /mnt/disk2 oder /mnt/disk3
# im „Haupt“-Kontext
thread_pool pool_1 threads=16;
thread_pool pool_2 threads=16;
thread_pool pool_3 threads=16;
http {
proxy_cache_path /mnt/disk1 levels=1:2 keys_zone=cache_1:256m max_size=1024G
use_temp_path=off;
proxy_cache_path /mnt/disk2 levels=1:2 keys_zone=cache_2:256m max_size=1024G
use_temp_path=off;
Proxy-Cache-Pfad /mnt/Disk3 Ebenen = 1:2 Schlüsselzone = Cache_3:256m max_size = 1024G
use_temp_path = aus;
split_clients $request_uri $disk {
33,3 % 1;
33,3 % 2;
* 3;
}
Server {
# ...
Standort / {
Proxy-Passwort http://backend;
Proxy-Cache-Schlüssel $request_uri;
Proxy-Cache Cache_$Disk;
aio Threads = Pool_$Disk;
Sendedatei ein;
}
}
}
In dieser Konfiguration definieren die thread_pool
-Direktiven einen dedizierten, unabhängigen Thread-Pool für jede Festplatte und die proxy_cache_path
-Direktiven definieren einen dedizierten, unabhängigen Cache auf jeder Festplatte.
Für den Lastausgleich zwischen den Caches (und damit zwischen den Festplatten) wird das Modul split_clients
verwendet, das sich perfekt für diese Aufgabe eignet.
Der Parameter use_temp_path=off
der Direktive proxy_cache_path
weist NGINX an, temporäre Dateien in denselben Verzeichnissen zu speichern, in denen sich die entsprechenden Cache-Daten befinden. Dies ist erforderlich, um das Kopieren von Antwortdaten zwischen den Festplatten beim Aktualisieren unserer Caches zu vermeiden.
All dies zusammen ermöglicht es uns, die maximale Leistung aus dem aktuellen Festplattensubsystem herauszuholen, da NGINX über separate Thread-Pools parallel und unabhängig mit den Laufwerken interagiert. Jedes der Laufwerke wird von 16 unabhängigen Threads mit einer dedizierten Aufgabenwarteschlange zum Lesen und Senden von Dateien bedient.
Ich wette, Ihre Kunden mögen diesen maßgeschneiderten Ansatz. Stellen Sie sicher, dass es Ihren Festplatten auch gefällt.
Dieses Beispiel demonstriert gut, wie flexibel NGINX speziell auf Ihre Hardware abgestimmt werden kann. Es ist, als würden Sie NGINX Anweisungen zur besten Art der Interaktion mit der Maschine und Ihrem Datensatz geben. Und indem Sie NGINX im Benutzerbereich optimieren, können Sie sicherstellen, dass Ihre Software, Ihr Betriebssystem und Ihre Hardware optimal zusammenarbeiten, um alle Systemressourcen so effektiv wie möglich zu nutzen.
Zusammenfassend lässt sich sagen, dass Thread-Pools eine großartige Funktion sind, die die Leistung von NGINX auf ein neues Niveau hebt, indem sie einen seiner bekannten und langjährigen Feinde – die Blockierung – eliminiert, insbesondere wenn es um wirklich große Inhaltsmengen geht.
Und es kommt noch mehr. Wie bereits erwähnt, ermöglicht diese brandneue Schnittstelle potenziell das Auslagern aller langen und blockierenden Vorgänge ohne Leistungseinbußen. NGINX eröffnet neue Horizonte im Hinblick auf eine Vielzahl neuer Module und Funktionen. Viele beliebte Bibliotheken bieten noch immer keine asynchrone, nicht blockierende Schnittstelle, was sie bisher mit NGINX inkompatibel machte. Wir werden zwar viel Zeit und Ressourcen in die Entwicklung unseres eigenen, nicht blockierenden Prototyps einer Bibliothek investieren, aber ist das immer den Aufwand wert? Mit den integrierten Thread-Pools ist es nun möglich, solche Bibliotheken relativ einfach zu verwenden und solche Module ohne Auswirkungen auf die Leistung zu erstellen.
Bleiben Sie dran.
Probieren Sie Thread-Pools in NGINX Plus selbst aus – starten Sie noch heute Ihre kostenlose 30-Tage-Testversion oder kontaktieren Sie uns, um Ihre Anwendungsfälle zu besprechen .
„Dieser Blogbeitrag kann auf Produkte verweisen, die nicht mehr verfügbar und/oder nicht mehr unterstützt werden. Die aktuellsten Informationen zu verfügbaren F5 NGINX-Produkten und -Lösungen finden Sie in unserer NGINX-Produktfamilie . NGINX ist jetzt Teil von F5. Alle vorherigen NGINX.com-Links werden auf ähnliche NGINX-Inhalte auf F5.com umgeleitet."