Was befindet sich in meinem JVM-Speicher?
"The Mendix Die Runtime führt das im Modeler erstellte Modell aus. Sie stellt dem Client Seiten zur Verfügung, führt Mikroflüsse aus, ruft Webdienste auf, generiert Dokumente, kommuniziert mit der Datenbank und vieles mehr.“
Der Mendix Runtime ist ein in Java und Scala geschriebenes Programm, das eine Java Virtual Machine (JVM) verwendet. Beim Ausführen verbraucht es eine gewisse Menge an Systemspeicher in Ihrem Betriebssystem.
Was befindet sich in meinem JVM-Speicher?
Schauen wir uns ein mögliches Diagramm an, das die Speichernutzung der JVM zeigt:

In dieser Grafik sehen wir eine Anwendung, die über 512 MB Java Heap-Speicherplatz verfügt. Dies ist der Speicherplatz, der hauptsächlich von der Mendix Anwendung selbst. Wenn Sie in einem Mikrofluss ein Objekt abrufen, werden hier die aus der Datenbank gelesenen Objekte im Speicher aufbewahrt.
Der Mendix Runtime verwendet diesen Speicherplatz auch zum Speichern von Java-Objekten für angemeldete Sitzungen, Zwischenobjekten, die die Rohdaten aus der Datenbank in Objekte umwandeln, die in einem Mikrofluss verwendet werden können, oder in Objekte, die direkt weitergeleitet werden können, wenn es sich um Daten handelt, die in einem Datenraster in Ihrem Webbrowser angezeigt werden müssen. Wussten Sie überhaupt, dass sogar Ihre Mikroflüsse selbst Objekte hier sind? Die Mendix Runtime ist ein Interpreter und speichert auch eine Kopie Ihrer modellierten Anwendung in seinem Speicher.
Verschiedene Teile dieses Speicherplatzes werden vom JVM Garbage Collector verwaltet. (siehe auch die JVM-Heap-Dokumentation)
Das zuvor gezeigte Beispiel stammt eigentlich von einer Anwendung, die eigentlich gar nicht verwendet wird, für die aber wahrscheinlich ein Ereignis geplant ist, das die Erstellung einer Reihe von Objekten bewirkt, die kurz darauf einer Speicherbereinigung unterzogen werden können.
Die JVM ist nicht allein …
Dieser Java-Speicher muss nun auch in den Systemspeicher des Betriebssystems selbst passen. Wenn Sie das Mendix Cloud, Sie können diese Diagramme im Überwachungsteil unseres Bereitstellungsportals anzeigen.
Hier ist das Diagramm, das die tatsächliche Nutzung des Betriebssystemspeichers anzeigt. Es handelt sich um einen sogenannten Appnode, der für die Ausführung des Anwendungsprozesses einer einzelnen Umgebung eines Mendix Anwendung (also entweder eine Test-, Abnahme- oder Produktionsumgebung).

Sieht doch ganz richtig aus, oder? Es werden etwa 500 MB verwendet, das muss also der Java Heap sein, den wir oben gesehen haben. Die Datenbank läuft auf einer anderen virtuellen Maschine, daher ist die Datenbanknutzung hier nicht sichtbar. Anscheinend Mendix Bereitstellung einer virtuellen Linux-Maschine mit 1024 MB Arbeitsspeicher, damit Ihr Java-Prozess hineinpasst.
Warte was?
Schauen wir uns nun ein weiteres Beispiel an:


Moment mal... Warum werden im Betriebssystem mehr als 900 MB Speicher verwendet, anstatt nur die 512 MB, die es verwenden sollte? Ist Mendix heimlich einige andere Prozesse neben unserer Anwendung ausführen, um den ansonsten ungenutzten Speicher zu nutzen?
Nein, das ist nicht der Fall. Wirklich. 🙂 Na gut … es gibt einige zusätzliche Prozesse, die neben der Anwendung selbst immer laufen, beispielsweise zwei Überwachungsagenten, einer, der das Trendsystem mit Daten versorgt (um diese Diagramme zu erstellen) und einer, der Daten an das Warnsystem liefert. Und es gibt einen Prozess, der Ihre Anwendung stoppen und starten kann, wenn er ein Signal vom Bereitstellungsportal erhält, wenn Sie dort die Stopp- oder Start-Schaltflächen drücken.
Aber all das war auch im ersten gezeigten Beispiel vorhanden. Also, was ist hier los…?
Schauen wir uns die Ausgabe des Befehls „ps“ an, der anzeigen kann, wie viel Speicher jeder Prozess auf einem Server belegt:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
xxxxx 19137 1.8 84.7 1248796 864696 ? Sl 2014 1177:33 java -Dfile.encoding=UTF-8 -XX:MaxPermSize=128M -Xmx512M -Xms512M -Djava.io.tmp...
Ja, es ist der Java-Prozess, der tatsächlich 864696 kB Speicher verbraucht.
Warum die Mühe?
Warum sollten wir uns darüber Gedanken machen? Schließlich passt der Prozess immer noch in den verfügbaren 1024 MB Speicher, selbst wenn dieser 512 MB große Java-Prozess auf magische Weise auf 844 MB anwächst.
Tatsächlich verursacht dieses Verhalten einige Probleme, da es die Ausführung anderer Programme auf dem Server verhindert, die von Zeit zu Zeit gestartet werden.
Wir führen Releases und Sicherheitsupdates durch …
At Mendix, wir führen regelmäßig und wahrscheinlich viel häufiger, als Sie vermuten würden, Änderungen an unserer Hosting-Plattform durch. Wöchentlich oder häufiger sogar mehrmals pro Woche werden kleine Änderungen an unserer gesamten Hosting-Plattform vorgenommen. Anstatt neue Funktionen und Fehlerbehebungen in einer riesigen monatlichen Version anzuhäufen, führen wir sie ein, sobald sie produktionsreif sind.
Und sie müssen rennen!
Um Änderungen in die Produktion zu bringen, verwenden wir hauptsächlich Puppet- und Debian-Pakete, um unsere Software auf Anwendungs- und Datenbankservern zu aktualisieren, auf denen Kundenprojekte laufen. Darüber hinaus gibt es ein Programm namens „Unattended Upgrades“, das Sicherheitsupdates auf allen Servern installiert, sobald diese verfügbar sind.
Dieser Java-Prozess, der über alle Maßen wächst, kann also verhindern, dass dies passiert. Wenn Sie keine Releases installieren, kann es sein, dass Ihre Anwendung Kompatibilitätsprobleme mit dem Rest des Bereitstellungssystems bekommt. Niemand sollte es sich wünschen, dass Sicherheitsupdates verpasst werden.
Glücklicherweise tritt dieses Problem nur in einer sehr begrenzten Anzahl von Umgebungen auf (derzeit 0.53 % aller Anwendungsumgebungen).
Linux-Kernel zur Rettung!
Der erste Schritt im Prozess, herauszufinden, was hier vor sich geht, und dieses Verhalten zu beheben, besteht darin, mehr Einblick in die Situation zu geben.
Wenn ein Java-Prozess so konfiguriert ist, dass er 512 MB Heap-Speicherplatz verwendet … was bedeutet das überhaupt? Das oben dargestellte Java-Heap-Diagramm zeigt einen Teil des Speichers, der zum Speichern tatsächlicher Objekte verwendet wird, die im Java-Code verwendet werden. Aber um einen Java-Prozess auszuführen … müssen wir das Java-Programm selbst starten. Muss dieses Programm auch im Speicher vorhanden sein? Befindet es sich in diesem JVM-Heap-Speicher? Und was ist mit den unzähligen JAR-Dateien, die wir gerade in unseren Userlib-Speicherort des Projekts gelegt haben? Was passiert mit ihnen?
Um dies besser zu verstehen, wäre es schön, ein Diagramm zu haben, das nicht nur den Java-Heap-Speicherplatz zeigt, sondern auch den Speicherplatz, den der gesamte Java-Prozess im Betriebssystem einnimmt.
…durch die Bereitstellung detaillierter Informationen zur Speichernutzung
Glücklicherweise ermöglicht uns der Linux-Kernel, mithilfe des Proc-Dateisystems einen vollständigen Überblick über den Speicheradressraum abzurufen, den ein einzelner Prozess belegt. Die PID (Programmkennung) des Beispiel-Java-Prozesses, den ich zuvor gezeigt habe, ist 19137. Das bedeutet, dass ich den folgenden Befehl in einer Linux-Shell-Eingabeaufforderung ausführen kann, damit der Linux-Kernel alles über die Speicherzuweisung dieses Prozesses anzeigt: cat /proc/19137/smaps
Mithilfe der verfügbaren SMAP-Informationen ist es möglich, recht genaue und fundierte Vermutungen darüber anzustellen, was im JVM-Prozess vor sich geht.
Hier ist das neue Diagramm zur JVM-Prozessspeichernutzung, das zum allerersten Anwendungsdiagramm auf dieser Seite gehört:

Aha! Es war also doch kein 512 MB großer Java-Heap, der den Speicher belegte! Es waren nur etwa 230 MB, und der andere Teil scheint von Speicher belegt zu sein, der außerhalb des Java-Objekt-Heaps zugewiesen ist. Ein Teil davon ist der permanente Generierungs- und Code-Cache, der Objekte speichert, die die JVM für nicht interessant genug zum Garbage Collect hält, weil sie nie verschwinden, solange die Anwendung läuft. Ein anderer Teil ist der native Speicherteil.
Schauen wir uns nun den Graphen der problematischen Anwendung an:

In diesem Szenario wird der Java-Heap aktiv verwendet, d. h. er wurde tatsächlich vom Betriebssystem zugewiesen, und es wurde ihm nicht nur versprochen, dass er bei tatsächlichem Bedarf zur Verfügung stehen könnte.
Die wichtigste Erkenntnis hieraus besteht darin, dass wir getäuscht wurden und dachten, der eigentliche Betriebssystemspeicher sei vollständig durch den JVM-Objektheap belegt, obwohl dies nicht der Fall war.
Wenn es wirklich außer Kontrolle gerät …
Zum Abschluss dieses Blogbeitrags hier ein Beispiel für eine Situation, die wirklich alle Grenzen vernünftigen Verhaltens überschreitet.
Hier haben wir eine 1024 MB große Java-Heap-Konfiguration, die von einem 512 MB großen Java-Objekt-Heap auf einen 1 GB großen Objekt-Heap aktualisiert wurde:

Leider explodierte der Speicherverbrauch des JVM-Prozesses und wir mussten die Größe des Betriebssystems ändern, um damit klarzukommen:

Das neue JVM-Prozessspeicherdiagramm zeigt, was hier vor sich geht:

To be continued ...
Die in den letzten Wochen durchgeführten Untersuchungen zu diesem Thema zeigen bereits eine Reihe möglicher Lösungen für die zunehmenden Probleme mit zu wenig Heap-Speicher. In einem zukünftigen Blogbeitrag werde ich näher darauf eingehen. Kundenanwendungen, bei denen die oben genannten Release- und Sicherheitsupdate-Probleme auftreten, können mit einem Anruf von rechnen. Mendix darüber, wie wir kurzfristig damit umgehen werden.
So funktioniert’s
Um diese Diagramme zu zeichnen, habe ich mir alle Informationen angesehen, die in der SMAP-Datei im Linux-Prozedurdateisystem verfügbar sind. Durch Reverse Engineering einiger Teile und einige fundierte Vermutungen habe ich eine Erweiterung der m2ee-Überwachungsdiagrammkonfiguration erstellt, die wir zum Erstellen von Trends verwenden. Der Überwachungs-Plugin-Code, der die SMAP-Informationen aus dem Linux-Kernel untersucht, befindet sich im m2ee-tools Quellcode. Der Munin-Plugin-Dokumentation enthält einige Hinweise, wie Sie dieses Plugin bei Bedarf selbst aktivieren können.