Etwa 30x Concurrency Boost in Node.js

Wie lässt sich die Parallelität im Node.js-Dienst, der in der Produktion verwendet wird, am besten erhöhen? Diese Frage musste mein Team vor ein paar Monaten beantworten.

Wir haben 4000 Node-Container (oder "Arbeiter") auf den Markt gebracht, die den Betrieb unseres Integrationsdienstes mit Banken sicherstellen. Der Dienst wurde ursprünglich so konzipiert, dass jeder Mitarbeiter jeweils nur eine Anfrage bearbeiten kann. Dies verringerte die Auswirkung solcher Vorgänge auf das System, die den Ereigniszyklus unerwartet blockieren und es uns ermöglichten, Unterschiede in der Ressourcennutzung durch verschiedene ähnliche Vorgänge zu ignorieren. Da unsere Kapazitäten jedoch auf die gleichzeitige Ausführung von nur 4.000 Anforderungen beschränkt waren, konnte das System nicht angemessen skaliert werden. Die Reaktionsgeschwindigkeit auf die meisten Anfragen hing nicht von der Kapazität des Geräts ab, sondern von den Fähigkeiten des Netzwerks. Aus diesem Grund könnten wir das System verbessern und die Supportkosten senken, wenn wir einen Weg finden würden, Anfragen zuverlässig parallel zu bearbeiten.



Nachdem wir dieses Problem untersucht haben, konnten wir keinen guten Leitfaden finden, der den Übergang von "mangelnder Parallelität" in Node.js zu einem "hohen Grad an Parallelität" diskutieren würde. Aus diesem Grund haben wir eine eigene Migrationsstrategie entwickelt, die auf sorgfältiger Planung, guten Tools, Überwachungstools und einer gesunden Dosis Debugging basiert. Infolgedessen ist es uns gelungen, die Parallelität unseres Systems um das 30-fache zu erhöhen. Dies entspricht einer Reduzierung der Kosten für die Wartung des Systems um ca. 300.000 US-Dollar pro Jahr.

Dieses Material widmet sich der Geschichte, wie wir die Produktivität und Effektivität unserer Node.js-Mitarbeiter gesteigert haben und was wir auf diese Weise gelernt haben.

Warum haben wir uns entschieden, in Parallelität zu investieren?


Es mag überraschen, dass wir ohne Parallelität zu solchen Dimensionen herangewachsen sind. Wie ist es dazu gekommen? Nur 10% der von Plaid-Tools ausgeführten Datenverarbeitungsvorgänge werden von Benutzern initiiert, die an Computern sitzen und ihre Konten mit der Anwendung verbunden haben. Der Rest sind Transaktionen zum regelmäßigen Aktualisieren von Transaktionen, die ohne Anwesenheit des Benutzers ausgeführt werden. Dem Lastausgleichssystem, das wir verwenden, wurde eine Logik hinzugefügt, um sicherzustellen, dass Anforderungen von Benutzern Vorrang vor Transaktionsaktualisierungsanforderungen haben. Dies ermöglichte es uns, Aktivitätsschübe von API-Zugriffsvorgängen in 1000% oder noch mehr zu bewältigen. Dies geschah durch Transaktionen zur Aktualisierung von Daten.

Obwohl dieses Kompromissschema schon lange funktioniert hatte, war es möglich, mehrere unangenehme Momente darin zu erkennen. Wir wussten, dass sie letztendlich die Zuverlässigkeit des Dienstes beeinträchtigen könnten.

  • Die Spitzenwerte der API-Anforderungen von Clients wurden immer höher. Wir waren besorgt, dass ein enormer Aktivitätsanstieg unsere Fähigkeiten zur Abfrageverarbeitung beeinträchtigen könnte.
  • Der plötzliche Anstieg der Verzögerungen bei der Erfüllung von Anfragen an Banken führte auch zu einem Rückgang der Arbeiterkapazität. Aufgrund der Tatsache, dass Banken eine Vielzahl von Infrastrukturlösungen verwenden, legen wir sehr konservative Zeitlimits für ausgehende Anforderungen fest. Infolgedessen kann es einige Minuten dauern, bis bestimmte Daten geladen sind. Wenn es passieren würde, dass die Verzögerungen bei der Erledigung vieler Anfragen an Banken plötzlich stark zunehmen würden, würde sich herausstellen, dass viele Arbeiter einfach festsitzen und auf Antworten warten.
  • Die Bereitstellung in ECS ist zu langsam geworden, und obwohl wir die Bereitstellungsgeschwindigkeit des Systems verbessert haben, wollten wir die Clustergröße nicht weiter erhöhen.

Wir haben beschlossen, dass die beste Möglichkeit zur Behebung von Anwendungsengpässen und zur Erhöhung der Systemzuverlässigkeit darin besteht, die Parallelität bei der Verarbeitung von Anforderungen zu erhöhen. Darüber hinaus erhofften wir uns als Nebeneffekt eine Reduzierung der Infrastrukturkosten und die Implementierung besserer Tools zur Überwachung des Systems. Sowohl das als auch ein anderes in der Zukunft würden Früchte tragen.

Wie wir Updates eingeführt haben, um die Zuverlässigkeit zu gewährleisten


▍Werkzeuge und Überwachung


Wir haben unseren eigenen Load Balancer, der Anfragen an die Mitarbeiter von Node.j weiterleitet. Jeder Worker führt einen gRPC-Server aus, mit dem Anforderungen verarbeitet werden. Der Worker teilt dem Load Balancer mit, dass er verfügbar ist. Dies bedeutet, dass das Hinzufügen von Parallelität zum System darauf hinausläuft, nur einige Codezeilen zu ändern. Der Arbeiter muss nämlich, anstatt unzugänglich zu werden, nachdem die Anfrage an ihn gerichtet wurde, melden, dass er verfügbar ist, bis festgestellt wird, dass er damit beschäftigt ist, die von ihm empfangenen N Anfragen (jede von ihnen) zu bearbeiten vertreten durch ein eigenes Promise-Objekt).

In der Tat ist nicht alles so einfach. Bei der Bereitstellung von Systemaktualisierungen betrachten wir es immer als unser Hauptziel, die Zuverlässigkeit aufrechtzuerhalten. Aus diesem Grund konnten wir das System nicht nur nach dem YOLO-Prinzip in den Modus für die parallele Abfrageverarbeitung versetzen. Wir haben erwartet, dass ein solches System-Upgrade besonders riskant ist. Tatsache ist, dass dies eine unvorhersehbare Auswirkung auf die Verwendung des Prozessors, den Arbeitsspeicher und Verzögerungen bei der Ausführung von Aufgaben haben würde. Da die in Node.js verwendete V8-Engine Aufgaben in der Ereignisschleife verarbeitet, war unsere Hauptsorge, dass sich herausstellen könnte, dass wir zu viel Arbeit in der Ereignisschleife leisten und damit den Systemdurchsatz verringern.

Um diese Risiken zu minimieren, haben wir bereits vor dem Produktionsstart des ersten Parallelarbeiters die Verfügbarkeit der folgenden Überwachungstools und Tools im System sichergestellt:

  • Der von uns bereits verwendete ELK-Stack lieferte uns eine ausreichende Menge an protokollierten Informationen, die hilfreich sein können, um schnell herauszufinden, was im System geschieht.
  • Wir haben dem System mehrere Prometheus- Metriken hinzugefügt. Einschließlich der folgenden:

    • V8-Heap-Größe, die mit process.memoryUsage() .
    • Informationen zu Garbage Collection-Vorgängen mit dem Paket gc-stats .
    • Daten zur Zeit, die für die Erledigung von Aufgaben benötigt wird, gruppiert nach Art der Vorgänge im Zusammenhang mit der Integration mit Banken und nach dem Grad der Parallelität. Wir brauchten dies, um zuverlässig zu messen, wie sich die Parallelität auf den Systemdurchsatz auswirkt.
  • Wir haben das Grafana Control Panel entwickelt, um den Grad der Auswirkung von Parallelität auf das System zu untersuchen.
  • Für uns war es äußerst wichtig, das Verhalten der Anwendung zu ändern, ohne den Dienst erneut bereitstellen zu müssen. Aus diesem Grund haben wir eine Reihe von LaunchDarkly- Flags erstellt, mit denen verschiedene Parameter gesteuert werden können. Bei diesem Ansatz ermöglichte die Auswahl der Parameter der Arbeiter, die so berechnet wurden, dass sie das maximale Niveau der Parallelität erreichten, die schnelle Durchführung von Experimenten und die Ermittlung der besten Parameter, wobei einige Minuten dafür aufgewendet wurden.
  • Um herauszufinden, wie verschiedene Teile der Anwendung den Prozessor belasten, haben wir die Datenerfassungstools für den Produktionsservice integriert, auf deren Grundlage Flammendiagramme erstellt wurden.

    • Wir haben das 0x-Paket verwendet, weil sich die Tools von Node.j einfach in unseren Service integrieren ließen und weil die endgültige Visualisierung der HTML-Daten die Suche unterstützte und uns einen guten Detaillierungsgrad lieferte.
    • Wir haben dem System einen Profiling-Modus hinzugefügt, als der Worker mit aktiviertem 0x-Paket startete und beim Beenden die endgültigen Daten in S3 notierte. Dann könnten wir die benötigten Protokolle von S3 herunterladen und sie lokal mit einem Befehl der Form 0x --visualize-only ./flamegraph .
    • Wir haben in einem bestimmten Zeitraum damit begonnen, nur für einen Mitarbeiter ein Profil zu erstellen. Durch die Profilerstellung wird der Ressourcenverbrauch erhöht und die Produktivität verringert. Daher möchten wir diese negativen Auswirkungen auf einen einzelnen Mitarbeiter beschränken.

