...

OOM Score und OOM Score Adjust im Hosting-Betrieb erklärt

Ich erkläre den OOM Score und OOM Score Adjust als konkrete Steuerhebel im Hosting-Betrieb: Sie legen fest, welche Prozesse der Linux-OOM-Killer bei Speichermangel beendet und welche er schützt. So behalte ich die Kontrolle, wenn RAM knapp wird und sorge dafür, dass essenzielle Dienste online bleiben.

Zentrale Punkte

Zur schnellen Orientierung fasse ich die wichtigsten Gedanken knapp zusammen.

  • Priorität bei Knappheit: OOM Score bewertet, welcher Prozess zuerst gehen muss.
  • Feinsteuerung mit oom_score_adj: Von -1000 (schützen) bis +1000 (opfern).
  • Dynamik statt Fixwert: Bewertung ändert sich mit Last und Konfiguration.
  • Hosting-Praxis: Kritische Dienste schützen, unkritische Worker eher beenden.
  • Ursachen beheben: Limits, Cgroups und RAM-Planung prüfen.

Wie der Linux OOM-Killer arbeitet

Bei hohem Speicherdruck entscheidet der Linux-Kernel, welche Prozesse er beendet, um das System reaktionsfähig zu halten. Ich beobachte dabei, wie der Kernel jedem Prozess eine Art „Badness“ zuweist, die stark vom aktuellen Speicherverbrauch abhängt. Wird keine ausreichende Menge an RAM oder Swap frei, greift der OOM-Killer ein und beendet das Opfer mit der höchsten Bewertung. Dieser Mechanismus verhindert Stillstand, ersetzt aber keine saubere Kapazitätsplanung auf Host- und Diensteebene. Ich lese die Entscheidung im OOM-Log und erkenne, ob ein Dienst durch Speicherhunger oder Fehlkonfiguration auffällig wurde.

OOM Score verstehen: Dynamik und Skala

Ich prüfe den OOM Score eines Prozesses in /proc/PID/oom_score und lese so ab, wie gefährdet er aktuell ist. Die Skala reicht praktisch von 0 bis 1000: Je näher an 1000, desto eher fällt der Prozess dem Killer zum Opfer. Dieser Wert bildet eine Momentaufnahme, weil Lastspitzen, Cgroup-Limits und Cache-Größen sich laufend ändern. Deshalb werte ich den Score niemals isoliert, sondern im Kontext von Arbeitsspeicher, Swap, Overcommit und parallelen Prozessen. Wer den Score regelmäßig betrachtet, erkennt typische Muster und kann Engpässe vorhersehen, bevor sie Dienste aus dem Rennen werfen.

OOM Score Adjust gezielt einsetzen

Mit oom_score_adj verschiebe ich die Bewertung eines Prozesses aktiv von -1000 bis +1000. Setze ich -1000, schütze ich den Prozess vollständig, während hohe positive Werte ihn bewusst opferbereit machen. Ich wähle sparsam, denn zu viele geschützte Prozesse nehmen dem OOM-Killer Handlungsspielraum. Typische Kandidaten für niedrige Werte sind SSH, Monitoring, Reverse-Proxy-Frontends und sensible Datenbank-Controller. Hintergrund-Jobs, Reporter oder kurzlebige Worker erhalten eher eine höhere Justierung, damit die Benutzerfläche weiter reagiert, wenn es eng wird.

Prioritäten im Hosting setzen

In produktiven Setups definiere ich klare Prioritäten zwischen Frontend, API, Datenbank und Batch-Verarbeitung. Ich lege zuerst fest, welche Dienste aus Benutzersicht leben müssen, und gebe ihnen eine günstige OOM-Justierung. In systemd setze ich dafür OOMScoreAdjust= im Service-Unit-File und dokumentiere den Zweck jedes Wertes. Wer Dienste ohnehin über systemd verwaltet, kann Abläufe straffen; ein Einstieg dazu bietet systemd im Hosting. So steuere ich Ausfälle vor, statt sie dem Zufall zu überlassen, und halte die Nutzerführung verlässlich online.

Cgroups, Container und Limits

Ich vergesse niemals die cgroups, denn Container und Dienste leben in eigenen Ressourcenwelten. Ein Prozess mit moderatem OOM Score kann trotzdem sterben, wenn seine Cgroup ein enges Memory-Limit hat und er es kurzfristig reißt. Deshalb prüfe ich Limits in cgroup v2 und orchestriere harte und weiche Grenzen passend zu Lastprofilen. Wer Multi-Tenancy oder Shared-Hosting betreibt, profitiert von sauber gesetzten Quoten und Accounting; mehr Hintergründe liefert cgroup v2 im Hosting. Stimmt das Zusammenspiel, wirken OOM-Justierung und Limits wie ein gut abgestimmtes Paar an Stellschrauben.

