Speicherlecks in Prozessen systematisch aufspüren
AI generated
$
/etc
Linux · Troubleshooting · Speicherverwaltung · PHP-FPM
Speicherlecks in Prozessen systematisch aufspüren
Warum ein Neustart das Symptom behebt, aber nie die Ursache

Ein Speicherleck zeigt sich zuerst als stetig wachsender RES-Wert in top, gefolgt von immer knapperem freiem Arbeitsspeicher und irgendwann einem OOM-Kill mitten am Tag. Wer den betroffenen Dienst einfach neu startet, gewinnt Zeit, aber keine Erkenntnis. Dieser Leitfaden zeigt, wie man mit Bordmitteln wie smaps_rollup, smem und pmap systematisch herausfindet, welcher Prozess Speicher verliert und wo im Code oder in der Konfiguration die Ursache liegt.

17 Min. Lesezeit smem · pmap · smaps_rollup · PHP-FPM Linux · Troubleshooting · Speicheranalyse

1. Was ein Speicherleck unter Linux wirklich ist

Ein Speicherleck liegt vor, wenn ein Prozess über die Zeit immer mehr Arbeitsspeicher belegt, ohne ihn wieder freizugeben, obwohl die eigentliche Arbeitslast konstant bleibt. Unter Linux bedeutet das konkret: Der Prozess fordert über malloc() oder Sprachlaufzeiten wie den PHP-Zend-Allocator immer neue Speicherseiten an, gibt sie aber wegen eines Programmierfehlers, einer zirkulären Referenz oder einer schlecht konfigurierten Cache-Schicht nie an das Betriebssystem zurück. Der Kernel selbst hat kein Konzept von Speicherlecks, er sieht nur, dass ein Prozess immer mehr physischen Speicher (RSS) beansprucht.

Wichtig ist die Abgrenzung zu normalem Speicherverhalten: Viele Anwendungen wachsen beim Start schnell, weil sie Caches füllen, Verbindungspools aufbauen oder Opcode-Caches wie OPcache initialisieren. Das ist kein Speicherleck, sondern ein einmaliger Anstieg auf ein stabiles Plateau. Ein echtes Leck erkennt man daran, dass die Kurve nach dem Plateau nicht abflacht, sondern über Stunden oder Tage linear oder stufenweise weiter ansteigt, während die Anzahl der bearbeiteten Anfragen konstant bleibt. Diese Unterscheidung zwischen normalem Wachstum und echtem Leck ist der erste und wichtigste Schritt jeder Diagnose.

2. Symptome erkennen: RSS-Wachstum, Swap-Druck, OOM-Kills

Das früheste sichtbare Symptom eines Speicherlecks ist ein stetig steigender RES-Wert in top oder htop für einen einzelnen Prozess oder eine Gruppe gleichartiger Worker, etwa alle PHP-FPM-Kindprozesse gleichzeitig. Parallel dazu sinkt der als "available" ausgewiesene Speicher in free -h kontinuierlich, während der Page-Cache verdrängt wird, weil der Kernel ihn zugunsten der wachsenden Prozesse verkleinert. Auf Servern mit aktivem Swap zeigt sich das Leck oft zuerst durch steigende Swap-Nutzung und wachsende si/so-Werte in vmstat, weil der Kernel versucht, selten genutzte Seiten auszulagern, um Platz zu schaffen.

Im Endstadium greift der OOM-Killer ein und beendet den Prozess mit dem höchsten Speicherverbrauch, sichtbar in dmesg als "Out of memory: Killed process". Das Problem dabei: Der OOM-Killer trifft oft nicht den eigentlichen Übeltäter, sondern den Prozess mit der ungünstigsten oom_score, was Verwirrung stiftet, wenn etwa der MySQL-Server statt des tatsächlich leckenden PHP-Workers beendet wird. Ein systematischer Blick in /proc/[pid]/oom_score vor dem nächsten Kill hilft, den wahren Kandidaten zu identifizieren, statt sich von der Symptomatik in die falsche Richtung leiten zu lassen.

3. Speicherbelegung präzise messen mit /proc/[pid]

Bevor man Werkzeuge wie smem installiert, lohnt sich der direkte Blick in /proc/[pid]/status und /proc/[pid]/smaps_rollup, weil beide ohne Zusatzsoftware auf jedem Linux-System verfügbar sind. VmRSS in status zeigt den insgesamt residenten Speicher, VmSwap den bereits ausgelagerten Anteil. Für ein Speicherleck entscheidend ist aber die Aufschlüsselung in smaps_rollup, die zwischen Rss, Pss (proportional geteilter Speicher), Private_Dirty und Shared_Clean unterscheidet. Ein wachsender Private_Dirty-Wert bei stabilem Shared_Clean ist ein starkes Indiz dafür, dass der Prozess selbst und nicht eine gemeinsam genutzte Bibliothek für das Wachstum verantwortlich ist.

