Bei Dateisystemen ist der Fragmentations-Effekt ziemlich bekannt:
- Zunächst werden viele Dateien erstellt.
- Einige dieser Dateien werden anschließend gelöscht.
- 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:
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