Grundlegendes zur Speicherbereinigung und zum Auffinden von Speicherlecks in Node.js

Ursprünglicher Autor: Daniel Khan
  • Übersetzung
Schlechte Pressestimmen zu Node.js beziehen sich häufig auf Leistungsprobleme. Dies bedeutet nicht, dass es mit Node.js mehr Probleme gibt als mit anderen Technologien. Nur der Benutzer sollte einige Funktionen seiner Arbeit berücksichtigen. Obwohl die Technologie eine sanfte Lernkurve aufweist, sind die Mechanismen, die ihre Funktionsweise sicherstellen, recht komplex. Sie müssen sie verstehen, um Leistungsfehler zu vermeiden. Und wenn etwas schief geht, muss man wissen, wie man alles schnell in Ordnung bringt. In diesem Artikel spricht Daniel Khan darüber, wie Node.js den Speicher verwaltet und wie Speicherprobleme verfolgt werden.



Im Gegensatz zu Plattformen wie PHP sind Node.js-Anwendungen langfristige Prozesse. Dies hat eine Reihe von positiven Aspekten - beispielsweise die Möglichkeit, eine einmalige Verbindung zur Datenbank herzustellen und diese Verbindung für alle Abfragen zu verwenden. Diese Funktion kann jedoch Probleme verursachen. Schauen wir uns zunächst die Grundlagen von Node.js an.


Der echte österreichische Müllsammler

Node.js ist ein C ++ - Programm, das von der JavaScript-Engine V8 von Google V8 gesteuert wird

- Eine Engine, die ursprünglich für Google Chrome geschrieben wurde, aber autonom verwendet werden kann. Daher ist es ideal für Node.js und der einzige Teil der Plattform, der JavaScript "versteht". V8 kompiliert JavaScript in Maschinencode und führt ihn aus. Zur Laufzeit steuert die Engine die Zuordnung und das Löschen des Speichers nach Bedarf. Dies bedeutet, dass es sich bei der Speicherverwaltung in Node.js tatsächlich um V8 handelt.

Hier sehen Sie ein einfaches Beispiel für die Verwendung von V8 aus Sicht von C ++.

V8-Speicherlayout Ein

laufendes Programm kann immer nach einer bestimmten Menge an Speicherplatz im Arbeitsspeicher dargestellt werden. Dieser Ort wird als Resident-Set bezeichnet. V8 verwendet ein Schema ähnlich dem Java Virtual Machine-Schema und unterteilt den Speicher in Segmente:

Code: Code, der gerade ausgeführt wird.
Stack: Enthält alle primitiven Werttypen (wie Integer oder Boolean) mit Zeigern, die auf Objekte im Heap verweisen und den Steuerungsfluss des Programms definieren.
Heap: Ein Speichersegment zum Speichern von Referenztypen wie Objekten, Zeichenfolgen und Abschlüssen.


V8-Speicherschema

In Node.js kann die aktuelle Speichernutzung durch Aufrufen von process.memoryUsage () ermittelt werden.

Die Funktion gibt ein Objekt zurück, das Folgendes enthält:

  • Resident Set Size;
  • Gesamthaufengröße;
  • die Menge an Speicherplatz im Heap.


Mit dieser Funktion können Sie die Speichernutzung über einen bestimmten Zeitraum hinweg aufzeichnen und ein Diagramm mit der Speicherverwaltung des V8 erstellen.


Speichernutzung von Node.js im Verhältnis zur Zeit

Wir sehen, dass das Diagramm des verwendeten Heapspeichers extrem instabil ist, aber immer innerhalb bestimmter Grenzen bleibt, um den durchschnittlichen Verbrauchswert konstant zu halten. Der Prozess, der Speicher zuordnet und freigibt, wird als Garbage Collection bezeichnet.

Eine Einführung in die Garbage

Collection Jedes speicherintensive Programm benötigt einen Mechanismus, um Speicherplatz zu reservieren und zurückzugewinnen. In C und C ++ wird diese Funktion von den Befehlen malloc () und free () ausgeführt, wie im folgenden Beispiel gezeigt:

char * buffer;
buffer = (char*) malloc (42);
// Do something with buffer
free (buffer);


Wir sehen, dass der Programmierer für die Freigabe von nicht verwendetem Speicher verantwortlich ist. Wenn das Programm nur Speicher reserviert und nicht freigibt, wächst der Heap, bis der verwendete Speicher voll ist, was zum Absturz des Programms führt. Wir nennen dies ein Speicherleck.

