Optimierung der Anwendung node.js.

Gegeben: alte http node.js-Anwendung und erhöhte Belastung.

Standardlösungen für das Problem: Beenden Sie Server, schreiben Sie alles von 0 um, optimieren Sie das, was bereits geschrieben wurde.

Versuchen wir, die Optimierung durchzugehen und herauszufinden, wie Anwendungsschwächen gefunden und verbessert werden können. Und vielleicht beschleunigen, ohne eine einzige Codezeile zu berühren :)

Alle Interessierten willkommen unter der Katze!

Lassen Sie uns zunächst eine Leistungstestmethode festlegen. Wir werden an der Anzahl der Anfragen interessiert sein, die in 1 Sekunde bearbeitet werden: rps.

Wir werden die Anwendung im Modus 1 von Worker (1 Prozess) ausführen und die Leistung des alten Codes und des Codes mit Optimierungen messen - absolute Leistung ist nicht wichtig, vergleichende Leistung ist wichtig.

In einer typischen Anwendung mit vielen verschiedenen Routen ist es logisch, zuerst die am meisten geladenen Anforderungen zu finden, deren Verarbeitung die meiste Zeit in Anspruch nimmt. Mit Dienstprogrammen wie Request-Log-Analizer oder vielen ähnlichen können Sie diese Informationen aus den Protokollen extrahieren.

Auf der anderen Seite können Sie eine echte Liste von Anfragen erstellen und alle auflisten (z. B. mit Yandex-Tank) - wir erhalten ein zuverlässiges Lastprofil.

Bei vielen Iterationen der Codeoptimierung ist es jedoch viel bequemer, ein einfacheres und schnelleres Tool und einen bestimmten Anfragetyp zu verwenden (und nach der Optimierung einer Anforderung die nächste zu studieren usw.). Meine Wahl ist wrk . Außerdem ist in meinem Fall die Anzahl der Routen nicht groß - es ist nicht schwierig, alles einzeln zu überprüfen.

Es sollte sofort beachtet werden, dass in Bezug auf das Blockieren von Abfragen, Datenbankerwartungen usw. Die Anwendung ist bereits optimiert, alles hängt von der CPU ab: Während der Tests verbraucht der Worker 100% CPU.

Die verkauften Server verwenden node.js Version 6 - beginnen wir damit:

Anfragen / Sek.: 1210

Wir versuchen es am 8. Knoten:
Anfragen / Sek.: 2308
10. Anmerkung:
Anfragen / Sek.: 2590

Der Unterschied ist offensichtlich. Die Schlüsselrolle spielt hier die Aktualisierung der v8-Version - viel schlecht optimierter v8-Code war in der Vergangenheit. Und um nicht mit den Windmühlen fertig zu werden, die in node.js v8 verschwunden sind, ist es besser, sofort zu aktualisieren und dann die Codeoptimierung durchzuführen.

Wir wenden uns der eigentlichen Suche nach Engpässen zu: Meiner Meinung nach ist Flamegraph das beste Werkzeug dafür. Und mit dem Aufkommen des 0x- Projekts war es sehr einfach, einen Flammengraphen zu erhalten - 0x anstelle von node zu starten: 0x -o yourscript.js, einen Test durchführen, das Skript stoppen, das Ergebnis im Browser anzeigen.

Der Flamegraph des getesteten Codes sieht vor den Optimierungen ungefähr so ​​aus:


Verlassen Sie unter den Filtern die App, deps - nur den Code der Anwendung und der Module von Drittanbietern.

Je breiter der Streifen, desto mehr Zeit wird für die Ausführung dieser Funktion aufgewendet (einschließlich verschachtelter Aufrufe).

Wir werden uns mit dem zentralen, größten Teil befassen.

Zunächst heben wir nicht optimierte Funktionen hervor. Ich habe einige davon in der Anwendung gefunden.

Darüber hinaus sind die Top-Funktionen typische Kandidaten für die Optimierung. Die übrigen Funktionen sind mit relativ gleichmäßigen Schritten ausgerichtet - jede Funktion trägt einen kleinen Teil zu den Verzögerungen bei, es gibt keinen offensichtlichen Anführer.

Dann ist ein einfacher Aktionsalgorithmus möglich: die breitesten Funktionen zu optimieren und von einer zur anderen zu wechseln. Ich habe jedoch einen anderen Ansatz gewählt: Optimierung vom Einstiegspunkt bis zur Anwendung (Anforderungshandler in http.createServer). Anstatt die folgenden Funktionen aufzurufen, beende ich am Ende der untersuchten Funktion die Anforderungsverarbeitung mit einer Dummy-Antwort und untersuche die Leistung dieser bestimmten Funktion. Nach seiner Optimierung bewegt sich die Dummy-Antwort weiter entlang des Aufrufstapels zur nächsten Funktion usw.