Ein einfaches Wiederholungsskript macht dieses manuelle Nachschauen zu einer verlässlichen Zeitreihe. Statt einmalig hinzuschauen, protokolliert man die Werte in festen Intervallen und kann später eine klare Wachstumskurve statt einer subjektiven Einschätzung vorlegen. Genau diese Kurve ist es, die im Gespräch mit Entwicklern den Unterschied zwischen "fühlt sich nach Leck an" und "beweisbar 40 MB Wachstum pro Stunde" macht.


#!/usr/bin/env bash
# track-rss.sh — sample RSS and private-dirty memory for a PID over time
set -euo pipefail

PID="${1:?Usage: track-rss.sh <pid> <log-file>}"
LOG_FILE="${2:?Usage: track-rss.sh <pid> <log-file>}"
INTERVAL=60

echo "timestamp,rss_kb,pss_kb,private_dirty_kb" > "$LOG_FILE"

while kill -0 "$PID" 2>/dev/null; do
  ts="$(date -Iseconds)"
  # Read the aggregated smaps rollup — no external tools required
  rss=$(awk '/^Rss:/ {print $2}' "/proc/$PID/smaps_rollup")
  pss=$(awk '/^Pss:/ {print $2}' "/proc/$PID/smaps_rollup")
  dirty=$(awk '/^Private_Dirty:/ {print $2}' "/proc/$PID/smaps_rollup")
  echo "$ts,$rss,$pss,$dirty" >> "$LOG_FILE"
  sleep "$INTERVAL"
done

echo "Process $PID exited — log written to $LOG_FILE"

4. smem und pmap: Shared von Private-Speicher trennen

smem baut auf denselben /proc-Daten auf, präsentiert sie aber aggregiert und mit dem sogenannten PSS (Proportional Set Size), das gemeinsam genutzten Speicher fair auf alle beteiligten Prozesse aufteilt. Das ist entscheidend, weil eine reine RSS-Summe über mehrere PHP-FPM-Worker den geteilten OPcache-Speicher mehrfach zählen und so ein Speicherleck vortäuschen würde, wo tatsächlich nur normale Bibliotheks- und Opcode-Cache-Nutzung vorliegt. Mit smem -tk -P php-fpm erhält man eine sortierte Übersicht aller PHP-FPM-Prozesse samt USS (rein privatem Speicher), was die verlässlichste Kennzahl für tatsächlichen, nicht geteilten Verbrauch ist.

pmap -x ergänzt diese Sicht, indem es die Speicher-Mappings eines einzelnen Prozesses auflistet: Heap, Stack, geladene Bibliotheken und anonyme Mappings, jeweils mit Größe und Dirty-Anteil. Ein Speicherleck im Anwendungscode zeigt sich hier typischerweise als kontinuierlich wachsender Heap-Eintrag ([heap]) oder als eine wachsende Zahl anonymer Mappings, während Bibliotheks-Mappings stabil bleiben. Wer regelmäßig pmap -x $PID | tail -1 für die Gesamtsumme mitschreibt, sieht sofort, ob das Wachstum im Heap oder in dynamisch nachgeladenen Bibliotheken stattfindet.


# Install smem if not already present (Debian/Ubuntu)
sudo apt-get install -y smem

# Sorted overview of all php-fpm workers by private memory (USS)
smem -tk -P php-fpm

# Example output (trimmed):
#   PID User     Command                         Swap      USS      PSS      RSS
#  4821 www-data php-fpm: pool www              0        48.2M    52.1M    61.4M
#  4822 www-data php-fpm: pool www              0        49.8M    53.6M    62.9M
#  4823 www-data php-fpm: pool www              0        91.3M    95.7M   104.2M   <- outlier
#  ----------------------------------------------------------------------
#                 3                              0       189.3M   201.4M   228.5M

# Detailed memory map for the suspicious worker (PID 4823)
pmap -x 4823 | sort -k3 -n -r | head -20

# Watch heap growth of a specific process every 30 seconds
watch -n 30 'pmap -x 4823 | grep "\[ heap \]"'