▍ Starten Sie die Bereitstellung


Nach Abschluss der Vorbereitungen haben wir einen neuen ECS-Cluster für „Parallelarbeiter“ erstellt. Dies waren die Worker, die mithilfe von LaunchDarkly-Flags ihre maximale Parallelität dynamisch festgelegt haben.

Unser Systembereitstellungsplan sah eine schrittweise Umleitung des wachsenden Verkehrsvolumens vom alten auf den neuen Cluster vor. Währenddessen sollten wir die Leistung des neuen Clusters genau überwachen. Wir planten, bei jeder Laststufe die Parallelität der einzelnen Arbeiter zu erhöhen, um sie auf den Maximalwert zu bringen, bei dem die Dauer der Aufgaben oder die Verschlechterung anderer Indikatoren nicht zugenommen hat. Wenn wir in Schwierigkeiten wären, könnten wir den Verkehr innerhalb weniger Sekunden dynamisch auf den alten Cluster umleiten.

Wie erwartet hatten wir einige knifflige Probleme. Wir mussten sie untersuchen und beseitigen, um den korrekten Betrieb des aktualisierten Systems sicherzustellen. Hier begann der Spaß.

Erweitern, Erkunden, Wiederholen


▍Erhöhen der maximalen Heap-Größe von Node.js


Als wir mit der Bereitstellung des neuen Systems begannen, erhielten wir Benachrichtigungen über den Abschluss von Aufgaben mit einem Beendigungscode ungleich Null. Na was soll ich sagen - ein vielversprechender Anfang. Dann haben wir in Kibana begraben und das nötige Protokoll gefunden:

 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - Javascript heap out of memory 1: node::Abort() 2: node::FatalException(v8::Isolate*, v8::Local, v8::Local) 3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) 4: v8::internal::Factory::NewFixedArray(int, v8::internal::PretenureFlag) 

Es erinnerte an die Auswirkungen von Speicherlecks, die bereits aufgetreten waren, als der Prozess unerwartet beendet wurde, und gab eine ähnliche Fehlermeldung aus. Dies schien durchaus zu erwarten: Eine Erhöhung des Parallelitätsniveaus führt zu einer Erhöhung der Speichernutzung.

Wir empfehlen, die maximale Größe des Node.js-Heapspeichers, der standardmäßig auf 1,7 GB festgelegt ist, zu erhöhen, um dieses Problem zu beheben. Dann haben wir Node.js gestartet und die maximale Größe des --max-old-space-size=6144 auf 6 GB festgelegt (mithilfe des Befehlszeilenflags --max-old-space-size=6144 ). Dies war der größte Wert, der für unsere EC2-Instanzen geeignet war. Zu unserer Freude konnten wir mit einem solchen Schritt den oben genannten Fehler in der Produktion bewältigen.

▍ Identifizierung von Speicherengpässen


Nachdem wir das Problem mit der Speicherzuweisung gelöst hatten, stellten wir auf Parallelarbeiter einen schlechten Durchsatz von Aufgaben fest. Gleichzeitig erregte eine der Grafiken auf dem Bedienfeld sofort unsere Aufmerksamkeit. Dies war ein Bericht darüber, wie parallele Worker-Prozesse einen Haufen verwenden.


Heap-Nutzung