Wie wir bereits wissen, wird in Node.js JavaScript mit V8 in Maschinencode kompiliert. Die nach dem Übersetzen erhaltenen Datenstrukturen können nichts mit ihrer ursprünglichen Darstellung anfangen und werden einfach mit V8 gesteuert. Dies bedeutet, dass wir in JavaScript keinen aktiven Speicher reservieren und löschen können. V8 verwendet einen bekannten Mechanismus für die Speicherbereinigung - die Speicherbereinigung.

Das Prinzip der Speicherbereinigung ist recht einfach: Wenn sich niemand auf das Speichersegment bezieht, können wir davon ausgehen, dass es nicht verwendet wird, und es löschen. Der Vorgang des Erhaltens und Speicherns dieser Informationen ist jedoch ziemlich kompliziert, da der Code Kettenglieder und Umleitungen enthalten kann, die eine komplexe Graphstruktur bilden.


Haufen zählen. Ein rotes Objekt kann nur gelöscht werden, wenn es nicht mehr referenziert

wird.Die Speicherbereinigung ist ein recht teurer Vorgang, da die Anwendung unterbrochen wird, was sich natürlich auf die Leistung auswirkt. Um dieses Problem zu beheben, verwendet V8 zwei Arten von Garbage Collection:

  • Scavenge - schnell aber unvollständig;
  • Mark-Sweep ist relativ langsam, löscht jedoch alle nicht verwendeten Links.


Einen ausgezeichneten Beitrag mit sehr detaillierten Informationen zur Garbage Collection finden Sie hier.

Wenn Sie sich nun die mit process.memoryUsage () erhaltene Grafik ansehen, können Sie leicht zwischen verschiedenen Arten von Müllsammlungen unterscheiden: Eine Zeichnung, die den Sägezähnen ähnelt, markiert die Arbeit von Scavenge und fällt herunter - Mark-Sweep.

Mit dem eingebauten Modul node-gc-profiler erhalten Sie noch mehr Informationen über die Arbeit des Garbage Collectors. Das Modul abonniert Garbage Collector-Ereignisse und übersetzt sie in JavaScript.

Das zurückgegebene Objekt gibt den Typ der Garbage Collection und die Dauer an. Auch diese Daten können leicht grafisch dargestellt werden, um das Verständnis der Funktionsweise zu erleichtern.


Dauer und Häufigkeit des Beginns der Müllabfuhr

Es ist deutlich zu sehen, dass Scavenge viel häufiger gestartet wird als Mark-Sweep. Je nach Komplexität der Anwendung kann die Dauer variieren. Es ist bemerkenswert, dass Sie in dieser Grafik häufige und kurze Mark-Sweep-Starts sehen können, deren Funktion mir noch nicht klar ist.

Wenn etwas schief geht

Wenn der Garbage Collector den Speicher selbst aufräumt, warum sollten wir uns dann Sorgen machen? In der Tat können Speicherverluste in Ihren Protokollen leicht auftreten.


Eine durch einen Speicherverlust verursachte Ausnahme

Anhand des zuvor erstellten Diagramms können wir beobachten, wie der Speicher ständig verstopft ist!


Fortschritt durch Speicherverlust

Der Garbage Collector tut alles, um Speicher freizugeben. Mit jedem Durchlauf nimmt der Speicherverbrauch jedoch stetig zu. Dies ist ein deutliches Zeichen für einen Speicherverlust. Da wir wissen, wie man einen Speicherverlust genau erkennt, wollen wir uns ansehen, was getan werden muss, um ihn zu verursachen.

Erstellen eines Speicherlecks

Einige Lecks sind offensichtlich - wie das Speichern von Daten in globalen Variablen (z. B. das Hinzufügen der IP-Adressen aller angemeldeten Benutzer zu einem Array). Andere sind nicht so auffällig - zum Beispiel das berühmte Walmart-Speicherleck, weil ein kleiner Ausdruck im Node.js-Kernel-Code fehlt , der Wochen brauchte, um die Quelle zu finden.

Ich werde hier keine Fehler im Kernel-Code berücksichtigen. Schauen wir uns nur ein schwer auffindbares Code-Leak an.aus dem Meteor-Blog, den Sie ganz einfach in Ihrem Code zulassen können.


Einen Fehler in Ihren JavaScript-Code einfügen

Auf den ersten Blick sieht es gut aus. Man könnte meinen, dass theThing bei jedem Aufruf von replaceThing () überschrieben wird. Das Problem ist, dass someMethod einen eigenen privaten Bereich als Kontext hat. Dies bedeutet, dass someMethod () unused () kennt und selbst wenn unused () nie aufgerufen wird, verhindert dies, dass der Garbage Collector Speicher von originalThing freigibt. Nur weil es zu viele indirekte Herausforderungen gibt. Dies ist kein Fehler, kann jedoch zu Speicherlecks führen, die schwer zu verfolgen sind.

Stimmt, es wäre großartig, wenn Sie in den Stapel schauen und sehen könnten, was sich dort jetzt befindet? Zum Glück gibt es eine solche Gelegenheit! Mit V8 können Sie derzeit Heaps sichern, und mit V8-Profiler können Sie diese Funktionalität für JavaScript verwenden.

/**
 * Simple userland heapdump generator using v8-profiler
 * Usage: require('[path_to]/HeapDump').init('datadir')
 *
 * @module HeapDump
 * @type {exports}
 */
var fs = require('fs');
var profiler = require('v8-profiler');
var _datadir = null;
var nextMBThreshold = 0;
/**
 * Init and scheule heap dump runs
 *
 * @param datadir Folder to save the data to
 */
module.exports.init = function (datadir) {
    _datadir = datadir;
    setInterval(tickHeapDump, 500);
};
/**
 * Schedule a heapdump by the end of next tick
 */
function tickHeapDump() {
    setImmediate(function () {
        heapDump();
    });
}
/**
 * Creates a heap dump if the currently memory threshold is exceeded
 */
function heapDump() {
    var memMB = process.memoryUsage().rss / 1048576;
    console.log(memMB + '>' + nextMBThreshold);
    if (memMB > nextMBThreshold) {
        console.log('Current memory usage: %j', process.memoryUsage());
        nextMBThreshold += 50;
        var snap = profiler.takeSnapshot('profile');
        saveHeapSnapshot(snap, _datadir);
    }
}
/**
 * Saves a given snapshot
 *
 * @param snapshot Snapshot object
 * @param datadir Location to save to
 */
function saveHeapSnapshot(snapshot, datadir) {
    var buffer = '';
    var stamp = Date.now();
    snapshot.serialize(
        function iterator(data, length) {
            buffer += data;
        }, function complete() {
            var name = stamp + '.heapsnapshot';
            fs.writeFile(datadir + '/' + name , buffer, function () {
                console.log('Heap snapshot written to ' + name);
            });
        }
    );
}


Dieses einfache Modul erstellt eine Heap-Dump-Datei, wenn die Speichernutzung ständig zunimmt. Ja, es gibt viel komplexere Ansätze zur Feststellung von Anomalien, aber für unsere Zwecke wird dies ausreichen. Im Falle eines Speicherverlusts verfügen Sie möglicherweise über viele solcher Dateien. Sie müssen dies also genau überwachen und die Möglichkeit hinzufügen, dieses Modul zu warnen. Chrome bietet die gleichen Funktionen für die Arbeit mit einem Heap-Dump. Mit den Chrome Developer Tools können Sie V8-Profiler-Dumps analysieren.


Chrome Developer Tools

Ein Speicherauszug des Heapspeichers hilft möglicherweise nicht, da Sie nicht sehen können, wie sich der Heapspeicher im Laufe der Zeit ändert. Daher können Sie mit Chrome Developer Tools verschiedene Dateien vergleichen. Beim Vergleich von 2 Dumps erhalten wir ein Delta von Werten, das zeigt, welche Strukturen zwischen zwei Dumps zunehmen:


Ein Vergleich der Deponien zeigt unser Leck.

Hier sehen wir unser Problem. Eine Variable, die eine Folge von Sternchen enthält und longStr heißt, wird von originalThing referenziert, das von einer Methode referenziert wird, auf die verwiesen wird ... Ich denke, Sie verstehen. Diese lange Reihe von verschachtelten Links und Schließungskontexten ermöglicht nicht, dass longStr gelöscht wird. Obwohl dieses Beispiel zu offensichtlichen Ergebnissen führt, ist der Prozess immer der gleiche:

  • Erstellen Sie mehrere Heap-Dumps mit einem Zeitunterschied und einer unterschiedlichen Menge an zugewiesenem Speicher.
  • Vergleichen Sie mehrere Speicherauszüge, um festzustellen, welche Werte zunehmen.


Fazit

Wie Sie sehen, ist der Speicherbereinigungsprozess ziemlich kompliziert, und selbst gültiger Code kann zu Speicherverlusten führen. Wenn Sie die integrierte V8-Funktionalität mit Chrome Developer Tools verwenden, können Sie die Ursachen von Speicherverlusten nachvollziehen und haben, wenn Sie diese Funktionalität in Ihre Anwendung einbetten, alles, was Sie benötigen, um ein ähnliches Problem zu lösen, wenn es auftritt.

Eine Frage bleibt: Wie kann das Leck behoben werden? Die Antwort ist einfach: fügen Sie einfach theThing = null hinzu; am Ende der Funktion und Sie werden gespeichert.

Jetzt auch beliebt: