Node.js: Verwalten des Speichers, der für Anwendungen verfügbar ist, die in Containern ausgeführt werden

Beim Ausführen von Node.js-Anwendungen in Docker-Containern funktionieren herkömmliche Speichereinstellungen nicht immer wie erwartet. Das Material, dessen Übersetzung wir heute veröffentlichen, ist darauf ausgerichtet, die Antwort auf die Frage zu finden, warum dies so ist. Es enthält auch praktische Empfehlungen zum Verwalten des Speichers, der für Node.js-Anwendungen verfügbar ist, die in Containern ausgeführt werden.



Überprüfung der Empfehlungen


Angenommen, eine Node.js-Anwendung wird in einem Container mit einem festgelegten Speicherlimit ausgeführt. Wenn es sich um Docker handelt, kann die Option --memory verwendet werden, um dieses Limit --memory . Ähnliches ist bei der Arbeit mit Container-Orchestrierungssystemen möglich. In diesem Fall wird empfohlen, beim Starten der Anwendung Node.js die --max-old-space-size . Auf diese Weise können Sie die Plattform darüber informieren, wie viel Speicher für sie verfügbar ist, und auch berücksichtigen, dass diese Menge unter dem auf Containerebene festgelegten Grenzwert liegen sollte.

Wenn die Anwendung Node.js im Container ausgeführt wird, legen Sie die Kapazität des verfügbaren Speichers entsprechend dem Spitzenwert der von der Anwendung verwendeten aktiven Speichermenge fest. Dies erfolgt, wenn die Container-Speichergrenzen konfiguriert werden können.

Lassen Sie uns nun näher auf das Problem der Verwendung von Speicher in Containern eingehen.

Docker-Speicherlimit


Standardmäßig haben Container keine Ressourcenbeschränkungen und können so viel Speicher verwenden, wie das Betriebssystem zulässt. Der docker run Befehl docker run verfügt über Befehlszeilenoptionen, mit denen Sie Grenzwerte für die Speichernutzung oder die Prozessorressourcen festlegen können.

Der Befehl zum Starten des Containers könnte folgendermaßen aussehen:

 docker run --memory <x><y> --interactive --tty <imagename> bash 

Bitte beachten Sie Folgendes:

  • x ist die Grenze der dem Container zur Verfügung stehenden Speichermenge, ausgedrückt in Einheiten von y .
  • y kann den Wert b (Bytes), k (Kilobyte), m (Megabyte), g (Gigabyte) annehmen.

Hier ist ein Beispiel für einen Container-Startbefehl:

 docker run --memory 1000000b --interactive --tty <imagename> bash 

Hier ist das Speicherlimit auf 1000000 Bytes festgelegt.

Um das auf Containerebene festgelegte Speicherlimit zu überprüfen, können Sie im Container den folgenden Befehl ausführen:

 cat /sys/fs/cgroup/memory/memory.limit_in_bytes 

Lassen Sie uns über das Verhalten des Systems sprechen, wenn Sie das Speicherlimit für die Anwendung Node.js mit dem --max-old-space-size . In diesem Fall entspricht dieses Speicherlimit dem auf Containerebene festgelegten Limit.

Was im Schlüsselnamen als "alter Raum" bezeichnet wird, ist eines der Fragmente des von V8 kontrollierten Heaps (der Ort, an dem die "alten" JavaScript-Objekte platziert werden). Diese Taste steuert die maximale Heap-Größe, wenn Sie nicht auf die Details eingehen, die wir unten berühren. Details zu den Befehlszeilenschaltern von Node.j finden Sie hier .

Wenn eine Anwendung versucht, mehr Speicher zu verwenden, als im Container verfügbar ist, wird ihr Betrieb im Allgemeinen beendet.

Im folgenden Beispiel (die Anwendungsdatei heißt test-fatal-error.js ) werden MyRecord Objekte im Abstand von 10 Millisekunden in das test-fatal-error.js MyRecord . Dies führt zu einem unkontrollierten Heap-Wachstum, das einen Speicherverlust simuliert.

 'use strict'; const list = []; setInterval(()=> { const record = new MyRecord(); list.push(record); },10); function MyRecord() { var x='hii'; this.name = x.repeat(10000000); this.id = x.repeat(10000000); this.account = x.repeat(10000000); } setInterval(()=> { console.log(process.memoryUsage()) },100); 

Bitte beachten Sie, dass alle Beispiele für Programme, die wir hier diskutieren werden, im Docker-Image enthalten sind, das vom Docker Hub heruntergeladen werden kann:

 docker pull ravali1906/dockermemory 

Sie können dieses Bild für unabhängige Experimente verwenden.

Darüber hinaus können Sie die Anwendung in einen Docker-Container packen, das Image sammeln und mit dem Speicherlimit ausführen:

 docker run --memory 512m --interactive --tty ravali1906/dockermemory bash 

Hier ist ravali1906/dockermemory der Name des Bildes.

Jetzt können Sie die Anwendung starten, indem Sie ein Speicherlimit angeben, das das Containerlimit überschreitet:

 $ node --max_old_space_size=1024 test-fatal-error.js { rss: 550498304, heapTotal: 1090719744, heapUsed: 1030627104, external: 8272 } Killed 

Hier repräsentiert der --max_old_space_size das in Megabyte angegebene Speicherlimit. Die Methode process.memoryUsage() gibt Auskunft über die Speichernutzung. Die Werte werden in Bytes ausgedrückt.

Die Anwendung wird zu einem bestimmten Zeitpunkt zwangsweise beendet. Dies geschieht, wenn die von ihm verwendete Speichermenge eine bestimmte Grenze überschreitet. Was ist diese Grenze? Über welche Einschränkungen der Speichermenge können wir sprechen?

Das erwartete Verhalten einer Anwendung, die mit dem Schlüssel ausgeführt wird, ist - max-old-space-size


Standardmäßig beträgt die maximale Heap-Größe in Node.js (bis Version 11.x) 700 MB auf 32-Bit-Plattformen und 1400 MB auf 64-Bit-Plattformen. Informationen zum Einstellen dieser Werte finden Sie hier .

Wenn Sie --max-old-space-size Schlüssel --max-old-space-size , --max-old-space-size Speicherlimit --max-old-space-size , das das Container-Speicherlimit überschreitet, können Sie davon ausgehen, dass die Anwendung durch den Kernel-Sicherheitsmechanismus des Linux OOM Killer-Kernels beendet wird.

In Wirklichkeit kann dies nicht passieren.

Das tatsächliche Verhalten der Anwendung, die mit dem Schlüssel ausgeführt wird, ist max-old-space-size


Die Anwendung weist unmittelbar nach dem Start nicht den gesamten Speicher zu, dessen Begrenzung mit --max-old-space-size . Die Größe des JavaScript-Heaps hängt von den Anforderungen der Anwendung ab. Sie können anhand des Werts des heapUsed aus dem von der Methode heapUsed process.memoryUsage() Objekt beurteilen, wie viel Speicher die Anwendung verwendet. Tatsächlich handelt es sich um den im Heap für Objekte zugewiesenen Speicher.

Infolgedessen schließen wir, dass die Anwendung zwangsweise beendet wird, wenn die Heap-Größe größer ist als der durch den Schlüssel --memory festgelegte Grenzwert, wenn der Container --memory .

Aber in Wirklichkeit kann dies auch nicht passieren.

Bei der Profilerstellung für ressourcenintensive Node.js-Anwendungen, die in Containern mit einem bestimmten Speicherlimit ausgeführt werden, können die folgenden Muster beobachtet werden:

  1. OOM Killer wird viel später ausgelöst als in dem Moment, in dem die heapUsed heapTotal und heapUsed deutlich über den Speichergrenzen liegen.
  2. OOM Killer reagiert nicht auf das Überschreiten von Grenzwerten.

Eine Erklärung des Verhaltens von Node.js-Anwendungen in Containern


Ein Container überwacht einen wichtigen Indikator für die Anwendungen, die auf ihm ausgeführt werden. Dies ist RSS (Resident Set Size). Dieser Indikator repräsentiert einen bestimmten Teil des virtuellen Speichers der Anwendung.

Darüber hinaus ist es ein Speicher, der der Anwendung zugewiesen wird.

Das ist aber noch nicht alles. RSS ist Teil des aktiven Speichers, der der Anwendung zugewiesen ist.

Möglicherweise ist nicht der gesamte einer Anwendung zugewiesene Speicher aktiv. Tatsache ist, dass „zugewiesener Speicher“ nicht unbedingt physisch zugewiesen wird, bis der Prozess beginnt, ihn wirklich zu verwenden. Darüber hinaus kann das Betriebssystem als Antwort auf Anforderungen zur Speicherzuweisung von anderen Prozessen inaktive Teile des Anwendungsspeichers in die Auslagerungsdatei kopieren und den freigegebenen Speicherplatz an andere Prozesse übertragen. Wenn die Anwendung diese Speicherelemente erneut benötigt, werden sie aus der Auslagerungsdatei entnommen und in den physischen Speicher zurückgeführt.

