Eine praktische Anleitung zum Umgang mit Speicherlecks in Node.js.

Speicherlecks ähneln Entitäten, die in einer Anwendung parasitär sind. Sie dringen leise in das System ein, ohne Schaden zu verursachen. Wenn sich jedoch herausstellt, dass das Leck stark genug ist, kann dies die Anwendung in eine Katastrophe bringen. Zum Beispiel - um es stark zu verlangsamen oder einfach um es zu "töten". Der Autor des Artikels, dessen Übersetzung wir heute veröffentlichen, schlägt vor, über Speicherlecks in JavaScript zu sprechen. Insbesondere werden wir über die Speicherverwaltung in JavaScript, das Erkennen von Speicherlecks in realen Anwendungen und den Umgang mit Speicherlecks sprechen.





Was ist ein Speicherverlust?


Ein Speicherverlust ist im weitesten Sinne ein Speicher, der einer Anwendung zugewiesen ist, die diese Anwendung nicht mehr benötigt, aber nicht zur zukünftigen Verwendung an das Betriebssystem zurückgegeben werden kann. Mit anderen Worten, es handelt sich um einen Speicherblock, der von der Anwendung erfasst wird, ohne dass beabsichtigt wird, diesen Speicher in Zukunft zu verwenden.

Speicherverwaltung


Die Speicherverwaltung ist ein Mechanismus zum Zuweisen von Systemspeicher zu einer Anwendung, die ihn benötigt, und ein Mechanismus zum Zurückgeben von unnötigem Speicher an das Betriebssystem. Es gibt viele Ansätze zur Speicherverwaltung. Welcher Ansatz verwendet wird, hängt von der verwendeten Programmiersprache ab. Hier ist eine Übersicht über einige gängige Ansätze zur Speicherverwaltung:

  • . . . , . C C++. , , malloc free, .
  • . , , , . , , , . , , , , . . — JavaScript, , JVM (Java, Scala, Kotlin), Golang, Python, Ruby .
  • Anwendung des Konzepts des Eigentums an der Erinnerung. Bei diesem Ansatz sollte jede Variable einen eigenen Eigentümer haben. Sobald der Eigentümer den Gültigkeitsbereich verlässt, wird der Wert in der Variablen zerstört, wodurch Speicherplatz frei wird. Diese Idee wird in Rust verwendet.

Es gibt andere Ansätze zur Speicherverwaltung, die in verschiedenen Programmiersprachen verwendet werden. Beispielsweise verwendet C ++ 11 das RAII- Idiom , während Swift den ARC- Mechanismus verwendet . Aber darüber zu sprechen, würde den Rahmen dieses Artikels sprengen. Um die oben genannten Methoden der Speicherverwaltung zu vergleichen und ihre Vor- und Nachteile zu verstehen, benötigen wir einen separaten Artikel.

JavaScript, eine Sprache, ohne die sich Webprogrammierer ihre Arbeit nicht vorstellen können, verwendet die Idee der Speicherbereinigung. Daher werden wir mehr darüber sprechen, wie dieser Mechanismus funktioniert.

JavaScript-Speicherbereinigung


Wie bereits erwähnt, ist JavaScript eine Sprache, die das Konzept der Speicherbereinigung verwendet. Während des Betriebs von JS-Programmen wird regelmäßig ein Mechanismus gestartet, der als Garbage Collector bezeichnet wird. Er findet aus dem Anwendungscode heraus, auf welche Teile des zugewiesenen Speichers zugegriffen werden kann. Das heißt, auf welche Variablen verwiesen wird. Wenn der Garbage Collector feststellt, dass über den Anwendungscode nicht mehr auf ein Speicherelement zugegriffen wird, wird dieser Speicher freigegeben. Der obige Ansatz kann unter Verwendung von zwei Hauptalgorithmen implementiert werden. Der erste ist der sogenannte Mark and Sweep-Algorithmus. Es wird in JavaScript verwendet. Die zweite ist die Referenzzählung. Es wird in Python und PHP verwendet.


Phasen Markierung (Markierung) und Sweep (Bereinigung) des Markierungs- und Sweep-

Algorithmus Bei der Implementierung des Markierungsalgorithmus wird zuerst eine Liste von Wurzelknoten erstellt, die durch globale Umgebungsvariablen dargestellt werden (dies ist ein Objekt im Browserwindow), und dann wird der resultierende Baum von Wurzel zu Blattknoten gecrawlt, die mit allen markiert sind auf dem Weg Objekte getroffen. Der Speicher auf dem Heap, der von unbeschrifteten Objekten belegt wird, wird freigegeben.

Speicherlecks in Node.js-Anwendungen


Bisher haben wir genügend theoretische Konzepte in Bezug auf Speicherlecks und Speicherbereinigung analysiert. Also - wir sind bereit zu sehen, wie alles in realen Anwendungen aussieht. In diesem Abschnitt schreiben wir einen Node.js-Server mit einem Speicherverlust. Wir werden versuchen, dieses Leck mit verschiedenen Werkzeugen zu identifizieren und es dann zu beseitigen.