Eine einzelne Momentaufnahme beweist nie ein Speicherleck, weil normale Lastschwankungen ähnliche Werte erzeugen können. Erst eine Zeitreihe über mehrere Stunden oder Tage zeigt, ob der Speicherverbrauch nach einem Anstieg wieder abfällt (normales Verhalten unter Last) oder monoton weiter wächst (Leck). Ein einfacher Cron-Job, der alle fünf Minuten smem oder das oben gezeigte track-rss.sh-Skript ausführt und in eine CSV-Datei schreibt, liefert die nötigen Rohdaten für eine belastbare Aussage.

Für die Auswertung genügt oft schon ein einfacher Plot mit gnuplot oder ein Import in eine Tabellenkalkulation. Wichtiger als die Visualisierung ist aber die Korrelation mit externen Ereignissen: Steigt der Speicherverbrauch nach jedem Deployment neu an, deutet das auf ein Leck in neuem Code hin. Wächst er dagegen unabhängig von Deployments gleichmäßig über Tage, liegt die Ursache eher in einer selten ausgelösten Codepfad-Kombination oder einem lange laufenden Cache ohne Eviction-Strategie. Diese Korrelation einzubauen ist der Unterschied zwischen einem Diagramm und einer echten Diagnose.

6. PHP-FPM-Worker: Der häufigste Verdächtige im Webstack

In PHP-basierten Setups wie Magento ist der PHP-FPM-Worker-Prozess die mit Abstand häufigste Quelle beobachtbarer Speicherlecks, weil ein einzelner Request-Handler-Prozess über viele Requests hinweg wiederverwendet wird und sich Zustand ansammeln kann, der eigentlich pro Request verworfen werden sollte. Statische Klassenvariablen, nie geleerte Objekt-Caches im Anwendungscode oder Erweiterungen mit eigenem C-Speicher-Handling (etwa fehlerhafte PHP-Extensions) sind typische Ursachen. Die eingebaute Gegenmaßnahme in PHP-FPM ist pm.max_requests: Nach der konfigurierten Anzahl an Requests wird der Worker-Prozess beendet und neu gestartet, wodurch der Speicher garantiert freigegeben wird, unabhängig davon, ob die eigentliche Ursache im Code behoben ist.

Dieser Mechanismus behebt aber nur das Symptom, nicht die Ursache, und kann bei sehr aggressiven Lecks (etwa mehreren hundert Megabyte in wenigen Minuten) sogar zu spät greifen. Sinnvoller ist es, pm.max_requests als Absicherung zu behalten, aber parallel über request_slowlog_timeout und den PHP-FPM-Statusendpunkt (pm.status_path) die tatsächliche Speicherentwicklung pro Worker zu beobachten. Ein Worker, der kurz vor dem konfigurierten Limit deutlich mehr Speicher belegt als seine Geschwister, ist ein starker Hinweis auf einen konkreten, reproduzierbaren Trigger-Request, den man über die Access-Logs zurückverfolgen kann.


; /etc/php/8.4/fpm/pool.d/www.conf — leak mitigation and observability
[www]
; Recycle workers after N requests — bounds the blast radius of any leak
pm.max_requests = 500

; Expose per-pool status for external monitoring
pm.status_path = /fpm-status
ping.path = /fpm-ping

; Log requests slower than 5 seconds with a full backtrace
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log

; Emergency restart if a worker exceeds this much memory (requires php.ini)
; php_admin_value[memory_limit] = 512M

7. Tiefer graben: strace, massif und Application-Metriken

Wenn Worker-Recycling und Monitoring den Verdacht bestätigen, aber die genaue Codestelle unklar bleibt, braucht es tiefere Werkzeuge. strace -e trace=memory -p $PID zeigt jeden brk()- und mmap()-Syscall in Echtzeit und macht sichtbar, ob der Speicher in großen, seltenen Sprüngen (typisch für einen einzelnen fehlerhaften Request) oder in vielen kleinen Schritten wächst. Für PHP-Anwendungen selbst ist memory_get_usage(true) und memory_get_peak_usage(true) an strategischen Stellen im Code oft aussagekräftiger als jedes Systemwerkzeug, weil es die Sicht der PHP-Laufzeit statt der Betriebssystemsicht zeigt.