Eine bequeme Konsequenz dieses Ansatzes: Sie können rps unter idealen Bedingungen sehen (mit nur einer Startfunktion liegt rps nahe an den maximalen rps der Anwendung hellow world node.js) und bei weiterer Bewegung des Antwortstubs tief in die Anwendung den Beitrag der untersuchten Funktion zum Leistungsabfall beobachten rps-ah.

Also lassen wir nur die Startfunktion, wir bekommen:

Anfragen / Sek.: 16176



Wenn Sie die Kernfilter der Version 8 anschließen, können Sie sehen, dass fast die gesamte untersuchte Funktion aus dem Senden einer Antwort, der Protokollierung und anderen schlecht optimierten Dingen besteht - wir gehen noch weiter.

Wir gehen zu folgender Funktion über:

Anfragen / Sek.: 16111
Nichts hat sich geändert - tauchen Sie weiter ein:
Anfragen / Sek.: 13330


Unser Kunde! Es ist ersichtlich, dass die betroffene getByUrl-Funktion einen signifikanten Teil der Startfunktion einnimmt - was gut mit der RPS-Senkung korreliert.

Wir schauen uns genau an, was darin passiert (Kern einschalten, Version 8):

Es passiert viel ... wir rauchen den Code, optimieren:

for (var i in this.data) { if (this[i]._options.regexp_obj.test(url)) return this[i]; } return null; 

verwandeln in

 let result = null; for (let i=0; i<this.length && !result; i++) { if (this[i]._options.regexp_obj.test(url)) result = this[i]; } 

In diesem Fall ist ein einfaches for viel schneller als for..in

Anfragen abrufen / Sek.: 16015



Optisch „entleert“ sich die Funktion und nimmt einen viel kleineren Teil der Startfunktion ein.
In den detaillierten Informationen zur Funktion wurde auch alles stark vereinfacht:

Wir fahren mit der nächsten Funktion fort.

Anfragen / Sek.: 13316



Diese Funktion verfügt über viele Array-Funktionen und ist trotz der erheblichen Beschleunigung in neueren Versionen von node.js immer noch langsamer als einfache Schleifen: change [] .map und filter. zu regelmäßig für und bekommen

Anfragen / Sek.: 15067



Und so immer wieder für jede nachfolgende Funktion.

Einige weitere nützliche Optimierungen: Für Hashes mit einem sich dynamisch ändernden Schlüsselsatz kann new Map () 40% schneller sein als reguläres {};

Math.round (el * 100) / 100 ist 2-mal schneller als toFixed (2).

Im Flamegraph für Core- und v8-Funktionen sehen Sie sowohl obskure Einträge als auch StringPrototypeSplit oder v8 :: internal :: Runtime_StringToNumber. Wenn dies ein wesentlicher Teil der Codeausführung ist, versuchen Sie beispielsweise, Code zu optimieren, der diese nicht ausführt Operationen.

Wenn Sie beispielsweise split durch mehrere indexOf- und substring-Aufrufe ersetzen, können Sie doppelte Leistungssteigerungen erzielen.

Ein separates großes und komplexes Thema ist die Jit-Optimierung bzw. deoptimierte Funktionen.
Wenn es einen großen Anteil solcher Funktionen gibt, müssen sie behandelt werden.

Eine sorgfältige Untersuchung der Ausgabe des Knotens --trace_file_names --trace_opt_verbose --trace-deopt --trace_opt kann hier helfen.

Zum Beispiel Zeilen des Formulars

deoptimizing (DEOPT soft): begin 0x2bcf38b2d079 <JS-Funktion getTime ... Unzureichende Typrückmeldung für den binären Betrieb führte zur Leitung

Rückgabewert> = 10? val: '0' + val;

Ersatz für

return (val> = 10? '': '0') + val;

korrigierte die Situation.

Es gibt viele Informationen für die alte v8-Engine aus Gründen und Möglichkeiten, um die Deoptimierung von Funktionen zu bekämpfen:

github.com/P0lip/v8-deoptimize-reasons - Liste,
www.netguru.co/blog/tracing-patterns-hinder-performance - Analyse typischer Ursachen,
www.html5rocks.com/de/tutorials/speed/v8 - über Optimierungen für v8 denke ich, dass dies auch für die aktuelle v8-Engine gilt.

Viele der Probleme sind für die neue Version 8 jedoch nicht mehr relevant.

Wie auch immer, nach all den Optimierungen gelang es mir, Requests / sec: 9971 zu erhalten , d. H. Aufgrund des Übergangs zur neuesten Version von node.js wird die Beschleunigung etwa zweimal und aufgrund der Codeoptimierung etwa viermal beschleunigt.

Ich hoffe, diese Erfahrung wird jemand anderem nützlich sein.

Source: https://habr.com/ru/post/de414541/


All Articles