▍ Vertrautheit mit einem Code, der einen Speicherverlust aufweist


Zu Demonstrationszwecken habe ich einen Express-Server geschrieben, der eine Speicherverlustroute aufweist. Wir werden diesen Server debuggen.

const express = require('express')

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

Es gibt ein Array leaks, das außerhalb des Bereichs des Verarbeitungscodes für API-Anforderungen liegt. Infolgedessen werden jedes Mal, wenn der entsprechende Code ausgeführt wird, einfach neue Elemente zum Array hinzugefügt. Das Array wird niemals gelöscht. Da die Verknüpfung zu diesem Array nach dem Beenden des Anforderungshandlers nicht verschwindet, gibt der Garbage Collector den von ihm verwendeten Speicher niemals frei.

▍Call Memory Leak


Hier kommen wir zu den interessantesten. Es wurden viele Artikel darüber geschrieben, wie node --inspectServer-Speicherlecks beim Debuggen verwendet werden, nachdem der Server mit Anfragen wie Artillerie gefüllt wurde . Dieser Ansatz hat jedoch einen wichtigen Nachteil. Stellen Sie sich vor, Sie haben einen API-Server mit Tausenden von Endpunkten. Jeder von ihnen benötigt viele Parameter. Der jeweilige Code, der aufgerufen wird, hängt von dessen Funktionen ab. Wenn der Entwickler unter realen Bedingungen nicht weiß, wo sich der Speicherverlust befindet, muss er daher mehrmals auf jede API zugreifen und dabei alle möglichen Kombinationen von Parametern verwenden, um den Speicher zu füllen. Für mich ist das nicht einfach. Die Lösung dieses Problems wird jedoch durch die Verwendung von so etwas erleichtertGoreplay - ein System, mit dem Sie echten Datenverkehr aufzeichnen und "abspielen" können.

Um unser Problem zu lösen, werden wir das Debuggen in der Produktion durchführen. Das heißt, wir erlauben unserem Server, den Speicher während seiner tatsächlichen Verwendung zu überlaufen (da er eine Vielzahl von API-Anforderungen empfängt). Und nachdem wir einen verdächtigen Anstieg der zugewiesenen Speichermenge festgestellt haben, werden wir das Debuggen durchführen.

▍ Heap Dump


Um zu verstehen, was ein Heap-Dump ist, müssen wir zuerst die Bedeutung des Konzepts eines Heaps herausfinden. Wenn Sie dieses Konzept so einfach wie möglich beschreiben, stellt sich heraus, dass der Heap der Ort ist, an den alles fällt, was dem Speicher zugewiesen ist. All dies befindet sich auf dem Haufen, bis der Garbage Collector alles entfernt, was als unnötig erachtet wird. Ein Heap-Dump ist eine Momentaufnahme des aktuellen Status des Heaps. Der Speicherauszug enthält alle internen Variablen und Variablen, die vom Programmierer deklariert wurden. Es repräsentiert den gesamten Speicher, der zum Zeitpunkt des Empfangs des Speicherauszugs auf dem Heap zugewiesen war.

Wenn wir also den Heap-Dump des gerade gestarteten Servers mit dem Dump des Server-Heaps vergleichen könnten, der seit langer Zeit ausgeführt wird und überfüllten Speicher verfügt, könnten wir verdächtige Objekte identifizieren, die die Anwendung nicht benötigt, aber vom Garbage Collector nicht gelöscht werden.

Bevor Sie die Konversation fortsetzen, wollen wir uns mit dem Erstellen von Heap-Dumps befassen. Um dieses Problem zu lösen, verwenden wir den npm-Paket- Heapdump , mit dem Sie programmgesteuert einen Dump des Server-Heaps abrufen können.

Installieren Sie das Paket:

npm i heapdump

Wir werden einige Änderungen am Servercode vornehmen, mit denen wir dieses Paket verwenden können:

const express = require('express');
const heapdump = require("heapdump");

const app = express();
const port = 3000;

const leaks = [];

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

app.get('/heapdump', (req, res) => {
  heapdump.writeSnapshot(`heapDump-${Date.now()}.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a bloated server written to", filename);

    res.status(200).send({msg: "successfully took a heap dump"})
  });
});

app.listen(port, () => {
  heapdump.writeSnapshot(`heapDumpAtServerStart.heapsnapshot`, (err, filename) => {
    console.log("Heap dump of a fresh server written to", filename);
  });
});

Hier haben wir dieses Paket verwendet, um einen frisch gestarteten Server zu sichern. Wir haben auch eine API erstellt /heapdump, mit der beim Zugriff ein Heap erstellt werden kann. Wir werden uns dieser API in dem Moment zuwenden, in dem wir feststellen, dass der Server zu viel Speicher belegt.

Wenn Ihr Server in einem Kubernetes-Cluster ausgeführt wird, können Sie sich nicht ohne zusätzlichen Aufwand an den Pod wenden, dessen Server ausgeführt wird und auf dem zu viel Speicher belegt ist. Zu diesem Zweck können Sie die Portweiterleitung verwenden . Da Sie keinen Zugriff auf das Dateisystem haben, das Sie zum Herunterladen von Dump-Dateien benötigen, ist es außerdem besser, diese Dateien in einen externen Cloud-Speicher (wie S3) hochzuladen.

▍ Speicherverlusterkennung


Und jetzt wird der Server bereitgestellt. Er arbeitet seit mehreren Tagen. Es werden viele Anfragen empfangen (in unserem Fall nur Anfragen desselben Typs), und wir haben auf die Zunahme des vom Server verbrauchten Arbeitsspeichers geachtet. Ein Speicherverlust kann mithilfe von Überwachungstools wie Express Status Monitor , Clinic , Prometheus erkannt werden . Danach rufen wir die API auf, um den Heap zu sichern. Dieser Speicherauszug enthält alle Objekte, die der Garbage Collector nicht löschen konnte.

So sieht die Abfrage aus, um einen Speicherauszug zu erstellen:

curl --location --request GET 'http://localhost:3000/heapdump'

Wenn ein Heap-Dump erstellt wird, muss der Garbage Collector ausgeführt werden. Infolgedessen müssen wir uns keine Gedanken über die Objekte machen, die möglicherweise in Zukunft vom Garbage Collector entfernt werden, sich aber noch auf dem Heap befinden. Das heißt - über die Objekte, mit denen bei der Arbeit keine Speicherlecks auftreten.

Nachdem wir beide Dumps zur Verfügung haben (einen Dump eines frisch gestarteten Servers und einen Dump eines Servers, der seit einiger Zeit funktioniert hat), können wir beginnen, sie zu vergleichen.

Das Abrufen eines Speicherauszugs ist ein Blockierungsvorgang, für dessen Ausführung viel Speicher erforderlich ist. Daher muss es mit Vorsicht durchgeführt werden. Weitere Informationen zu möglichen Problemen bei diesem Vorgang finden Sie hier .

Starten Sie Chrome und drücken Sie die Taste.F12. Dies wird zur Entdeckung von Entwicklertools führen. Hier müssen Sie zur Registerkarte gehen Memoryund beide Snapshots des Speichers laden.


Speicher Download - Dumps auf der Registerkarte Speicher der Chrome - Entwickler - Tools

Nachdem beide Schnappschüsse herunterzuladen, müssen Sie ändernperspectivezuComparisonund klicken Sie auf den Snapshot des Speichers des Servers,für einige Zeit gearbeitet.


Vergleichen Sie Snapshots.

Hier können Sie die Spalte analysierenConstructorund nach Objekten suchen, die der Garbage Collector nicht entfernen kann. Die meisten dieser Objekte werden durch interne Links dargestellt, die von Knoten verwendet werden. Hier ist es nützlich, einen Trick zu verwenden, der darin besteht, die Liste nach Feldern zu sortierenAlloc. Size. Dadurch werden schnell die Objekte gefunden, die den meisten Speicher belegen. Wenn Sie den Block erweitern(array)und dann -(object elements), sehen Sie ein Arrayleaksmit einer großen Anzahl von Objekten, die mit dem Garbage Collector nicht gelöscht werden können.


Analyse eines verdächtigen Arrays Mit

dieser Technik können wir zum Array gehenleaksund verstehen, dass es die falsche Operation ist, die einen Speicherverlust verursacht.

▍Speicherverlust beheben


Jetzt, da wir wissen, dass der "Täter" ein Array ist leaks, können wir den Code analysieren und herausfinden, dass das Problem darin besteht, dass das Array außerhalb des Anforderungshandlers deklariert ist. Infolgedessen stellt sich heraus, dass der Link dazu niemals gelöscht wird. Das Problem zu beheben ist ganz einfach: Übertragen Sie einfach die Deklaration des Arrays an den Handler:

app.get('/bloatMyServer', (req, res) => {
  const redundantObj = {
    memory: "leaked",
    joke: "meta"
  };

  const leaks = [];

  [...Array(10000)].map(i => leaks.push(redundantObj));

  res.status(200).send({size: leaks.length})
});

Um die Wirksamkeit der ergriffenen Maßnahmen zu überprüfen, reicht es aus, die obigen Schritte zu wiederholen und die Heap-Bilder erneut zu vergleichen.

Zusammenfassung


Speicherlecks treten in verschiedenen Sprachen auf. Insbesondere in - solchen, die Garbage Collection-Mechanismen verwenden. Zum Beispiel in JavaScript. Es ist normalerweise nicht schwierig, ein Leck zu beheben - die wirklichen Schwierigkeiten treten nur auf, wenn Sie danach suchen.

In diesem Artikel haben Sie sich mit den Grundlagen der Speicherverwaltung und der Organisation der Speicherverwaltung in verschiedenen Sprachen vertraut gemacht. Hier haben wir ein reales Szenario eines Speicherverlusts reproduziert und eine Methode zur Fehlerbehebung beschrieben.

Liebe Leser! Sind in Ihren Webprojekten Speicherlecks aufgetreten?


All Articles