oom_score, dmesg-Analyse und gezielte cgroup-Limits
Wenn der Linux-Kernel unter Speicherdruck gerät, entscheidet der OOM-Killer in Sekundenbruchteilen, welcher Prozess sterben muss, oft die Datenbank oder der PHP-FPM-Pool statt des eigentlichen Speicherfressers. Dieser Artikel erklärt die oom_score-Berechnung, zeigt wie man OOM-Kills in dmesg und journalctl aufspürt und wie man kritische Dienste durch oom_score_adj und cgroup-Limits gezielt schützt.
Inhaltsverzeichnis
- 1. Was der OOM-Killer wirklich tut
- 2. Wie der Kernel Speicherdruck erkennt
- 3. Die Opferauswahl: oom_score und Badness-Berechnung
- 4. OOM-Kills in dmesg und journalctl aufspüren
- 5. oom_score_adj gezielt für kritische Dienste setzen
- 6. Systemd, cgroup v2 und systemd-oomd
- 7. Docker und Kubernetes: Speicherlimits richtig setzen
- 8. Prävention: Swap, PSI-Monitoring und Alerting
- 9. OOM-Killer-Konfiguration im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was der OOM-Killer wirklich tut
Der OOM-Killer (Out-Of-Memory Killer) ist ein Kernel-Mechanismus, der einen Prozess gewaltsam beendet, wenn der Kernel eine Speicheranforderung nicht mehr erfüllen kann, obwohl er zuvor versucht hat, Speicher freizugeben. Er ist kein Bug und keine Fehlfunktion, sondern eine bewusste Design-Entscheidung: Ein System, das komplett einfriert, weil es keinen Speicher mehr allozieren kann, ist schlimmer als ein System, das gezielt einen einzelnen Prozess opfert, um weiterzulaufen. Ohne diesen Mechanismus würde ein Kernel-Panic drohen, sobald der letzte freie Speicherblock verbraucht ist.
Wichtig ist die Unterscheidung zum Swapping: Solange noch Swap-Speicher verfügbar ist und der Kernel Seiten auf die Platte auslagern kann, greift der OOM-Killer nicht ein, auch wenn das System dabei spürbar langsamer wird. Erst wenn Reclaim-Versuche, Swap-Auslagerung und Kompaktierung des Speichers gemeinsam nicht ausreichen, um die angeforderte Speicherseite bereitzustellen, wird der OOM-Killer im Kernel-Kontext direkt aus dem Page-Allocator heraus aufgerufen. Das Ergebnis ist fast immer ein SIGKILL, der Prozess bekommt keine Chance auf eine saubere Beendigung.
2. Wie der Kernel Speicherdruck erkennt
Linux erlaubt standardmäßig Memory Overcommit: Prozesse dürfen mehr virtuellen Speicher reservieren, als physisch vorhanden ist, weil viele Allokationen nie vollständig beschrieben werden. Gesteuert wird das über /proc/sys/vm/overcommit_memory mit drei Modi: 0 nutzt eine heuristische Schätzung, 1 erlaubt praktisch unbegrenztes Overcommit, und 2 erzwingt strikte Buchführung anhand von vm.overcommit_ratio plus Swap-Größe. Erst wenn Prozesse den reservierten Speicher tatsächlich beschreiben, wird er physisch belegt, und genau an dieser Stelle kann der Speicherdruck entstehen.
Bevor der OOM-Killer eingreift, versucht der Kernel über kswapd im Hintergrund und über Direct Reclaim im Allokationspfad selbst, Speicher freizugeben: ungenutzte Page-Cache-Seiten verwerfen, anonyme Seiten in den Swap schreiben, Speicher defragmentieren. Bei cgroup v2 gibt es zusätzlich einen eigenen Reclaim- und OOM-Mechanismus pro Cgroup: Erreicht ein Dienst sein memory.max-Limit, löst das einen OOM-Kill innerhalb dieser Cgroup aus, unabhängig davon, wie viel freier Speicher auf dem restlichen System noch verfügbar ist. Diese Trennung ist der Schlüssel zu kontrolliertem Speicherverhalten in containerisierten Umgebungen.
3. Die Opferauswahl: oom_score und Badness-Berechnung
Wenn der OOM-Killer aktiv wird, berechnet der Kernel für jeden infrage kommenden Prozess einen sogenannten Badness-Score, sichtbar unter /proc/PID/oom_score. Die Basis dieser Berechnung ist grob der Anteil des tatsächlich genutzten Speichers (RSS plus Swap-Nutzung) am insgesamt verfügbaren Speicher, ausgedrückt in Promille. Ein Prozess, der vierzig Prozent des Systemspeichers belegt, bekommt tendenziell einen deutlich höheren Score als ein kleiner Hintergrunddienst, selbst wenn beide gleich lange laufen. Der Prozess mit dem höchsten Score wird getötet, nicht zwingend der Prozess, der die aktuelle Allokation ausgelöst hat.
Beeinflussbar ist dieser Score über /proc/PID/oom_score_adj, einen Wert zwischen -1000 und 1000. Der Wert -1000 schließt einen Prozess vollständig von der OOM-Auswahl aus, das nutzt der Kernel selbst für init/PID 1 und typischerweise für systemd, damit das System nach einem OOM-Kill überhaupt handlungsfähig bleibt. Positive Werte erhöhen die Wahrscheinlichkeit, negative Werte senken sie. Das ältere oom_adj-Interface (Bereich -17 bis 15) ist als deprecated markiert, sollte in neuen Skripten nicht mehr verwendet werden und existiert nur noch aus Kompatibilitätsgründen.
4. OOM-Kills in dmesg und journalctl aufspüren
Das typische Symptom eines OOM-Kills: Ein Dienst stirbt scheinbar grundlos, ohne Fehlermeldung in seinem eigenen Log, ohne Core-Dump, oft mit Exit-Code 137 (128 plus Signal 9 für SIGKILL). Die erste Anlaufstelle ist immer der Kernel-Ringpuffer: dmesg -T | grep -i "killed process" zeigt die Kill-Zeile mit Zeitstempel, Prozessname und PID. Läuft das System mit systemd-journald, liefert journalctl -k --since "-1h" dieselben Kernel-Meldungen persistent über Neustarts hinweg, was dmesg allein nicht kann, da dessen Puffer beim Reboot verloren geht.
Eine vollständige OOM-Kill-Meldung enthält deutlich mehr als nur die Kill-Zeile: Sie beginnt mit invoked oom-killer, listet danach eine Tabelle aller Kandidaten-Prozesse mit ihrem jeweiligen oom_score_adj und ihrer Speichernutzung, und endet mit der eigentlichen Killed process-Zeile inklusive total-vm, anon-rss und file-rss. Diese Tabelle ist entscheidend für die Diagnose, denn sie zeigt, warum gerade dieser Prozess und kein anderer ausgewählt wurde. Bei containerisierten Diensten ergänzt docker inspect <container> --format '{{.State.OOMKilled}}' die Kernel-Sicht um den Container-Kontext.
# Kernel-Ringpuffer nach OOM-Kills durchsuchen
dmesg -T | grep -i "killed process"
# [Sa Jul 11 03:14:22 2026] Killed process 18422 (mysqld) total-vm:4823112kB, anon-rss:3912456kB, file-rss:2048kB
# Persistente Suche über journald, auch nach einem Reboot
journalctl -k --since "-24h" | grep -iE "oom|killed process"
# Vollständigen OOM-Kontext eines Ereignisses ausgeben
journalctl -k -o short-precise | grep -A 40 "invoked oom-killer" | head -n 50
# Exit-Code eines systemd-Dienstes prüfen (137 = SIGKILL nach OOM)
systemctl status mysql.service | grep -i "code="
# OOM-Status eines Docker-Containers direkt abfragen
docker inspect webapp --format '{{.State.OOMKilled}} exit={{.State.ExitCode}}'
5. oom_score_adj gezielt für kritische Dienste setzen
Um zu verhindern, dass ausgerechnet die Datenbank oder der zentrale Cache-Dienst als Erstes stirbt, während ein fehlerhafter Batch-Job unkontrolliert Speicher allokiert, lässt sich oom_score_adj gezielt pro Dienst setzen. Für systemd-verwaltete Dienste geschieht das deklarativ über die Unit-Direktive OOMScoreAdjust= in der Service-Datei oder in einem Override via systemctl edit mysql.service. Für bereits laufende Prozesse bietet das util-linux-Paket das Kommando choom, mit dem sich der Wert zur Laufzeit ändern lässt, ohne den Prozess neu zu starten.
Wichtig ist dabei die Nebenwirkung: Der Gesamtspeicher des Systems ändert sich durch oom_score_adj nicht, es verschiebt nur, welcher Prozess im Ernstfall geopfert wird. Wird die Datenbank vollständig geschützt (-800 oder niedriger), trifft es im Zweifel den nächstschlechtesten Kandidaten, im schlimmsten Fall einen ebenfalls kritischen Dienst wie den Reverse-Proxy oder gar den SSH-Daemon. Sinnvoll ist eine abgestufte Priorisierung über mehrere Dienste hinweg, kombiniert mit echten Speicherlimits, statt einen einzelnen Prozess pauschal komplett zu immunisieren.
# Aktuellen Badness-Score und Adjustment eines laufenden Prozesses lesen
cat /proc/$(pgrep -f mysqld)/oom_score
cat /proc/$(pgrep -f mysqld)/oom_score_adj
# oom_score_adj zur Laufzeit setzen, ohne Neustart (choom aus util-linux)
choom -p "$(pgrep -f mysqld)" -n -500
# Gleiches Verhalten dauerhaft über systemd-Override konfigurieren
sudo systemctl edit mysql.service
# In den Override-Editor einfügen, siehe nächster Codeblock
sudo systemctl daemon-reload
sudo systemctl restart mysql.service
6. Systemd, cgroup v2 und systemd-oomd
Neben OOMScoreAdjust= bietet systemd über cgroup v2 direkten Zugriff auf harte Speicherlimits: MemoryMax= setzt das memory.max-Limit der Cgroup eines Dienstes, MemoryHigh= setzt eine weichere Drosselgrenze, ab der der Kernel den Dienst aktiv bremst, bevor das harte Limit erreicht wird. Wichtig ist OOMPolicy=: Der Standardwert stop beendet den Dienst kontrolliert, sobald einer seiner Prozesse vom Kernel-OOM-Killer getroffen wird, während continue den Dienst trotz eines internen Kills weiterlaufen lässt und kill die gesamte Cgroup sofort beendet.
Ergänzend dazu läuft auf modernen Distributionen häufig systemd-oomd, ein Userspace-Daemon, der nicht auf den harten Kernel-OOM-Killer wartet, sondern proaktiv anhand von Pressure Stall Information (PSI) aus /proc/pressure/memory eingreift. PSI misst, wie viel Zeit Prozesse blockiert auf Speicher warten, und systemd-oomd kann bereits bei anhaltendem Druck einen Kandidaten beenden, lange bevor der Kernel selbst in einen kritischen Zustand gerät. Konfiguriert wird das global über /etc/systemd/oomd.conf und pro Unit über ManagedOOMMemoryPressure= sowie ManagedOOMSwap=.
; /etc/systemd/system/mysql.service.d/override.conf
; OOM-Score für die Datenbank absenken und hartes Speicherlimit setzen
[Service]
OOMScoreAdjust=-500
MemoryMax=6G
MemoryHigh=5G
OOMPolicy=stop
; systemd-oomd für diesen Dienst deaktivieren, Entscheidung
; bleibt beim Kernel-OOM-Killer statt beim PSI-basierten Daemon
ManagedOOMMemoryPressure=auto
ManagedOOMSwap=auto
7. Docker und Kubernetes: Speicherlimits richtig setzen
Container erhalten ihr Speicherlimit ebenfalls über cgroups, bei Docker per --memory-Flag oder deploy.resources.limits.memory in einer Compose-Datei, bei Kubernetes über resources.limits.memory im Pod-Manifest. Erreicht ein Container dieses Limit, greift der OOM-Killer innerhalb der Container-Cgroup, tötet dort einen Prozess, meist PID 1 des Containers, und der Container beendet sich mit Exit-Code 137. Sichtbar wird das über docker inspect im Feld OOMKilled oder bei Kubernetes über kubectl describe pod mit dem Status OOMKilled im letzten Zustand.
Ein häufiger Fehler in Kubernetes-Manifesten: ein limit ohne passendes request, wodurch der Scheduler die tatsächliche Auslastung falsch einschätzt und Pods unter Last unerwartet evictet oder OOM-killt. Besser ist ein realistisches requests.memory nahe dem typischen Verbrauch und ein limits.memory mit angemessenem Puffer für Lastspitzen. memory.high-artiges Verhalten lässt sich in Kubernetes derzeit nur indirekt über QoS-Klassen und LimitRange-Objekte annähern, eine native weiche Drosselgrenze wie bei systemd fehlt auf Pod-Ebene bislang.
# docker-compose.yml: Speicherlimit für einen PHP-FPM-Dienst
services:
php-fpm:
image: php:8.4-fpm
deploy:
resources:
limits:
memory: 768M
reservations:
memory: 384M
# Bei Erreichen von 768M killt der cgroup-OOM-Killer
# einen Prozess innerhalb dieses Containers, Exit-Code 137
restart: unless-stopped
8. Prävention: Swap, PSI-Monitoring und Alerting
Die wirksamste Prävention ist realistische Kapazitätsplanung: ausreichend RAM für den erwarteten Spitzenlastfall, ein moderates Swap-Volumen als Sicherheitspuffer statt als primärer Speicher, und eine niedrige vm.swappiness auf Servern, wo Latenz wichtiger ist als maximale Cache-Ausnutzung. Komprimiertes Swap über zram reduziert dabei die I/O-Kosten des Auslagerns deutlich gegenüber klassischem Datei- oder Partitions-Swap und verschafft dem System bei kurzen Lastspitzen wertvolle zusätzliche Reaktionszeit, bevor der OOM-Killer überhaupt in Betracht gezogen wird.
Für proaktives Monitoring lohnt sich ein regelmäßiger Blick auf /proc/pressure/memory: Steigt der avg60-Wert der full-Zeile dauerhaft über niedrige einstellige Prozentwerte, kündigt sich Speicherdruck oft Minuten bis Stunden vor dem eigentlichen OOM-Kill an. Der node_exporter für Prometheus exportiert diese PSI-Metriken direkt, sodass sich Alerting-Regeln definieren lassen, die anschlagen, bevor der Kernel überhaupt einen Prozess tötet, statt erst hinterher aus den Logs zu rekonstruieren, was passiert ist.
{
"_TRANSPORT": "kernel",
"PRIORITY": "0",
"SYSLOG_IDENTIFIER": "kernel",
"MESSAGE": "Out of memory: Killed process 18422 (mysqld) total-vm:4823112kB, anon-rss:3912456kB, file-rss:2048kB, shmem-rss:0kB, UID:112 pgtables:8420kB oom_score_adj:0",
"__REALTIME_TIMESTAMP": "1783825462123456"
}
9. OOM-Killer-Konfiguration im Vergleich
Ob ein OOM-Ereignis kontrolliert und nachvollziehbar abläuft oder den falschen Dienst trifft, hängt fast immer von wenigen Konfigurationsentscheidungen ab. Die folgende Übersicht zeigt die häufigsten unsicheren Standardeinstellungen im Vergleich zur empfohlenen Konfiguration.
| Bereich | Unsicher / Standard | Empfohlene Konfiguration | Vorteil |
|---|---|---|---|
| Overcommit-Modus | overcommit_memory=1 unbegrenzt | overcommit_memory=2 + overcommit_ratio | Speicherzusagen bleiben nachvollziehbar |
| Kritische Dienste | kein oom_score_adj gesetzt | OOMScoreAdjust=-500 für DB/Cache | Datenbank stirbt nicht zuerst |
| Container-Limits | limits ohne requests | requests nahe realer Nutzung + limits mit Puffer | Weniger unerwartete Evictions |
| Swap-Konfiguration | kein Swap, kein Puffer | zram + moderate swappiness | Zeitpuffer bei kurzen Lastspitzen |
| Monitoring | nur reaktiv per dmesg nach dem Vorfall | PSI-Metriken + Alerting vor dem Kill | Vorfälle vor dem Kill erkennen |
Die meisten Vorfälle in der Praxis entstehen nicht durch eine einzelne falsche Einstellung, sondern durch das Fehlen jeder bewussten Konfiguration: Standard-Overcommit, kein Speicherlimit, kein oom_score_adj, kein Monitoring. Wer diese fünf Stellschrauben einmal je Server oder Cluster durchgeht, reduziert die Wahrscheinlichkeit eines überraschenden Produktionsausfalls durch den OOM-Killer erheblich, ohne zusätzliche Hardware anschaffen zu müssen.
Mironsoft
Server-Administration, Speicher-Tuning und Incident-Analyse für Linux-Infrastruktur
Unerklärliche Prozessabbrüche durch den OOM-Killer?
Wir analysieren eure OOM-Kill-Logs, identifizieren die tatsächliche Ursache des Speicherdrucks und konfigurieren oom_score_adj, cgroup-Limits und Monitoring so, dass kritische Dienste wie Datenbank und Cache geschützt bleiben.
Incident-Analyse
Auswertung von dmesg und journalctl, um die genaue OOM-Ursache zu rekonstruieren
Speicher-Tuning
oom_score_adj, cgroup v2 Limits und systemd-oomd für kritische Dienste konfigurieren
Proaktives Monitoring
PSI-basiertes Alerting einrichten, bevor der nächste OOM-Kill produktiv wird
10. Zusammenfassung
Der OOM-Killer ist kein Zufallsereignis, sondern folgt einer klaren, nachvollziehbaren Logik: Er greift erst ein, wenn Reclaim und Swap-Auslagerung nicht mehr ausreichen, und wählt den Prozess mit dem höchsten Badness-Score anhand seines Speicheranteils am Gesamtsystem. Diese Auswahl lässt sich über oom_score_adj gezielt beeinflussen, ohne den Gesamtspeicherverbrauch zu verändern. In dmesg und journalctl hinterlässt jeder OOM-Kill eine vollständige Diagnose-Tabelle, die zeigt, warum genau dieser Prozess getroffen wurde und wie viel Speicher die Konkurrenten zu diesem Zeitpunkt belegten.
Nachhaltig gelöst wird das Problem nicht durch das Beschützen eines einzelnen Prozesses, sondern durch eine Kombination aus realistischen cgroup-Limits pro Dienst, moderatem Swap als Zeitpuffer und PSI-basiertem Monitoring, das Speicherdruck erkennt, bevor der Kernel überhaupt einen Prozess töten muss. Wer diese Bausteine konsequent auf Produktionsservern etabliert, verwandelt den OOM-Killer von einer überraschenden Fehlerquelle in ein planbares, beherrschbares Sicherheitsnetz.
Den OOM-Killer verstehen und Speicherprobleme vermeiden, das Wichtigste auf einen Blick
Auslöser
Erst wenn Reclaim, Swap und Kompaktierung nicht ausreichen, wird der OOM-Killer aus dem Page-Allocator heraus aufgerufen.
Opferauswahl
Der Badness-Score in /proc/PID/oom_score basiert auf dem Speicheranteil, beeinflussbar über oom_score_adj.
Diagnose
dmesg -T | grep -i "killed process" und journalctl -k zeigen die vollständige Kill-Tabelle.
Prävention
cgroup-Limits, zram-Swap und PSI-Monitoring per systemd-oomd verhindern unkontrollierte Kills.