Für native Erweiterungen oder C-Bibliotheken, die von PHP eingebunden werden, ist valgrind --tool=massif das gründlichste, aber auch langsamste Werkzeug: Es protokolliert jede Speicherallokation über die Laufzeit und erzeugt mit ms_print ein Diagramm, das exakt zeigt, welche Aufrufkette für das Wachstum verantwortlich ist. Wegen des erheblichen Performance-Overheads von Valgrind eignet sich dieser Ansatz nur in einer Staging-Umgebung mit reproduziertem Leck, nicht im Live-Betrieb. Die Kombination aus Application-Metriken für die schnelle Eingrenzung und Massif für die tiefe Ursachenanalyse deckt fast jedes Speicherleck ab, das in der Praxis auftritt.


# Trace memory-related syscalls of a running process for 30 seconds
timeout 30 strace -tt -e trace=memory -p 4823 2>&1 | tee /tmp/memtrace.log

# Count how many brk() calls actually grew the heap
grep -c '^brk(' /tmp/memtrace.log

# Reproduce a suspected leak under Valgrind's massif profiler (staging only)
valgrind --tool=massif --massif-out-file=massif.out \
  php-fpm -F -y /etc/php/8.4/fpm/php-fpm.conf

# Turn the raw profile into a human-readable call-graph report
ms_print massif.out | less

8. cgroup-v2-Limits als Sicherheitsnetz einsetzen

Solange die eigentliche Ursache eines Speicherlecks nicht behoben ist, verhindert ein hartes cgroup-v2-Speicherlimit, dass ein einzelner leckender Dienst den gesamten Server destabilisiert. Über systemd lässt sich MemoryMax direkt für einen Service setzen, wodurch der Kernel den Cgroup-OOM-Killer statt des globalen OOM-Killers aktiviert, sobald der Dienst sein Limit überschreitet. Der entscheidende Vorteil: Nur der betroffene Dienst wird beendet, nicht willkürlich ein anderer Prozess wie die Datenbank, was die Vorhersagbarkeit von Ausfällen erheblich verbessert.

Zusätzlich liefert systemd-cgtop eine Live-Übersicht über den Speicherverbrauch aller Cgroups und macht sofort sichtbar, welcher Dienst sich seinem Limit nähert, lange bevor ein Kill nötig wird. Diese Limits ersetzen keine Ursachenanalyse, sie sind aber die pragmatischste Sofortmaßnahme, um ein bekanntes Speicherleck in der Produktion beherrschbar zu machen, während die eigentliche Behebung im Code oder in der Konfiguration parallel läuft.


# /etc/systemd/system/php8.4-fpm.service.d/memory-limit.conf
[Service]
# Hard cap — cgroup OOM killer targets only this service, not the whole host
MemoryMax=1536M
# Soft warning threshold logged to the journal before the hard limit hits
MemoryHigh=1280M

sudo systemctl daemon-reload
sudo systemctl restart php8.4-fpm

# Live view of memory usage per cgroup, refreshed every 2 seconds
systemd-cgtop -m

# Check whether the cgroup OOM killer already intervened
journalctl -u php8.4-fpm --since "1 hour ago" | grep -i "killed process\|oom"

9. Diagnosewerkzeuge für Speicherlecks im Vergleich

Die vorgestellten Werkzeuge decken unterschiedliche Phasen der Untersuchung eines Speicherlecks ab, von der ersten Erkennung bis zur exakten Ursachenanalyse. Die folgende Übersicht ordnet sie nach Einsatzzweck und Aufwand ein.

Werkzeug Einsatzzweck Overhead Live-Betrieb geeignet
smaps_rollup Schnelle Erkennung, Private-Dirty-Trend Minimal Ja, jederzeit
smem USS/PSS über mehrere Worker vergleichen Gering Ja, jederzeit
pmap -x Heap- vs. Mapping-Wachstum unterscheiden Gering Ja, jederzeit
strace -e memory Zeitpunkt und Größe von Allokationen Mittel Kurzzeitig ja
valgrind massif Exakte Call-Chain der Allokation Sehr hoch Nein, nur Staging

Der pragmatische Weg beginnt oben in der Tabelle: smaps_rollup und smem für die erste Bestätigung, pmap zur groben Lokalisierung, strace für die zeitliche Zuordnung zu Requests und erst zuletzt valgrind massif in einer Staging-Umgebung, wenn alle anderen Mittel keine eindeutige Ursache liefern. Diese Reihenfolge spart Zeit, weil die meisten Speicherlecks in der Praxis bereits mit den ersten drei Werkzeugen lokalisiert werden können.

Mironsoft

Linux-Troubleshooting und Server-Diagnose für Magento- und PHP-Infrastruktur

Wachsender Speicherverbrauch ohne erkennbare Ursache?

Wir analysieren eure PHP-FPM-Worker und Hintergrunddienste, finden das eigentliche Speicherleck statt nur den Server neu zu starten und richten Monitoring ein, das Wachstum erkennt, bevor der OOM-Killer eingreift.

Speicher-Audit

Systematische Analyse von RSS, PSS und Private-Dirty über alle produktiven Worker

Ursachenanalyse

strace- und Massif-gestützte Eingrenzung bis auf die konkrete Codestelle

Absicherung

cgroup-Limits und Worker-Recycling als Sicherheitsnetz gegen zukünftige Lecks

10. Zusammenfassung

Ein Speicherleck unter Linux erkennt man nicht an einer einzelnen hohen Zahl, sondern an einer über Stunden oder Tage monoton wachsenden Kurve, die auch nach einem Lastabfall nicht zurückgeht. Die systematische Untersuchung beginnt mit /proc/[pid]/smaps_rollup für die erste Bestätigung, geht über smem und pmap zur Lokalisierung zwischen Heap und Shared-Mappings weiter und nutzt strace sowie in hartnäckigen Fällen valgrind massif für die exakte Ursache. Bei PHP-FPM-Workern ist pm.max_requests eine wirksame, aber symptomatische Sofortmaßnahme.

Parallel zur eigentlichen Ursachenforschung sorgt ein cgroup-v2-Speicherlimit dafür, dass ein bekanntes, noch nicht behobenes Speicherleck nicht den gesamten Server destabilisiert, sondern kontrolliert nur den betroffenen Dienst betrifft. Wer Zeitreihen protokolliert, Deployments korreliert und die Werkzeuge in der richtigen Reihenfolge einsetzt, findet die Ursache eines Speicherlecks in den meisten Fällen innerhalb weniger Stunden statt über Wochen mit wiederholten Neustarts zu leben.

Speicherlecks in Prozessen: Das Wichtigste auf einen Blick

Erkennungsmerkmal

Monoton wachsender Private_Dirty-Wert über Stunden, der auch nach Lastabfall nicht zurückgeht.

Erste Werkzeuge

smaps_rollup, smem -tk und pmap -x liefern ohne Zusatzsoftware die erste Bestätigung.

PHP-FPM-Sofortmaßnahme

pm.max_requests recycelt Worker regelmäßig und begrenzt den Schaden, behebt aber nicht die Ursache.

Sicherheitsnetz

cgroup-v2 MemoryMax verhindert, dass ein Leck den gesamten Server destabilisiert.

11. FAQ: Speicherlecks in Prozessen systematisch aufspüren

1Echtes Leck vs. normales Wachstum?
Normales Wachstum erreicht ein Plateau. Ein echtes Leck wächst darüber hinaus weiter und geht nach Lastabfall nicht zurück.
2Reicht RSS aus top für die Diagnose?
Nein, RSS zählt geteilten Speicher mehrfach. PSS aus smem teilt ihn fair auf und ist die verlässlichere Kennzahl.
3Was zeigt Private_Dirty?
Speicher, der nur dem Prozess gehört und verändert wurde. Kontinuierliches Wachstum ist das stärkste Leck-Indiz.
4Löst pm.max_requests das Problem?
Nein, es begrenzt nur den Schaden durch Worker-Recycling. Die Ursache im Code bleibt bestehen.
5Wann valgrind massif statt strace?
Massif zeigt die genaue Aufrufkette, verlangsamt aber stark. Nur in Staging mit reproduziertem Leck einsetzen.
6Verhindert ein cgroup-Limit das Leck?
Nein, es begrenzt nur den Blast-Radius. Der Cgroup-OOM-Killer trifft gezielt nur den betroffenen Dienst.
7Warum trifft OOM den falschen Prozess?
oom_score bezieht Laufzeit und Priorität ein, nicht nur Speicher. /proc/[pid]/oom_score zeigt den echten Kandidaten.
8Wie protokolliere ich ohne Zusatzsoftware?
Ein Bash-Skript liest periodisch smaps_rollup aus und schreibt eine CSV-Zeitreihe, ganz ohne smem oder andere Tools.
9Warum mit Deployments korrelieren?
Wachstum nach jedem Deployment deutet auf neuen Code als Ursache hin, gleichmäßiges Wachstum eher auf einen Cache ohne Eviction.
10Ist tägliches Cron-Restart akzeptabel?
Nur kurzfristig. Als Dauerzustand verschleiert es das Problem, pm.max_requests plus Ursachenanalyse ist nachhaltiger.