Einige der Kurven dieses Diagramms stiegen kontinuierlich an - bis sie sich auf der Höhe der maximalen Heap-Größe in fast horizontale Linien verwandelten. Uns hat es wirklich nicht gefallen.

Wir haben in Prometheus Systemmetriken verwendet, um Lecks in einem Dateideskriptor oder einem Netzwerk-Socket von den Ursachen für ein solches Systemverhalten auszuschließen. Unsere am besten geeignete Annahme war, dass die Speicherbereinigung für alte Objekte nicht oft genug durchgeführt wurde. Dies könnte dazu führen, dass der Worker bei der Bearbeitung der Aufgaben immer mehr Speicherplatz für bereits unnötige Objekte reserviert. Wir gingen davon aus, dass der Betrieb des Systems, während dessen sein Durchsatz abnimmt, folgendermaßen aussieht:

  • Der Arbeiter erhält eine neue Aufgabe und führt bestimmte Aktionen aus.
  • Während der Ausführung der Aufgabe wird Speicher auf dem Heap für Objekte zugewiesen.
  • Aufgrund der Tatsache, dass eine bestimmte Operation, mit der sie nach dem Prinzip „erledigt und vergessen“ arbeiten (damals war noch nicht klar, welche), unvollständig ist, werden Verweise auf Objekte auch nach Abschluss der Aufgabe gespeichert.
  • Die Speicherbereinigung wird verlangsamt, da der V8 immer mehr Objekte auf dem Heap scannen muss.
  • Da V8 ein Garbage Collection-System implementiert, das nach dem Stop-the-World- Schema (Anhalten des Programms für die Dauer der Garbage Collection) arbeitet, erhalten neue Tasks zwangsläufig weniger Prozessorzeit, was den Durchsatz des Workers verringert.

Wir haben begonnen, in unserem Code nach Operationen zu suchen, die nach dem Prinzip „erledigt und vergessen“ ausgeführt werden. Sie werden auch als "Floating Promises" ("schwimmendes Versprechen") bezeichnet. Es war einfach - es genügte, die Zeilen zu finden, in denen die Linter-Regel " Keine schwebenden Versprechungen" deaktiviert war. Eine Methode hat unsere Aufmerksamkeit erregt. Er rief compressAndUploadDebuggingPayload ohne auf Ergebnisse zu warten. Es schien, als könne ein solcher Anruf auch nach Abschluss der Bearbeitung der Aufgabe noch lange Zeit problemlos fortgesetzt werden.

 const postTaskDebugging = async (data: TypedData) => {    const payload = await generateDebuggingPayload(data);       //       ,    //        .    // tslint:disable-next-line:no-floating-promises    compressAndUploadDebuggingPayload(payload)        .catch((err) => logger.error('failed to upload data', err)); } 

Wir wollten die Hypothese testen, dass solche schwebenden Versprechungen die Hauptursache für Probleme sind. Können wir die Geschwindigkeit von Aufgaben verbessern, wenn Sie diese Herausforderungen, die den ordnungsgemäßen Betrieb des Systems nicht beeinträchtigten, nicht erfüllen? So sahen die Informationen zur Heap-Nutzung aus, nachdem wir vorübergehend die Aufrufe von postTaskDebugging .


Verwenden des Heapspeichers nach dem Deaktivieren von postTaskDebugging

Es stellte sich heraus! Jetzt bleibt der Heap-Auslastungsgrad bei Parallelarbeitern über einen langen Zeitraum stabil.

Es bestand das Gefühl, dass sich im System nach und nach "Schulden" von compressAndUploadDebuggingPayload Aufrufen ansammelten, als die Aufgaben erledigt waren. Wenn der Arbeiter Aufgaben schneller erhielt, als er diese "Schulden" "abbezahlen" konnte, wurden die Objekte, denen der Speicher zugewiesen wurde, keinen Speicherbereinigungsoperationen unterzogen. Dies führte dazu, dass der Haufen bis ganz nach oben gefüllt wurde, was wir oben bei der Analyse des vorherigen Diagramms berücksichtigt haben.

Wir begannen uns zu fragen, was diese schwebenden Versprechen so langsam machte. Wir wollten compressAndUploadDebuggingPayload nicht vollständig aus dem Code entfernen, da dieser Aufruf äußerst wichtig war, damit unsere Ingenieure Produktionsaufgaben auf ihren lokalen Computern debuggen konnten. Aus technischer Sicht könnten wir das Problem lösen, indem wir auf die Ergebnisse dieses Aufrufs warten und anschließend die Aufgabe erledigen und so das schwebende Versprechen loswerden. Dies würde jedoch die Ausführungszeit jeder von uns bearbeiteten Aufgabe erheblich verlängern.

Nachdem wir beschlossen hatten, eine solche Lösung des Problems nur als letzten Ausweg zu verwenden, begannen wir über eine Optimierung des Codes nachzudenken. Wie kann dieser Vorgang beschleunigt werden?

▍ Engpass S3 beheben


Die Logik von compressAndUploadDebuggingPayload leicht zu verstehen. Hier komprimieren wir die Debug-Daten und sie können sehr groß sein, da sie den Netzwerkverkehr enthalten. Dann laden wir die komprimierten Daten in S3 hoch.

 export const compressAndUploadDebuggingPayload = async (    logger: Logger,    data: any, ) => {    const compressionStart = Date.now();    const base64CompressedData = await streamToString(        bfj.streamify(data)            .pipe(zlib.createDeflate())            .pipe(new b64.Encoder()),    );    logger.trace('finished compressing data', {        compression_time_ms: Date.now() - compressionStart,    );           const uploadStart = Date.now();    s3Client.upload({        Body: base64CompressedData,        Bucket: bucket,        Key: key,    });    logger.trace('finished uploading data', {        upload_time_ms: Date.now() - uploadStart,    ); } 

Aus den Kibana-Protokollen ging hervor, dass das Herunterladen von Daten in S3, selbst wenn das Volumen gering ist, viel Zeit in Anspruch nimmt. Anfangs dachten wir nicht, dass Sockets zu einem Engpass im System werden könnten, da der Standard-HTTPS-Agent von Node.js den Parameter maxSockets auf Infinity . Am Ende haben wir jedoch die AWS-Dokumentation zu Node.js gelesen und etwas Überraschendes für uns gefunden: Der S3-Client reduziert den Wert des Parameters maxSockets auf 50 . Selbstverständlich kann dieses Verhalten nicht als intuitiv bezeichnet werden.

Da wir den Mitarbeiter in einen Zustand gebracht haben, in dem im Wettbewerbsmodus mehr als 50 Aufgaben ausgeführt wurden, wurde der Download-Schritt zu einem Engpass: Er sah vor, dass darauf gewartet wurde, dass der Socket freigegeben wurde, um Daten in S3 hochzuladen. Wir haben die Datenladezeit verbessert, indem wir den S3-Client-Initialisierungscode wie folgt geändert haben:

 const s3Client = new AWS.S3({    httpOptions: {        agent: new https.Agent({            //                 //          S3.            maxSockets: 1024 * 20,        }),    },    region, }); 

▍ Beschleunigung der JSON-Serialisierung


Verbesserungen des S3-Codes haben das Wachstum der Heap-Größe verlangsamt, aber nicht zu einer vollständigen Lösung des Problems geführt. Es gab ein weiteres offensichtliches Ärgernis: Nach unseren Messwerten dauerte der Datenkomprimierungsschritt im obigen Code einmal 4 Minuten. Es war viel länger als die übliche Bearbeitungszeit, die 4 Sekunden beträgt. Da wir unseren Augen nicht trauen und nicht verstehen, wie lange dies 4 Minuten dauern kann, haben wir beschlossen, lokale Benchmarks zu verwenden und den Slow-Code-Block zu optimieren.

Die Datenkomprimierung besteht aus drei Schritten (hier werden Node.js- Streams verwendet, um die Speichernutzung zu begrenzen). In der ersten Phase werden JSON-Daten für Zeichenfolgen generiert, in der zweiten Phase werden Daten mit zlib komprimiert und in der dritten Phase in Base64-Codierung konvertiert. Wir hatten den Verdacht, dass die Quelle der Probleme die Bibliothek eines Drittanbieters sein könnte, mit der wir JSON-Zeichenfolgen generieren - bfj . Wir haben ein Skript geschrieben, das die Leistung verschiedener Bibliotheken zum Generieren von JSON-Zeichenfolgendaten mithilfe von Streams untersucht (den entsprechenden Code finden Sie hier ). Es stellte sich heraus, dass das von uns verwendete Big Friendly JSON-Paket überhaupt nicht freundlich war. Schauen Sie sich die Ergebnisse einiger Messungen an, die während des Experiments durchgeführt wurden:

 benchBFJ*100:    67652.616ms benchJSONStream*100: 14094.825ms 

Erstaunliche Ergebnisse. Selbst in einem einfachen Test erwies sich das bfj-Paket als fünfmal langsamer als das andere Paket, JSONStream. Als wir das herausfanden, stellten wir bfj schnell auf JSONStream um und stellten sofort eine deutliche Leistungssteigerung fest.

▍ Reduzieren der für die Speicherbereinigung erforderlichen Zeit


Nachdem wir die Probleme mit dem Gedächtnis gelöst hatten, haben wir begonnen, auf den Zeitunterschied zu achten, der für die Bearbeitung von Aufgaben des gleichen Typs zwischen regulären und parallelen Mitarbeitern erforderlich ist. Dieser Vergleich war völlig legitim, nach seinen Ergebnissen konnten wir die Wirksamkeit des neuen Systems beurteilen. Wenn also das Verhältnis zwischen regulären und parallelen Mitarbeitern ungefähr 1 wäre, würde dies uns die Gewissheit geben, dass wir den Verkehr sicher zu diesen Mitarbeitern umleiten können. Während der ersten Systemstarts sah das entsprechende Diagramm im Grafana-Bedienfeld jedoch wie das unten gezeigte aus.


Das Verhältnis der Ausführungszeit von Aufgaben durch konventionelle und Parallelarbeiter

Bitte beachten Sie, dass der Indikator manchmal im Bereich von 8: 1 liegt, obwohl der durchschnittliche Grad der Parallelisierung von Aufgaben relativ niedrig ist und im Bereich von 30 liegt. Wir waren uns bewusst, dass die Aufgaben, die wir im Zusammenhang mit der Interaktion mit Banken lösen, keine schaffen starke Belastung der Prozessoren. Wir wussten auch, dass unsere „parallelen“ Container in keiner Weise eingeschränkt sind. Da wir nicht wussten, wo wir nach der Ursache des Problems suchen sollten, haben wir Materialien zur Optimierung von Node.js-Projekten gelesen. Trotz der geringen Anzahl solcher Artikel sind wir auf dieses Material gestoßen, das sich mit der Erreichung von 600.000 wettbewerbsfähigen Web-Socket-Verbindungen in Node.js befasst.

Insbesondere wurde auf die Verwendung des --nouse-idle-notification . Können unsere Node.js-Prozesse so viel Zeit damit verbringen, Müll zu sammeln? Das gc-stats-Paket hat uns übrigens die Möglichkeit gegeben, die durchschnittliche Zeit für die Müllabfuhr zu betrachten.


Analyse der für die Müllabfuhr aufgewendeten Zeit

Es bestand das Gefühl, dass unsere Prozesse etwa 30% der Zeit mit der Sammlung von Müll mithilfe des Scavenge-Algorithmus verbrachten. Hier werden die technischen Details zu den verschiedenen Arten der Speicherbereinigung in Node.js nicht beschrieben. Wenn Sie sich für dieses Thema interessieren, schauen Sie sich dieses Material an. Die Essenz des Scavenge-Algorithmus besteht darin, dass die Speicherbereinigung häufig gestartet wird, um den Speicher zu löschen, der von kleinen Objekten im Node.js-Heap belegt wird, der als "neuer Speicher" bezeichnet wird.

Es stellte sich also heraus, dass in unseren Node.js-Prozessen die Garbage Collection zu oft startet. Kann ich die V8-Garbage Collection deaktivieren und selbst ausführen? Gibt es eine Möglichkeit, die Häufigkeit eines Garbage Collection-Aufrufs zu verringern ? Es stellte sich heraus, dass die erste der oben genannten Maßnahmen nicht durchgeführt werden kann, aber die letzte - es ist möglich! Wir können einfach die Größe des Bereichs "new space" erhöhen, indem wir die Grenze des Bereichs "semi space" in Node.js mit dem Befehlszeilenflag --max-semi-space-size=1024 . Auf diese Weise können Sie mehr Speicherzuordnungsvorgänge für kurzlebige Objekte ausführen, bis der V8 mit der Garbage Collection beginnt. Infolgedessen wird die Häufigkeit des Startens solcher Operationen verringert.


Ergebnisse der Speicherbereinigungsoptimierung

Ein weiterer Sieg! Die Vergrößerung des Bereichs „neuer Speicherplatz“ führte zu einer signifikanten Reduzierung des Zeitaufwands für die Speicherbereinigung mithilfe des Scavenge-Algorithmus von 30% auf 2%.

▍Optimieren Sie die Prozessorauslastung


Nachdem all diese Arbeiten erledigt waren, passte das Ergebnis zu uns. Aufgaben, die bei Parallelarbeitern mit einer 20-fachen Parallelisierung der Arbeit ausgeführt wurden, funktionierten fast so schnell wie Aufgaben, die bei separaten Arbeitern separat ausgeführt wurden. Es schien uns, dass wir alle Engpässe überwunden hatten, aber wir wussten immer noch nicht genau, welche Vorgänge das System in der Produktion verlangsamten. Da es im System keine Stellen mehr gab, die offensichtlich optimiert werden mussten, beschlossen wir zu untersuchen, wie die Mitarbeiter die Prozessorressourcen nutzen.

Basierend auf den Daten, die von einem unserer Parallelarbeiter gesammelt wurden, wurde ein feuriger Zeitplan erstellt. Wir hatten eine ordentliche Visualisierung zur Verfügung, mit der wir an der lokalen Maschine arbeiten konnten. Ja, hier ist ein interessantes Detail: Die Größe dieser Daten betrug 60 MB. Dies haben wir gesehen, als wir nach dem Wort logger im 0x-Diagramm des Fiery gesucht haben.


Datenanalyse mit 0x Tools

Die in den Spalten hervorgehobenen blaugrünen Bereiche zeigen an, dass mindestens 15% der Prozessorzeit für die Erstellung des Worker-Protokolls aufgewendet wurden. Infolgedessen konnten wir diese Zeit um 75% reduzieren. Richtig, die Geschichte, wie wir das gemacht haben, ist in einem separaten Artikel zusammengefasst. (Hinweis: Wir haben reguläre Ausdrücke verwendet und viel mit Eigenschaften gearbeitet).

Nach dieser Optimierung konnten wir gleichzeitig bis zu 30 Aufgaben in einem Mitarbeiter bearbeiten, ohne die Systemleistung zu beeinträchtigen.

Zusammenfassung


Durch die Umstellung auf Parallelarbeiter konnten die jährlichen Kosten für EC2 um rund 300.000 US-Dollar gesenkt und die Systemarchitektur erheblich vereinfacht werden. Jetzt verbrauchen wir in der Produktion etwa 30-mal weniger Behälter als zuvor. Unser System ist widerstandsfähiger gegen Verzögerungen bei der Verarbeitung ausgehender Anforderungen und gegen API-Spitzenanforderungen von Benutzern.

Bei der Parallelisierung unseres Integrationsdienstes mit Banken haben wir viele neue Erkenntnisse gewonnen:

  • Unterschätzen Sie niemals die Bedeutung von Systemmetriken auf niedriger Ebene. Die Möglichkeit, Daten im Zusammenhang mit der Speicherbereinigung und der Speichernutzung zu überwachen, hat uns bei der Bereitstellung und Fertigstellung des Systems sehr geholfen.
  • Flammende Grafiken sind ein großartiges Werkzeug. Nachdem wir gelernt haben, wie man sie verwendet, können wir mit ihrer Hilfe leicht neue Engpässe im System identifizieren.
  • Durch das Verständnis der Laufzeitmechanismen von Node.j konnten wir besseren Code schreiben. Als wir beispielsweise wussten, wie V8 Speicher für Objekte zuweist und wie die Garbage Collection funktioniert, sahen wir den Sinn darin, die Wiederverwendungstechnik von Objekten so weit wie möglich zu verwenden. Manchmal müssen Sie, um all dies besser zu verstehen, direkt mit V8 arbeiten oder mit Node.js.-Befehlszeilenflags experimentieren.
  • Es ist sehr wichtig, die Dokumentation für alle Mechanismen, aus denen das System besteht, sorgfältig zu lesen. Wir vertrauten den Daten maxSocketin der Dokumentation zu Node.js, aber nach eingehender Recherche stellte sich heraus, dass sich das Standardverhalten von Node.js in AWS ändert. Vielleicht kann in jedem Projekt, das auf der Infrastruktur eines anderen basiert, etwas Ähnliches passieren.

Sehr geehrte Leser! Wie optimieren Sie Ihre Node.js-Projekte?

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


All Articles