Wenn es um die Erstellung von Webanwendungen mit hohem Durchsatz geht, sind NginX und Node.js eine natürliche Ergänzung. Sie basieren alle auf dem ereignisgesteuerten Modell und können den C10K-Engpass herkömmlicher Webserver wie Apache problemlos überwinden. Mit der Standardkonfiguration kann bereits eine hohe Parallelität erreicht werden, aber wenn Sie auf günstiger Hardware mehr als Tausende von Anfragen pro Sekunde erreichen möchten, gibt es noch einiges zu tun.
In diesem Artikel wird davon ausgegangen, dass Leser das HttpProxyModule von NginX verwenden, um als Reverse-Proxy für den Upstream-Server node.js zu fungieren. Wir werden die Optimierung von sysctl in Systemen mit Ubuntu 10.04 und höher sowie die Optimierung von node.js-Anwendungen und NginX vorstellen. Wenn Sie ein Debian-System verwenden, können Sie das gleiche Ziel natürlich auch erreichen, die Optimierungsmethoden sind jedoch unterschiedlich.
Netzwerkoptimierung
Wenn Sie nicht zunächst die zugrunde liegenden Übertragungsmechanismen von Nginx und Node.js verstehen und eine gezielte Optimierung durchführen, ist die Optimierung möglicherweise vergeblich, egal wie detailliert die beiden sind. Im Allgemeinen verbindet Nginx den Client und die Upstream-Anwendungen über TCP-Sockets.
Unser System verfügt über viele Schwellenwerte und Einschränkungen für TCP, die über Kernel-Parameter festgelegt werden. Die Standardwerte dieser Parameter werden häufig für allgemeine Zwecke festgelegt und können den Anforderungen von Webservern an hohen Datenverkehr und kurze Lebensdauer nicht gerecht werden.
Hier sind einige Parameter, die für die Optimierung von TCP in Frage kommen. Um sie wirksam zu machen, können Sie sie in die Datei /etc/sysctl.conf oder in eine neue Konfigurationsdatei wie /etc/sysctl.d/99-tuning.conf einfügen und dann sysctl -p ausführen Lassen Sie den Kernel sie laden. Wir verwenden sysctl-cookbook, um diese körperliche Arbeit zu erledigen.
Es ist zu beachten, dass die hier aufgeführten Werte sicher zu verwenden sind. Es wird jedoch dennoch empfohlen, dass Sie die Bedeutung jedes Parameters studieren, um einen geeigneteren Wert basierend auf Ihrer Last, Hardware und Nutzung auszuwählen.
Heben Sie einige wichtige hervor.
Um den Downstream-Client für die Upstream-Anwendung zu bedienen, muss NginX zwei TCP-Verbindungen öffnen, eine zum Client und eine zur Anwendung. Wenn ein Server viele Verbindungen empfängt, sind die verfügbaren Ports des Systems schnell erschöpft. Durch Ändern des Parameters net.ipv4.ip_local_port_range können Sie den Bereich der verfügbaren Ports vergrößern. Wenn in /var/log/syslog ein solcher Fehler gefunden wird: „Mögliches SYN-Flooding auf Port 80. Cookies werden gesendet“, bedeutet dies, dass das System keinen verfügbaren Port finden kann. Durch Erhöhen des Parameters net.ipv4.ip_local_port_range kann dieser Fehler reduziert werden.
Wenn der Server zwischen einer großen Anzahl von TCP-Verbindungen wechseln muss, wird eine große Anzahl von Verbindungen im Status TIME_WAIT generiert. TIME_WAIT bedeutet, dass die Verbindung selbst geschlossen ist, die Ressourcen jedoch nicht freigegeben wurden. Wenn Sie net_ipv4_tcp_tw_reuse auf 1 setzen, kann der Kernel versuchen, Verbindungen wiederzuverwenden, wenn dies sicher ist. Dies ist viel kostengünstiger als die Wiederherstellung neuer Verbindungen.
Dies ist die Mindestzeit, die eine Verbindung im Status TIME_WAIT warten muss, bevor sie wiederhergestellt wird. Eine Verkleinerung kann das Recycling beschleunigen.
So überprüfen Sie den Verbindungsstatus
Verwenden Sie netstat:
oder verwenden Sie ss:
NginX
Wenn die Belastung des Webservers allmählich zunimmt, werden wir auf einige seltsame Einschränkungen von NginX stoßen. Die Verbindung wird unterbrochen und der Kernel meldet weiterhin SYN-Flut. Zu diesem Zeitpunkt sind der Lastdurchschnitt und die CPU-Auslastung sehr gering, und der Server kann offensichtlich mehr Verbindungen verarbeiten, was wirklich frustrierend ist.
Nach einer Untersuchung wurde festgestellt, dass sich viele Verbindungen im Status TIME_WAIT befinden. Dies ist die Ausgabe von einem der Server:
Gesamt-IP IPv6 transportieren
* 541 - - -
RAW 0 0 0 0
UDP 13 10 3
TCP 326 325 1
INET 339 335 4
FRAG 0 0 0 0
Es gibt 47135 TIME_WAIT-Verbindungen! Darüber hinaus ist aus ss ersichtlich, dass es sich bei allen um geschlossene Verbindungen handelt. Dies weist darauf hin, dass der Server die meisten verfügbaren Ports verbraucht hat, und impliziert auch, dass der Server jeder Verbindung neue Ports zuweist. Das Optimieren des Netzwerks half ein wenig bei der Lösung des Problems, aber es waren immer noch nicht genügend Ports vorhanden.
Nach weiteren Recherchen habe ich ein Dokument über den Uplink-Keepalive-Befehl gefunden, das lautet:
Interessant. Theoretisch minimiert dieses Setup die Verschwendung von Verbindungen, indem Anfragen über zwischengespeicherte Verbindungen weitergeleitet werden. In der Dokumentation wird auch erwähnt, dass wir „proxy_http_version“ auf „1.1“ setzen und den „Connection“-Header löschen sollten. Nach weiteren Recherchen habe ich herausgefunden, dass dies eine gute Idee ist, da HTTP/1.1 die Nutzung von TCP-Verbindungen im Vergleich zu HTTP1.0 erheblich optimiert und Nginx standardmäßig HTTP/1.0 verwendet.
Nach der Änderung wie im Dokument vorgeschlagen sieht unsere Uplink-Konfigurationsdatei wie folgt aus:
Ich habe auch die Proxy-Einstellungen im Serverbereich wie vorgeschlagen geändert. Gleichzeitig wurde ein Proxy_next_upstream hinzugefügt, um ausgefallene Server zu überspringen, das Keepalive_Timeout des Clients angepasst und das Zugriffsprotokoll deaktiviert. Die Konfiguration sieht folgendermaßen aus:
client_max_body_size 16M;
keepalive_timeout 10;
Standort / {
Proxy_next_upstream-Fehler-Timeout http_500 http_502 http_503 http_504;
Proxy_set_header-Verbindung „“;
Proxy_http_version 1.1;
Proxy_pass http://backend_nodejs;
}
access_log off;
error_log /dev/null crit;
}
Nach der Übernahme der neuen Konfiguration stellte ich fest, dass die von den Servern belegten Sockets um 90 % reduziert wurden. Anfragen können jetzt über deutlich weniger Verbindungen übermittelt werden. Die neue Ausgabe lautet wie folgt:
Gesamt: 558 (Kernel 604)
TCP: 4675 (estab 485, geschlossen 4183, verwaist 0, synrecv 0, Wartezeit 4183/0), Ports 2768
Gesamt-IP IPv6 transportieren
* 604 - - -
RAW 0 0 0 0
UDP 13 10 3
TCP 492 491 1
INET 505 501 4
Node.js
Dank des ereignisgesteuerten Designs, das I/O asynchron verarbeitet, kann Node.js eine große Anzahl von Verbindungen und Anfragen sofort verarbeiten. Obwohl es andere Optimierungsmethoden gibt, konzentriert sich dieser Artikel hauptsächlich auf den Prozessaspekt von node.js.
Der Knoten ist Single-Threaded und verwendet nicht automatisch mehrere Kerne. Mit anderen Worten: Die Anwendung kann nicht automatisch alle Funktionen des Servers nutzen.
Erzielen Sie ein Clustering von Knotenprozessen
Wir können die Anwendung so ändern, dass sie mehrere Threads aufteilt und Daten auf demselben Port empfängt, wodurch die Last über mehrere Kerne verteilt werden kann. Node verfügt über ein Cluster-Modul, das alle zum Erreichen dieses Ziels erforderlichen Tools bereitstellt. Das Hinzufügen dieser Tools zur Anwendung erfordert jedoch viel manuelle Arbeit. Wenn Sie Express verwenden, verfügt eBay über ein Modul namens cluster2, das verwendet werden kann.
Kontextwechsel verhindern
Wenn Sie mehrere Prozesse ausführen, sollten Sie sicherstellen, dass jeder CPU-Kern jeweils nur mit einem Prozess beschäftigt ist. Wenn die CPU über N Kerne verfügt, sollten wir im Allgemeinen N-1 Anwendungsprozesse generieren. Dadurch wird sichergestellt, dass jeder Prozess eine angemessene Zeitspanne erhält, sodass ein Kern frei bleibt, damit der Kernel-Scheduler andere Aufgaben ausführen kann. Wir müssen außerdem sicherstellen, dass grundsätzlich keine anderen Aufgaben außer Node.js auf dem Server ausgeführt werden, um CPU-Konflikte zu verhindern.
Wir haben einmal einen Fehler gemacht und zwei node.js-Anwendungen auf dem Server bereitgestellt, und dann hat jede Anwendung N-1 Prozesse geöffnet. Dadurch konkurrieren sie untereinander um die CPU, wodurch die Systemlast stark ansteigt. Obwohl unsere Server alle 8-Core-Maschinen sind, ist der durch den Kontextwechsel verursachte Leistungsaufwand immer noch deutlich zu spüren. Unter Kontextwechsel versteht man das Phänomen, dass die CPU die aktuelle Aufgabe unterbricht, um andere Aufgaben auszuführen. Beim Umschalten muss der Kernel den gesamten Status des aktuellen Prozesses anhalten und dann einen anderen Prozess laden und ausführen. Um dieses Problem zu lösen, haben wir die Anzahl der von jeder Anwendung gestarteten Prozesse reduziert, damit sie sich die CPU gerecht teilen können. Dadurch wurde die Systemlast reduziert:
Bitte achten Sie auf das Bild oben, um zu sehen, wie die Systemlast (blaue Linie) unter die Anzahl der CPU-Kerne (rote Linie) sinkt. Auf anderen Servern haben wir dasselbe gesehen. Da die Gesamtarbeitslast gleich bleibt, kann die Leistungsverbesserung in der obigen Grafik nur auf die Reduzierung der Kontextwechsel zurückgeführt werden.