Heim >Web-Frontend >js-Tutorial >Hohe Speichernutzung in Node.js aufspüren

Hohe Speichernutzung in Node.js aufspüren

Patricia Arquette
Patricia ArquetteOriginal
2024-12-17 00:40:25817Durchsuche

In diesem Artikel werde ich meinen Ansatz zum Aufspüren und Beheben einer hohen Speichernutzung in Node.js teilen.

Inhalt

  • Kontext
  • Ansatz
    • Verstehen Sie den Code
    • Replizieren Sie das Problem isoliert
    • Erfassen Sie Profile von Staging-Diensten
    • Überprüfen Sie den Fix
  • Ergebnisse
  • Fazit

Kontext

Kürzlich habe ich ein Ticket mit dem Titel „Speicherleckproblem in Bibliothek x beheben“ erhalten. Die Beschreibung enthielt ein Datadog-Dashboard, das ein Dutzend Dienste zeigte, die unter hoher Speicherauslastung litten und schließlich mit OOM-Fehlern (nicht genügend Speicher) abstürzten, und alle hatten die x-Bibliothek gemeinsam.

Ich wurde erst vor kurzem (<2 Wochen) in die Codebasis eingeführt, was die Aufgabe zu einer Herausforderung machte und es auch wert war, geteilt zu werden.

Ich begann mit zwei Informationen zu arbeiten:

  • Es gibt eine Bibliothek, die von allen Diensten verwendet wird, die eine hohe Speicherauslastung verursacht und Redis beinhaltet (Redis war im Namen der Bibliothek enthalten).
  • Die Liste der betroffenen Dienste.

Unten ist das Dashboard, das mit dem Ticket verknüpft war:

Tracking down high memory usage in Node.js

Dienste liefen auf Kubernetes und es war offensichtlich, dass Dienste im Laufe der Zeit Speicher ansammelten, bis sie das Speicherlimit erreichten, abstürzten (Speicher zurückforderten) und neu starteten.

Ansatz

In diesem Abschnitt werde ich mitteilen, wie ich an die vorliegende Aufgabe herangegangen bin, indem ich den Schuldigen für die hohe Speichernutzung identifiziert und ihn später behoben habe.

Verstehe den Code

Da ich relativ neu in der Codebasis war, wollte ich zunächst den Code verstehen, was die betreffende Bibliothek tat und wie sie verwendet werden sollte, in der Hoffnung, dass es mit diesem Prozess einfacher wäre, das Problem zu identifizieren. Leider gab es keine ordnungsgemäße Dokumentation, aber durch das Lesen des Codes und die Suche, wie Dienste die Bibliothek nutzen, konnte ich den Kern davon verstehen. Es handelte sich um eine Bibliothek, die sich um Redis-Streams drehte und praktische Schnittstellen für die Produktion und Nutzung von Ereignissen bereitstellte. Nachdem ich anderthalb Tage damit verbracht habe, den Code zu lesen, war ich aufgrund der Codestruktur und -komplexität (viel Klassenvererbung und RXJS, mit denen ich nicht vertraut bin) nicht in der Lage, alle Details und den Datenfluss zu erfassen.

Also beschloss ich, eine Lesepause einzulegen und zu versuchen, das Problem zu erkennen, während ich den Code in Aktion beobachtete und Telemetriedaten sammelte.

Replizieren Sie das Problem isoliert

Da keine Profilierungsdaten verfügbar waren (z. B. kontinuierliche Profilierung), die mir bei der weiteren Untersuchung helfen würden, habe ich beschlossen, das Problem lokal zu reproduzieren und zu versuchen, Speicherprofile zu erfassen.

Ich habe ein paar Möglichkeiten zum Erfassen von Speicherprofilen in Node.js gefunden:

  • Heap Snapshot verwenden
  • Heap Profiler verwenden
  • Leistungsprofilierung JavaScript
  • Clinic.js