Die RSS-Metrik gibt die Menge des aktiven und verfügbaren Speichers für die Anwendung in ihrem Adressraum an. Er beeinflusst die Entscheidung über das erzwungene Herunterfahren der Anwendung.

Beweise


▍ Beispiel Nr. 1. Eine Anwendung, die Speicher für einen Puffer reserviert


Das folgende Beispiel, buffer_example.js , zeigt ein Programm, das Speicher für einen Puffer reserviert:

 const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024) console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024))) 

Führen Sie den Container zunächst mit dem folgenden Befehl aus, damit die vom Programm zugewiesene Speichermenge den beim Start des Containers festgelegten Grenzwert überschreitet:

 docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash 

Führen Sie danach das Programm aus:

 $ node buffer_example 2000 2000 16 

Wie Sie sehen, hat das System das Programm nicht abgeschlossen, obwohl der vom Programm zugewiesene Speicher das Containerlimit überschreitet. Dies geschah aufgrund der Tatsache, dass das Programm nicht mit dem gesamten zugewiesenen Speicher arbeitet. RSS ist sehr klein und überschreitet nicht das Container-Speicherlimit.

▍ Beispiel Nr. 2. Anwendung, die den Puffer mit Daten füllt


Im folgenden Beispiel, buffer_example_fill.js , wird der Speicher nicht nur zugewiesen, sondern auch mit Daten gefüllt:

 const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x') console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024))) 

Führen Sie den Container aus:

 docker run --memory 1024m --interactive --tty ravali1906/dockermemory bash 

Führen Sie danach die Anwendung aus:

 $ node buffer_example_fill.js 2000 2000 984 

Anscheinend endet die Anwendung auch jetzt noch nicht! Warum? Tatsache ist, dass einige der alten Seiten im Prozessspeicher in die Auslagerungsdatei verschoben werden, wenn die Menge des aktiven Speichers den beim Starten des Containers festgelegten Grenzwert erreicht und in der Auslagerungsdatei Platz ist. Der freigegebene Speicher wird für denselben Prozess verfügbar gemacht. Standardmäßig weist Docker der Auslagerungsdatei Speicherplatz zu, der dem mit dem Flag --memory festgelegten Speicherlimit --memory . Vor diesem Hintergrund können wir sagen, dass der Prozess 2 GB Speicher hat - 1 GB im aktiven Speicher und 1 GB in der Auslagerungsdatei. Das heißt, aufgrund der Tatsache, dass die Anwendung ihren eigenen Speicher verwenden kann, dessen Inhalt vorübergehend in die Auslagerungsdatei verschoben wird, liegt die Größe des RSS-Index innerhalb des Containerlimits. Infolgedessen funktioniert die Anwendung weiterhin.

▍ Beispiel Nr. 3. Eine Anwendung, die einen Puffer mit Daten füllt, die in einem Container ausgeführt werden, der keine Auslagerungsdatei verwendet


Hier ist der Code, mit dem wir hier experimentieren werden (dies ist dieselbe Datei buffer_example_fill.js ):

 const buf = Buffer.alloc(+process.argv[2] * 1024 * 1024,'x') console.log(Math.round(buf.length / (1024 * 1024))) console.log(Math.round(process.memoryUsage().rss / (1024 * 1024))) 

Führen Sie diesmal den Container aus und richten Sie die Funktionen für die Arbeit mit der Auslagerungsdatei explizit ein:

 docker run --memory 1024m --memory-swap=1024m --memory-swappiness=0 --interactive --tty ravali1906/dockermemory bash 

Starten Sie die Anwendung:

 $ node buffer_example_fill.js 2000 Killed 

Siehe die Nachricht Killed ? Wenn der Schlüsselwert --memory-swap gleich dem Schlüsselwert --memory , teilt dies dem Container mit, dass die --memory nicht verwendet werden soll. Darüber hinaus kann der Kernel des Betriebssystems, in dem der Container selbst ausgeführt wird, standardmäßig eine bestimmte Anzahl anonymer Speicherseiten, die vom Container verwendet werden, in die Auslagerungsdatei kopieren. --memory-swappiness auf 0 deaktivieren wir diese Funktion. Infolgedessen stellt sich heraus, dass die Auslagerungsdatei nicht im Container verwendet wird. Der Prozess endet, wenn die RSS-Metrik das Speicherlimit des Containers überschreitet.

Allgemeine Empfehlungen


Wenn Node.js-Anwendungen mit dem --max-old-space-size gestartet werden, dessen Wert das beim --max-old-space-size des Containers festgelegte Speicherlimit überschreitet, scheint es, dass Node.js das Containerlimit nicht beachtet. Wie aus den vorherigen Beispielen hervorgeht, ist der offensichtliche Grund für dieses Verhalten die Tatsache, dass die Anwendung einfach nicht das gesamte Heap-Volume verwendet, das mit dem --max-old-space-size .

Denken Sie daran, dass sich die Anwendung nicht immer gleich verhält, wenn sie mehr Speicher verwendet, als im Container verfügbar ist. Warum? Tatsache ist, dass der aktive Speicher (RSS) des Prozesses von vielen externen Faktoren beeinflusst wird, die die Anwendung selbst nicht beeinflussen kann. Sie hängen von der Belastung des Systems und den Eigenschaften der Umgebung ab. Dies sind beispielsweise Funktionen der Anwendung selbst, der Grad der Parallelität im System, Funktionen des Betriebssystem-Schedulers, Funktionen des Garbage Collector usw. Darüber hinaus können sich diese Faktoren von Start zu Start ändern.

Empfehlungen zum Festlegen der Größe des Node.js-Heaps für Fälle, in denen Sie diese Option steuern können, jedoch nicht mit Speicherbeschränkungen auf Containerebene


  • Führen Sie die minimale Node.js-Anwendung im Container aus und messen Sie die statische RSS-Größe (in meinem Fall sind dies für Node.js 10.x etwa 20 MB).
  • Der Node.js-Heap enthält nicht nur den alten_Space, sondern auch andere (z. B. den neuen_Space, den Code_Space usw.). Wenn Sie die Standardkonfiguration der Plattform berücksichtigen, sollten Sie sich daher darauf verlassen, dass das Programm etwa 20 MB mehr Speicher benötigt. Wenn sich die Standardeinstellungen geändert haben, müssen diese Änderungen ebenfalls berücksichtigt werden.
  • Jetzt müssen wir den erhaltenen Wert (angenommen, er beträgt 40 MB) von der im Container verfügbaren Speichermenge subtrahieren. Was bleibt, ist ein Wert, der ohne Angst vor einer Programmausführung aufgrund von Speichermangel als Schlüsselwert angegeben werden kann - --max-old-space-size .

Empfehlungen zum Festlegen von Containerspeichergrenzen für Fälle, in denen dieser Parameter gesteuert werden kann, die Anwendungsparameter von Node.j jedoch nicht


  • Führen Sie die Anwendung in Modi aus, mit denen Sie die Spitzenwerte des von ihr verbrauchten Speichers ermitteln können.
  • Analysieren Sie den RSS-Score. Insbesondere hier kann zusammen mit der Methode process.memoryUsage() der Linux-Befehl top nützlich sein.
  • Vorausgesetzt, dass in dem Container, in dem die Anwendung ausgeführt werden soll, nichts anderes als die Ausführung ausgeführt wird, kann der erhaltene Wert als Container-Speicherlimit verwendet werden. Um sicher zu gehen, wird empfohlen, diese um mindestens 10% zu erhöhen.

Zusammenfassung


In Node.js 12.x werden einige der hier diskutierten Probleme gelöst, indem die Größe des Heaps adaptiv angepasst wird, was entsprechend der Menge des verfügbaren RAM durchgeführt wird. Dieser Mechanismus funktioniert auch, wenn Node.js-Anwendungen in Containern ausgeführt werden. Die Einstellungen können jedoch von den Standardeinstellungen abweichen. Dies ist beispielsweise der --max_old_space_size , wenn beim Starten der Anwendung der Schlüssel --max_old_space_size verwendet wurde. Für solche Fälle bleibt alles oben Genannte relevant. Dies legt nahe, dass jeder, der Node.js-Anwendungen in Containern ausführt, vorsichtig und verantwortungsbewusst mit den Speichereinstellungen umgehen sollte. Darüber hinaus kann die Kenntnis der Standardbeschränkungen für die Speichernutzung, die eher konservativ ist, die Anwendungsleistung verbessern, indem diese Beschränkungen absichtlich geändert werden.

Liebe Leser! Haben Sie nicht genügend Speicherprobleme, wenn Sie Node.js-Anwendungen in Docker-Containern ausführen?



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


All Articles