Diagnose und Monitoring bei OOM-Events

Wenn es knallt, brauche ich klare Signale und Wiederholbarkeit in der Analyse. Ich werte dmesg, journald und /var/log/kern.log aus, sichere die OOM-Zeilen und greife die PID des Opfers samt oom_score und oom_score_adj ab. Für Routinechecks nutze ich Skripte, die die größten Speicherverbraucher listen und Warnschwellen auslösen. Wer tiefer einsteigen möchte, findet eine strukturierte Herangehensweise in der OOM-Killer Analyse. In dauerhaften Setups binde ich Metriken wie RSS, Cache, Swap-In/Out und Container-Limits ins Monitoring ein, damit ich Trends rechtzeitig erkenne.

Tabellarischer Spickzettel für Admins

Die folgende Übersicht nutze ich als kompakten Leitfaden, wenn ich Rollen priorisiere und Justierungen dokumentiere. Die Spalte „Begründung“ zeigt, warum ein Dienst Schutz oder Opferbereitschaft erhält. Ich passe die Zahlen an das Projekt an, doch die Richtung hilft bei schnellen Entscheidungen. Wer die Tabelle als Ausgangspunkt nutzt, gewinnt Klarheit in Post-Mortems und in Änderungsanträgen. Wichtig bleibt: Ich halte immer einen Puffer im Gesamtsystem, damit harte Kills selten nötig sind.

Komponente Typisches Ziel Beispiel oom_score_adj Begründung
SSH-Daemon Schützen -500 bis -900 Zugang für Eingriffe sichern, selbst bei Engpässen.
Reverse Proxy (nginx/HAProxy) Schützen -300 bis -700 Eingangs-Traffic bedienen, Fehlerseiten liefern.
DB-Controller/Primärinstanz Schützen -200 bis -600 Verbindungen halten, Datenzugriff sichern.
PHP-FPM/Anwendungs-Worker Neutral bis opferbereit 0 bis +300 Viele parallele Worker können weichen.
Batch/Backup/Reports Opferbereit +300 bis +800 Verschiebbar ohne Benutzer-Impact.
Indexer/Queue-Verbraucher Opferbereit +200 bis +600 Kurz pausierbar, später nachholbar.

WordPress- und PHP-Worker richtig begrenzen

Bei WordPress achte ich auf Worker-Anzahl, memory_limit und große Operationen wie Bildverarbeitung oder Importe. Ich setze PHP-FPM so, dass die Zahl aktiver Prozesse zum RAM passt und nicht Lawinen erzeugt. In der Datenbank zähle ich Puffer und Cache-Größen zusammen und lasse noch Headroom, damit Spikes nicht alles blockieren. Ich beobachte OpCache, Objekt-Cache und Bildoptimierer, weil sie den Speicherverbrauch schnell hochtreiben. So sorge ich dafür, dass kurze Lastspitzen nicht gleich die wichtigen Frontend-Prozesse kosten.

Praxis: Policies und Playbooks

Ich halte meine Policies knapp und umsetzbar, damit das Team im Ernstfall nicht zögert. Dazu gehört: Schutzkandidaten definieren, Opferrollen benennen, systemd-Units mit OOMScoreAdjust= versehen und die Werte im Repo dokumentieren. Ich prüfe die Wirkung mit Tools und Testlast, bis die Reihenfolge der Opfer zu den Zielen passt. Danach schreibe ich ein Playbook, das Logs, Alarmierung und Erstmaßnahmen beschreibt. So bleibt die Reaktion konsistent, selbst wenn neue Kolleginnen und Kollegen übernehmen.

# Beispielfragment für systemd-Unit
[Service]
OOMScoreAdjust=-400
# Reload und Restart:
# systemctl daemon-reload && systemctl restart nginx

# Laufend prüfen:
cat /proc/$(pidof nginx)/oom_score
cat /proc/$(pidof nginx)/oom_score_adj

# Temporär anheben/absenken (root):
echo 300 | sudo tee /proc/<PID>/oom_score_adj

Häufige Fehler und Gegenmaßnahmen

Viele Probleme entstehen, weil Limits nicht zusammenpassen: zu viele PHP-Worker, zu große DB-Caches und kein Raum für Spike-Last. Dann greift der OOM-Killer regelmäßig ein, obwohl wenige Stellschrauben reichen würden. Ich korrigiere die Worker-Zahl zuerst, messe den Effekt und erhöhe nur dann den RAM, wenn sich der Bedarf klar zeigt. Auch das Setzen vieler Prozesse auf -1000 schadet, denn der Kernel braucht Handlungsfreiheit. Ich priorisiere mit Augenmaß, damit das System im Notfall geordnet reagieren kann.

Overcommit, Swap und Speicher-Wasserstände

Ich stelle meine Overcommit-Strategie bewusst ein, denn sie entscheidet, wie schnell ein System in die OOM-Zone gerät. Mit vm.overcommit_memory=0 (Heuristik) arbeite ich oft stabil, weil der Kernel die Commit-Grenze anhand Nutzung und Historie einschätzt. Strenger wird es mit vm.overcommit_memory=2 plus vm.overcommit_ratio, das die maximal zulässige virtuelle Belegung definiert. Wer pauschal vm.overcommit_memory=1 setzt, riskiert, dass Speicherreservierungen gelingen und später beim Allozieren hart scheitern – ein häufiger Nährboden für OOM-Ereignisse unter Last.

Ich kalibriere Swap so, dass er Puffer liefert, aber nicht zum Latency-Killer wird. Eine moderate vm.swappiness hält reinen Arbeitsspeicher für heiße Pfade frei, während selten genutzte Seiten ausweichen. Zswap oder zram kann ich als elastisches Polster nutzen, wenn I/O langsam ist – das senkt OOM-Risiko, kostet aber CPU. Wichtig sind auch Wasserstände: vm.min_free_kbytes muss ausreichend hoch sein, damit der Kernel rechtzeitig reclaimen kann. Wer zu knapp bemisst, drückt das System in hektischen Reclaim und provoziert Pfadologiken, die in OOM münden.

# Beispiel: konservativer Overcommit und moderates Swapping
sysctl -w vm.overcommit_memory=2
sysctl -w vm.overcommit_ratio=90
sysctl -w vm.swappiness=30
# Für Tests in /etc/sysctl.d/ persistent eintragen

Systemd-Optionen jenseits von OOMScoreAdjust

Neben OOMScoreAdjust nutze ich systemd, um Speicherleitplanken direkt am Dienst zu setzen. Mit MemoryMax= begrenze ich hart (cgroup memory.max), MemoryHigh= bremst sanft unter Last und MemorySwapMax= zügelt Auslagerungen. MemoryLow= und MemoryMin= priorisieren bei Druck Cache-Anteile eines Dienstes, sodass wichtige Prozesse weniger schnell auskühlen. Zusammen mit OOMPolicy= steuere ich, was systemd bei OOM auf Unit-Ebene unternimmt (z. B. nur Dienst stoppen oder ganze Abhängigkeiten terminieren). In Slices aggregiere ich Rollen – Web-Frontend, Batch, DB – und leite einheitliche Regeln ab, damit einzelne Ausreißer nicht das Ganze destabilisieren.

Ich beachte, dass Schutz nie absolut ist: Selbst Prozesse mit -1000 können in aussichtslosen Situationen weichen müssen. Deshalb setze ich großzügige, aber realistische Minima (MemoryLow/Min) nur für sehr wenige Kerndienste und überprüfe, ob die Summe aller Zusagen unterhalb des physisch verfügbaren Speichers bleibt. So verhindere ich, dass gut gemeinte Schutzschilde den OOM-Killer blind machen.

Kubernetes und Container-Orchestrierung

In Orchestrierungen wie Kubernetes spielt OOM-Logik in mehreren Ebenen. Ich setze Requests und Limits so, dass Pods in die gewünschte QoS-Klasse fallen: Guaranteed schützt am stärksten, Burstable federt ab, BestEffort ist am ehesten Opfer. Der kubelet vergibt daraus resultierende OOMScoreAdjust-Werte automatisch – ich plane also über Ressourcenangaben statt über manuelle Tuningwerte in den Containern. Trifft ein Container sein memory.limit, stirbt er innerhalb seiner cgroup auch dann, wenn der Host noch frei hat; das ist kein klassischer Host-OOM, sondern eine gezielte Selbstverteidigung des Limits.

Ich berücksichtige native Speicheranteile außerhalb der Heap-Konfigurationen (z. B. bei JVM/Node), damit Container nicht überraschend am Limit zerschellen. Zudem kalkuliere ich Pod-Puffer gegen Spitzen und plane Node-Overcommit nur in Maßen, damit Evictions selten werden. Wenn cgroup v2 aktiv ist, verwende ich memory.oom.group gezielt, damit im Notfall eine ganze Prozessgruppe geordnet fällt, statt einzelne Worker in einem Zombie-Pod zu belassen. Das hält das System sauber und die Recovery kalkulierbar.

Diagnosetiefe: SMaps, PSI und reproduzierbare Tests

Für tiefe Analysen greife ich zu /proc-Einblicken und Druckmetriken. /proc/PID/status zeigt VmRSS, VmSwap und Threads; /proc/PID/smaps_rollup fasst Anteile wie Anon, File, Shmem zusammen, ohne mich im Detail zu verlieren. So erkenne ich, ob Page Cache trügt oder anonyme Seiten (echter Arbeitsdatensatz) wachsen. Mit /proc/pressure/memory messe ich PSI-Signale, also wie viel Zeit das System unter aktivem Reclaim oder Stalls leidet. Diese Werte alarmiere ich, lange bevor OOM zuschlägt – ideal, um automatisch Gegenmaßnahmen (Throttling, Skalierung, Worker-Reduktion) auszulösen.

# Relevante Schnappschüsse
journalctl -k -g "Out of memory|oom-killer"
cat /proc/pressure/memory
grep -E "VmRSS|VmSwap|Threads" /proc/<PID>/status
cat /proc/<PID>/smaps_rollup

# OOM reproduzieren (Testumgebung!)
stress-ng --vm 2 --vm-bytes 80% --timeout 30s

Spezialfälle: JVM, Node.js und PHP in Containern

JVM-Dienste benötigen Aufmerksamkeit, weil neben dem Heap auch Metaspace, Thread-Stacks, Direct Buffers und natives Allocator-Verhalten zählen. Ich steuere Container-freundlich mit MaxRAMPercentage und setze einen Heap, der Headroom für diese Anteile lässt. Bei hoher Parallelität begrenze ich Thread-Pools, denn viele kleine Stacks addieren sich schmerzhaft. Für Node.js passe ich –max-old-space-size an das Container-Limit an, um harte Kills zu verhindern. Und bei PHP-FPM kalkuliere ich pm.max_children aus RAM, durchschnittlichem pro-Request-Verbrauch und memory_limit – plus Reserve für Caches und Webserver. So verhindere ich schleichende Lawinen, die erst bei Spitzen sichtbar werden.

Ich behalte die Allocator-Strategie im Blick: glibc mit vielen Arenen kann in Workloads mit vielen Threads Speicher fragmentieren und hochtreiben. Für bestimmte Dienste liefert jemalloc oder tcmalloc konsistentere Peaks; ich teste das gezielt, dokumentiere den Effekt und rolle es kontrolliert aus. Außerdem limitiere ich tmpfs-Verzeichnisse im Container, damit Uploads oder Tempfiles nicht unbemerkt den RAM auffressen.

Tmpfs, Huge Pages und Seitencache

tmpfs wird gern übersehen: Ohne Größenlimit wächst es bis zu einem Anteil des RAM, und plötzlich fehlt Platz an anderer Stelle. Ich mounte tmpfs mit bewusster size= Angabe, gerade bei Build- oder Upload-Pfaden. Transparent Huge Pages (THP) beeinflussen Fragmentierung und Latenz; für latenzkritische Services setze ich häufig „madvise“, damit nur passende Allokationen profitieren. KSM kann deduplizieren und Speicher sparen, kostet jedoch CPU – auf Entwicklungs-Hosts nützlich, in Performance-Pfaden prüfe ich Wirkung und overhead.

Der Seitencache ist nicht „vergeudeter“ Speicher; er beschleunigt I/O. Wenn ich ihn zu aggressiv verdränge oder Drop-Caches als Dauermaßnahme nutze, verlagere ich die Kosten in Latenz-Spitzen. Besser ist es, Speicherziele pro Rolle zu definieren und über cgroup-Mechanismen (memory.high / memory.max) ein faires Verhältnis zu erzwingen. So bleiben Hotsets der wichtigen Dienste im RAM und OOM-Situationen werden seltener.

Zusammenfassung für den Alltag

Ich nutze den OOM Score als Barometer für Gefahr und justiere mit oom_score_adj die richtige Reihenfolge der Opfer. Dienste mit Nutzerwirkung schütze ich, verschiebbare Jobs mache ich opferbereit, und ich dokumentiere jeden Wert nachvollziehbar. Cgroup-Limits, Worker-Anzahl und Cache-Größen plane ich als Einheit, damit Spikes nicht zum Flächenbrand führen. Logs, Monitoring und ein kurzes Playbook sorgen dafür, dass ich OOM-Ereignisse schnell erkenne und gezielt behebe. Mit dieser Disziplin bleibt der Host verlässlich, und ich vermeide harte Überraschungen in produktiven Nächten.

Aktuelle Artikel