Da ich keine Hinweise darauf hatte, wo ich suchen sollte, beschloss ich, den meiner Meinung nach „datenintensivsten“ Teil der Bibliothek auszuführen, den Redis-Streams-Produzenten und -Konsumenten. Ich habe zwei einfache Dienste erstellt, die Daten aus einem Redis-Stream erzeugen und verbrauchen würden, und habe mit der Erfassung von Speicherprofilen und dem Vergleich der Ergebnisse im Zeitverlauf fortgefahren. Leider konnte ich nach ein paar Stunden Auslastung der Dienste und Vergleich der Profile keinen Unterschied im Speicherverbrauch bei einem der beiden Dienste feststellen, alles sah normal aus. Die Bibliothek stellte eine Reihe verschiedener Schnittstellen und Möglichkeiten zur Interaktion mit den Redis-Streams bereit. Mir wurde klar, dass es komplizierter sein würde, das Problem zu reproduzieren, als ich erwartet hatte, insbesondere angesichts meiner begrenzten domänenspezifischen Kenntnisse der tatsächlichen Dienste.

Die Frage war also: Wie kann ich den richtigen Zeitpunkt und die richtigen Bedingungen finden, um den Speicherverlust zu erfassen?

Erfassen Sie Profile von Staging-Diensten

Wie bereits erwähnt, wäre die einfachste und bequemste Möglichkeit, Speicherprofile zu erfassen, eine kontinuierliche Profilierung der tatsächlich betroffenen Dienste, eine Option, die ich nicht hatte. Ich begann zu untersuchen, wie ich zumindest unsere Staging-Dienste (die mit dem gleichen hohen Speicherverbrauch konfrontiert waren) nutzen könnte, um die von mir benötigten Daten ohne zusätzlichen Aufwand zu erfassen.

Ich begann nach einer Möglichkeit zu suchen, Chrome DevTools mit einem der laufenden Pods zu verbinden und im Laufe der Zeit Heap-Snapshots zu erfassen. Ich wusste, dass der Speicherverlust beim Staging auftrat. Wenn ich also diese Daten erfassen könnte, hoffte ich, dass ich zumindest einige der Hotspots erkennen könnte. Zu meiner Überraschung gibt es eine Möglichkeit, genau das zu tun.

Der Prozess dafür

  • Aktivieren Sie den Node.js-Debugger auf dem Pod, indem Sie ein SIGUSR1-Signal an den Knotenprozess auf Ihrem Pod senden.
kubectl exec -it <nodejs-pod-name> -- kill -SIGUSR1 <node-process-id>




</p>
<p><em>Mehr über Node.js-Signale unter Signal Events</em></p>

<p>Bei Erfolg sollte ein Protokoll Ihres Dienstes angezeigt werden:<br>
</p>

<pre class="brush:php;toolbar:false">Debugger listening on ws://127.0.0.1:9229/....
For help, see: https://nodejs.org/en/docs/inspector
  • Machen Sie den Port, den der Debugger überwacht, lokal verfügbar, indem Sie ihn ausführen
kubectl port-forward <nodejs-pod-name> 9229
  • Verbinden Sie Chrome Devtools mit dem Debugger, den Sie in den vorherigen Schritten aktiviert haben. Besuchen Sie chrome://inspect/ und dort sollten Sie in der Liste der Ziele Ihren Node.js-Prozess sehen:

Tracking down high memory usage in Node.js

Wenn nicht, stellen Sie sicher, dass Ihre Zielerkennungseinstellungen ordnungsgemäß eingerichtet sind

Tracking down high memory usage in Node.js

Jetzt können Sie damit beginnen, Schnappschüsse im Laufe der Zeit zu erfassen (der Zeitraum hängt von der Zeit ab, die erforderlich ist, bis der Speicherverlust auftritt) und sie vergleichen. Chrome DevTools bietet hierfür eine sehr praktische Möglichkeit.

Weitere Informationen zu Speicher-Snapshots und Chrome Dev Tools finden Sie unter Heap-Snapshot aufzeichnen

Beim Erstellen eines Snapshots werden alle anderen Arbeiten in Ihrem Hauptthread gestoppt. Abhängig vom Heap-Inhalt kann es sogar mehr als eine Minute dauern. Der Snapshot ist im Speicher integriert, sodass er die Heap-Größe verdoppeln kann, was dazu führt, dass der gesamte Speicher gefüllt wird und die App dann abstürzt.

Wenn Sie einen Heap-Snapshot in der Produktion erstellen, stellen Sie sicher, dass der Prozess, aus dem Sie ihn erstellen, abstürzen kann, ohne die Verfügbarkeit Ihrer Anwendung zu beeinträchtigen.

Aus Node.js-Dokumenten

