Seite wählen

NETWAYS Blog

Out-of-Memory: Marathon 1.6+ und Heap Size

Auch wenn in Sachen Container Orchestrierung Kubernetes klar gegen Marathon und Mesos gewonnen hat bietet das Duo seit Jahren eine zuverlässig Plattform. Durch die stetige Weiterentwicklung und die Umstellung auf Systemd ändert sich leider auch immer wieder die Konfigurationen der Dienste.

Für einen stabilen Betrieb von Marathon sollte die maximale Heap-Size der Java Virtual Machine (JVM) eine angemessene Größe haben. Zu wenig Memory äußert sich z.B. durch erhöhte Antwortzeiten oder durch die Aktivierung des OOM-Killer. Wo in vergangenen Versionen und Konfigurationen noch die /etc/defaults/marathon zum Setzen der Javaoption verwendet wurde, kann man hier seit Marathon 1.6 vergeblich sein Glück suchen. Aus meiner Sicht ist ein Verzicht auf /etc/defaults/marathon und eine saubere Definition in einer systemd Unit-Datei natürlich zu begrüßen. Die Umgebungsvariablen lassen sich auch dort einfach definieren, aber wie genau muss das für Marathon aussehen?

# systemctl edit marathon.service
[Service]
Environment=JAVA_OPTS=-Xmx2g

Mit systemclt edit kann man eine bestehende Unit-Datei einfach erweitern und der Parameter Environment setzt die Umgebungsvariable.  Alternativ könnte man auch Environmentfile=/etc/defaults/marathon verwenden um das gewohnte Verhalten wieder herzustellen.

Nach dem Speichern braucht es einen Daemon-Reload, damit die neue Unit-Datei eingelesen wird.

# systemctl daemon-reload
# systemctl restart marathon.service

Eigentlich bleibt also alles beim Alten, man muss nur die zusätzlichen Parameter in die JAVA_OPTS setzen und den Service neu starten ?.

 

Achim Ledermüller
Achim Ledermüller
Senior Manager Cloud

Der Exil Regensburger kam 2012 zu NETWAYS, nachdem er dort sein Wirtschaftsinformatik Studium beendet hatte. In der Managed Services Abteilung ist er für den Betrieb und die Weiterentwicklung unserer Cloud-Plattform verantwortlich.

PHP SPL: Heaps

Viele Daten im Nachhinein zu sortieren kostet Zeit. Seit PHP Version 5.3 gibt es in der SPL die Heap Klassen welche eine Sortierung on-the-fly erlauben. PHP stellt eine generische Heap Klasse bereit (hier im unteren Beispiel) um eigenen Datentypen zu vergleichen. Es gibt aber auch eigene Klassen für Min- und Max-Heaps.
Heap Strukturen eignen sich im Allgemeinen erst bei einer größeren Menge an Daten. So ist ein Max-Heap mit 1.000.000 Einträgen fast um ein drittel schneller als ein konventionelles Array welches anschließend eine absteigende Sortierung erfährt.

class TimestampMaxHeap extends SplHeap
{
    public function compare($array1, $array2)
    {
        if ($array1[0] === $array2[0]) {
            return 0;
        }
        return $array1[0] < $array2[0] ? -1 : 1;
    }
}
$heap = new TimestampMaxHeap();
$heap->insert(array(new DateTime('2014-01-04'), 4));
$heap->insert(array(new DateTime('2014-01-01'), 1));
$heap->insert(array(new DateTime('2014-01-06'), 6));
$heap->insert(array(new DateTime('2014-01-03'), 3));
$heap->insert(array(new DateTime('2014-01-05'), 5));
$heap->insert(array(new DateTime('2014-01-02'), 2));
foreach ($heap as $entry) {
    echo $entry[1] . ' ';
}
echo PHP_EOL;
Marius Hein
Marius Hein
Head of IT Service Management

Marius Hein ist schon seit 2003 bei NETWAYS. Er hat hier seine Ausbildung zum Fachinformatiker absolviert und viele Jahre in der Softwareentwicklung gearbeitet. Mittlerweile ist er Herr über die interne IT und als Leiter von ITSM zuständig für die technische Schnittmenge der Abteilungen der NETWAYS Gruppe. Wenn er nicht gerade IPv6 IPSec Tunnel bohrt, sitzt er daheim am Schlagzeug und treibt seine Nachbarn in den Wahnsinn.

Heap-Fragmentation analysieren

Bei Dateisystemen ist der Fragmentations-Effekt ziemlich bekannt:

  1. Zunächst werden viele Dateien erstellt.
  2. Einige dieser Dateien werden anschließend gelöscht.
  3. Wenn nun eine große Datei angelegt wird und anderweitig kein Platz mehr frei ist, wird diese in mehrere Teile aufgesplittet, um die entstandenen Lücken zu füllen. – Es entsteht Fragmentation.

Während das Problem bei Dateisystemen durch Neuanordnung der Dateiteile (Defragmentation) vergleichsweise einfach gelöst werden kann, gibt es beim Arbeitsspeicher ein ähnliches Problem, das sich nicht ganz so einfach in den Griff bekommen lässt.
Für malloc() muss die libc immer mehr Speicher vom Kernel anfordern, wenn kein Speicherbereich mehr frei ist, der groß genug ist. Dabei kann die libc bestehende Allocations nicht im Speicher verschieben, da sie nicht weiss, wo die Anwendung Pointer auf diese Allocations besitzt. Eine nachträgliche Defragmentierung ist somit nicht möglich.
Um dieses Problem zu veranschaulichen, habe ich einen Profiler geschrieben (fprofile.cpp), der malloc(), realloc() und free() instrumentiert, um beobachten zu können, wie sich die Speicherbenutzung bei meinem Testprogramm (Icinga 2) mit der Zeit verändert.
Der Profiler ist eine Library, die prinzipiell aus zwei Teilen besteht:

  • Einem eigenen Allocator: Der Profiler benötigt für die Profil-Daten selbst Speicher. Wenn ich hier malloc() verwenden würde, würde der Profiler eventuell selbst zur Heap-Fragmentation beitragen. Deswegen habe ich einen vergleichsweise naiven Allocator geschrieben, der intern mmap() verwendet.
  • Dem eigentlichen Profiler: Um malloc(), realloc() und free() durch meine eigenen Funktionen ersetzen zu können, muss die Library per LD_PRELOAD geladen werden. Der Profiler merkt sich, welche Speicherbereiche von malloc() „belegt“ wurden und schreibt diese Informationen sekündlich als Bild in eine Datei. Die Wahl ist dabei auf das PNM-Format gefallen, da es trivial zu schreiben ist und keine externen Libraries benötigt (http://en.wikipedia.org/wiki/Portable_anymap).

Der Profiler kann mit folgendem Befehl kompiliert werden:

g++ -fPIC -shared -g -o libfprofiler.so fprofiler.cpp -pthread

Anschließend können wir unsere eigene Anwendung wie folgt profilen:

LD_PRELOAD=./libfprofiler.so ./sbin/icinga2 -c ...

Der Profiler erstellt nun im aktuellen Verzeichnis seine Reports:

$ ls *.pnm
frame100.pnm  frame105.pnm  frame110.pnm  frame115.pnm  frame120.pnm
frame101.pnm  frame106.pnm  frame111.pnm  frame116.pnm  frame121.pnm
frame102.pnm  frame107.pnm  frame112.pnm  frame117.pnm  frame122.pnm
frame103.pnm  frame108.pnm  frame113.pnm  frame118.pnm  frame123.pnm
frame104.pnm  frame109.pnm  frame114.pnm  frame119.pnm  frame124.pnm

Die Dateien können wir nun mit einem beliebigen Bildbearbeitungsprogramm öffnen (GIMP, EOG, usw.).
Jeder Pixel steht dabei für eine Speicherseite (4kB). Weiß bedeutet, dass zumindest ein Teil der Seite belegt ist. Schwarze Pixel stehen für komplett freie Seiten. Wenn zwischen den Adressen von zwei Allocations mehr als 100MB Unterschied ist, zeichnet der Profiler eine blaue Linie.
In den folgenden beiden Beispielreports ist der Speicherverbrauch von Icinga 2 zu sehen. Im ersten Bild ist Icinga 2 dabei gerade mit dem Parsen der Config fertig geworden – während das zweite Bild den Stand zeigt, nachdem Icinga 2 alle Speicherbereiche freigegeben hat, die der Config-Parser intern benötigt hat:
frame120
frame130
Sehr schön sieht man im zweiten Bild, wie der Heap nun ziemlich viele Lücken aufweist. Auch wenn rein rechnerisch nur ein Bruchteil des Speichers tatsächlich verwendet wird, kann die libc diese Speicherbereiche nicht an den Kernel zurückgeben.
Im Allgemeinen gibt es folgende Lösungsansätze, um Heap-Fragmentation zu vermeiden:

  • Unterschiedliche Heaps für Objekte mit unterschiedlicher Lebensdauer verwenden (z.B. eigener Heap für den Config-Parser)
  • Anzahl der gleichzeitig vorhandenen Allocations verringern
  • Speicherverbrauch allgemein verringern

Abschließend noch ein Tipp: Mit ffmpeg kann man aus den *.pnm-Dateien einen Film generieren:

ffmpeg -f image2 -r 1 -pattern_type glob -i 'frame*.pnm' -r 24 output.mp4