In diesem Artikel werde ich meinen Ansatz zum Aufspüren und Beheben einer hohen Speichernutzung in Node.js teilen.
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:
Unten ist das Dashboard, das mit dem Ticket verknüpft war:
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.
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.
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.
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:
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?
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
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
kubectl port-forward <nodejs-pod-name> 9229
Wenn nicht, stellen Sie sicher, dass Ihre Zielerkennungseinstellungen ordnungsgemäß eingerichtet sind
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.
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.
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.
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:
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.
Als der Patch in der Produktion bereitgestellt wurde, wurde die Speichernutzung drastisch reduziert und die Zuverlässigkeit des Dienstes verbessert (keine OOMs mehr).
Ein schöner Nebeneffekt war die 50-prozentige Reduzierung der Anzahl der Pods, die zur Bewältigung des gleichen Datenverkehrs benötigt werden.
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!