Also zurück zu meinem Fall, als ich zwei Schnappschüsse zum Vergleichen und Sortieren nach Delta auswählte, bekam ich, was Sie unten sehen können.

Tracking down high memory usage in Node.js

Wir können sehen, dass das größte positive Delta beim String-Konstruktor auftrat, was bedeutete, dass der Dienst zwischen den beiden Snapshots viele Strings erstellt hatte, diese aber noch verwendet wurden. Die Frage war nun, wo diese erstellt wurden und wer sie bezog. Gut, dass die erfassten Schnappschüsse auch diese Informationen enthalten, sogenannte Retainer.

Tracking down high memory usage in Node.js

Als ich mich in den Schnappschüssen und der nie schrumpfenden Liste von Zeichenfolgen umgesehen habe, ist mir ein Muster aus Zeichenfolgen aufgefallen, die einer ID ähnelten. Als ich darauf klickte, konnte ich die Kettenobjekte sehen, die auf sie verwiesen – auch bekannt als Retainer. Es war ein Array namens sentEvents aus einem Klassennamen, den ich anhand des Bibliothekscodes erkennen konnte. Tadaaa, wir haben unseren Übeltäter, eine ständig wachsende Liste von IDs, von denen ich zu diesem Zeitpunkt annahm, dass sie nie veröffentlicht wurden. Ich habe im Laufe der Zeit eine Menge Schnappschüsse gemacht und dies war der einzige Ort, der immer wieder als Hotspot mit einem großen positiven Delta auftauchte.

Überprüfen Sie den Fix

Anstatt zu versuchen, den Code in seiner Gesamtheit zu verstehen, musste ich mich mit diesen Informationen auf den Zweck des Arrays konzentrieren, wann es gefüllt und gelöscht wurde. Es gab eine einzige Stelle, an der der Code Elemente in das Array schob, und eine andere, an der der Code sie herausschob, was den Umfang des Fixes einschränkte.

Man kann mit Sicherheit davon ausgehen, dass das Array nicht zum vorgesehenen Zeitpunkt geleert wurde. Abgesehen von den Details des Codes passierte im Grunde Folgendes:

  • Die Bibliothek stellte Schnittstellen entweder zum Konsumieren, Produzieren von Ereignissen oder zum Produzieren und Konsumieren von Ereignissen bereit.
  • Wenn es Ereignisse sowohl konsumierte als auch produzierte, musste es die Ereignisse verfolgen, die der Prozess selbst erzeugte, um sie zu überspringen und nicht erneut zu konsumieren. Die sentEvents wurden beim Produzieren ausgefüllt und gelöscht, wenn beim Versuch, sie zu konsumieren, die Nachrichten übersprungen wurden.

Können Sie sehen, wohin das führt? ? Wenn Dienste die Bibliothek nur zum Erzeugen von Ereignissen verwendeten, wurden die sentEvents immer noch mit allen Ereignissen gefüllt, aber es gab keinen Codepfad (Consumer) zum Löschen.

Ich habe den Code so gepatcht, dass er Ereignisse nur im Producer- und Consumer-Modus verfolgt, und ihn im Staging bereitgestellt. Selbst bei der Staging-Last war klar, dass der Patch dabei half, die hohe Speichernutzung zu reduzieren und keine Regressionen mit sich brachte.

Ergebnisse

Als der Patch in der Produktion bereitgestellt wurde, wurde die Speichernutzung drastisch reduziert und die Zuverlässigkeit des Dienstes verbessert (keine OOMs mehr).

Tracking down high memory usage in Node.js

Ein schöner Nebeneffekt war die 50-prozentige Reduzierung der Anzahl der Pods, die zur Bewältigung des gleichen Datenverkehrs benötigt werden.

Tracking down high memory usage in Node.js

Abschluss

Dies war eine großartige Gelegenheit für mich, Speicherprobleme in Node.js zu verfolgen und mich weiter mit den verfügbaren Tools vertraut zu machen.

Ich hielt es für das Beste, nicht auf die Details der einzelnen Tools einzugehen, da dies einen separaten Beitrag verdienen würde, aber ich hoffe, dass dies ein guter Ausgangspunkt für alle ist, die daran interessiert sind, mehr über dieses Thema zu erfahren oder mit ähnlichen Problemen konfrontiert sind.

Das obige ist der detaillierte Inhalt vonHohe Speichernutzung in Node.js aufspüren. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn