Mit systemd resource steuere ich gezielt CPU, RAM, I/O und PIDs für Linux-Dienste und halte so produktive Services planbar. Die folgenden Schritte zeigen praxisnah, wie ich Limits in Units und Slices setze, auf cgroups v2 aufbaue und Ressourcenkonflikte mit klaren Regeln entschärfe; so bleibt jede Instanz berechenbar.
Zentrale Punkte
Die folgende Übersicht bündelt die wichtigsten Aussagen, die ich im Artikel ausführlich erkläre; sie dient als schneller Leitfaden.
- cgroups v2 als einheitliche Hierarchie mit systemd als zentralem Manager
- Unit-Typen Service, Scope und Slice gezielt kombinieren
- CPUQuota und CPUWeight für faire CPU-Verteilung
- MemoryMax und MemoryHigh gegen OOM und Drosselung
- Slices für Gruppenlimits und Prioritäten im Serverbetrieb
Warum systemd und cgroups v2 zusammenarbeiten
Ich ordne alle Prozesse in cgroups v2 und nutze systemd als Steuerzentrale. Die einheitliche Hierarchie unter /sys/fs/cgroup fasst Controller wie cpu, memory, io und pids sauber zusammen. Jede Unit erhält ihre eigene cgroup, wodurch ich Limits konsistent auf eine ganze Dienstfamilie anwende. Diese Struktur verhindert, dass einzelne PIDs Limits umgehen, weil die Gruppe als Ganzes zählt. Seit systemd Version 232 verwaltet systemd die Hierarchie exklusiv und schreibt Limits in die Kernel-Interfaces; Delegation erlaube ich nur bewusst, damit nichts an den Kontrollen vorbeiläuft. So halte ich meine Ressourcen jederzeit beherrschbar.
Unit-Typen verstehen: Service, Scope, Slice
Ich kapsle klassische Daemons in Service-Units und fasse fremd gestartete Prozesse in Scopes zusammen. Für die Hierarchie lege ich Slices an, die als innere Knoten Ressourcen für ganze Gruppen definieren. Services und Scopes bilden die Blätter, die Limits vom jeweiligen Slice erben. So verteile ich CPU-, Speicher- und I/O-Budgets entlang des Baums, statt jeden Dienst isoliert zu betrachten. Für Einsteiger eignet sich ein Blick auf Hosting-Dienste effizient verwalten, um die Rolle von Units im Serverbetrieb zu verstehen und eigene Slices zu planen.
Voraussetzungen prüfen: Einheitliche Hierarchie und Controller
Ich stelle sicher, dass das System im unified mode läuft und alle benötigten Controller aktiv sind. Das erkenne ich an /sys/fs/cgroup (ein Mountpunkt) und daran, dass systemd die Baumstruktur verwaltet. Fehlen Controller (z. B. io), prüfe ich Kernel-Config und ggf. Boot-Parameter. Besonders in älteren Umgebungen migriere ich bewusst von v1 auf v2, damit die beschriebenen Direktiven wie IOWeight, MemoryHigh oder AllowedCPUs wirken. Erst wenn Accounting und Controller greifen, lohnt sich die Feinarbeit an Gewichten und Quoten.
CPU-Kontrolle: CPUWeight und CPUQuota richtig einsetzen
Ich steuere CPU-Anteile über CPUQuota und relative Prioritäten über CPUWeight. Eine Quote von 50% begrenzt den Dienst auf halbe Kernzeit, während ein Weight von 200 gegenüber Diensten mit kleineren Gewichten bevorzugt. Damit reguliere ich dauerlastige Jobs, ohne interaktive Services auszubremsen. In der Praxis starte ich mit moderaten Quoten, beobachte die Latenzen und hebe das Weight wichtiger Dienste an. So verteile ich die Rechenzeit nach Wichtigkeit statt nach Zufallslast.
CPU-Affinität, AllowedCPUs und Quoten-Perioden
Wenn ich Kerne fest zuordnen möchte, nutze ich CPUAffinity oder die feinere cpuset-Steuerung über AllowedCPUs. So entkopple ich z. B. Batch-Workloads von Latenz-sensiblen Diensten auf separaten Cores. Für Bursts passe ich CPUQuotaPeriodSec an: Eine längere Periode erlaubt größere, kurzzeitige Ausschläge innerhalb derselben durchschnittlichen Quote, was bei spikigen Services die P99-Latenz verbessert.
[Service]
# Kernauswahl (sched_affinity) vs. cpuset (cgroup v2)
CPUAffinity=0 1 2 3
AllowedCPUs=0-3
# 150% Gesamtzeit bei 200ms Periode (mehr Burst-Spielraum)
CPUQuota=150%
CPUQuotaPeriodSec=200ms
# Relative Gewichtung im gleichen Slice
CPUWeight=200
Speicherlimits mit MemoryMax, MemoryHigh, MemoryLow
Ich setze ein hartes Limit mit MemoryMax, um OOM-Situationen durch Ausreißer zu vermeiden. Mit MemoryHigh drossele ich den Speicherzugriff bereits vor dem harten Anschlag, was die Gesamtstabilität erhöht. MemoryLow und MemoryMin geben Diensten Schutzräume, damit der Kernel zuerst andere Gruppen reclaimt. Diese Staffelung verhindert Kaskadeneffekte, wenn mehrere Services zeitgleich wachsen. Wer Hintergründe zum Controller sucht, findet bei Memory-Controller erklärt eine anschauliche Einführung in die zugehörigen Mechanismen.
Swap-Strategie und OOM-Verhalten bewusst festlegen
Ich definiere klar, ob und wie stark eine Unit Swap nutzen darf. Mit MemorySwapMax setze ich eine Obergrenze auf die kombinierte Nutzung aus RAM und Swap. Für Latenz-kritische Services begrenze ich Swap oft stark oder deaktiviere ihn, um Page-Outs zu vermeiden. Zusätzlich beeinflusse ich mit OOMScoreAdjust die Wahrscheinlichkeit, mit der der Kernel einzelne Prozesse beendet – und mit OOMPolicy bestimme ich, wie systemd auf einen OOM in der Unit reagiert (z. B. gesamte Unit stoppen oder weiterlaufen lassen).
[Service]
# Maximal 2G inkl. Swap; harte RAM-Grenze bleibt MemoryMax
MemoryMax=1.5G
MemorySwapMax=2G
# Priorisierung der OOM-Entscheidung (kleiner = geschützter)
OOMScoreAdjust=-500
# Reaktion, wenn der OOM-Killer innerhalb der Unit zuschlägt
OOMPolicy=stop
Mit dieser Kombination verhindere ich unkontrollierte Swaps, sorge für definierte Failover-Szenarien und halte Datenbanken oder In-Memory-Caches zuverlässig unter einem planbaren Dach.
I/O- und Prozesslimits: IOWeight, Bandbreiten und TasksMax
Ich begrenze Lese- und Schreibraten per IOReadBandwidthMax und IOWriteBandwidthMax, wenn Disks geteilt werden. Für relative Priorisierung nutze ich IOWeight, damit zentrale Workloads Vorrang vor Batch-Streams erhalten. Mit TasksMax ziehe ich eine klare Obergrenze für Prozesse und Threads, was Fork-Bomben effektiv stoppt. Diese Kontrollen stabilisieren Multiserver-Umgebungen, in denen einzelne Jobs sonst die gesamte I/O dominieren. Besonders bei Build-Servern sichere ich so reproduzierbare Durchsätze ab.
Per-Device-I/O gezielt steuern
In heterogenen Setups mit NVMe und HDDs reguliere ich pro Gerät. Das verhindert, dass schnelle SSDs von einem lauten Nachbarn auf der HDD ausgebremst werden. Die Kombination aus relativen Gewichten und absoluten Deckeln pro Device deckt die meisten Praxisfälle ab.
[Service]
# Relatives Gewicht für alle Geräte
IOWeight=300
# Pro-Device-Gewicht (z. B. NVMe priorisieren)
IODeviceWeight=/dev/nvme0n1 500
IODeviceWeight=/dev/sda 100
# Absoluter Deckel pro Gerät (Lese-/Schreibrate)
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 30M
Wichtig: IOWeight wirkt nur relativ zwischen aktiven cgroups; die Max-Direktiven setzen harte Kappen. Ich beginne oft mit Gewichten und ergänze harte Grenzen nur dort, wo ich „Noisy Neighbors“ sicher einfangen muss.
Konfigurieren: Unit-Files, Drop-ins und set-property
Ich trage Limits direkt in das Unit-File ein oder nutze Drop-ins, die Originaldateien unangetastet lassen. Mit systemctl edit NAME.service erstelle ich ein Fragment, das CPUQuota, CPUWeight, MemoryMax und weitere Direktiven ergänzt. Für schnelle Tests setze ich systemctl set-property ein; systemd schreibt die Änderung sauber in ein Drop-in. Nach Anpassungen lade ich Daemons neu und prüfe den Status, um die Wirkung zu verifizieren. Diese Arbeitsweise hält Updates konfliktfrei und versieht jede Änderung mit klarer Historie.
Drop-in-Prioritäten, Presets und Defaults
Ich achte auf die Reihenfolge der Drop-ins: systemd lädt numerisch sortiert; ein 90-override.conf überschreibt z. B. frühere 10-*.conf. Vendor-Presets fasse ich nicht an; ich überschreibe sie in /etc, damit Paketupdates unkritisch bleiben. Systemweite Defaults wie DefaultTasksMax, DefaultCPUAccounting oder DefaultMemoryAccounting setze ich bewusst in systemd.conf, um einheitliche Metriken und Schutzgeländer auch für neue Units sicherzustellen.
# Prüfen der aktiven Werte
systemctl show NAME.service -p CPUQuota -p CPUWeight -p MemoryMax
systemd-analyze dump | grep -E "Default(TasksMax|CPUAccounting|MemoryAccounting)"
# Persistente Override-Datei öffnen/erstellen
systemctl edit NAME.service
Slices in der Praxis: Gruppen sinnvoll begrenzen
Ich bündele verwandte Dienste in eigenen Slices, etwa web.slice, db.slice und batch.slice. In batch.slice erlaube ich zum Beispiel 200% CPU und 4G RAM, damit Hintergrundjobs genug Luft bekommen, ohne Frontends zu verdrängen. Services ordne ich per Slice= in ihren Ziel-Slice; Limits greifen dann für alle Mitglieder gemeinsam. Diese Gruppierung vereinfacht Richtlinien enorm: Ein neues Teamprojekt übernimmt automatisch die Policies seines Slices. Für isolierte Kunden- oder App-Gruppen hilft zudem der Blick auf cgroups‑Isolation, um die Trennung sauber zu planen.
Standard-Slices: system.slice, user.slice, machine.slice
Ich lasse Systemdienste im system.slice und setze dort nur vorsichtig globale Caps, damit essenzielle Services nicht verhungern. Benutzerprozesse landen im user.slice, wo ich interaktive Sitzungen begrenze, ohne Shells hart zu blockieren. Virtualisierungen und Container führe ich im machine.slice zusammen und gebe pro VM oder Container klare Budgets. Diese Standardstruktur schafft Ordnung und bietet sinnvolle Ankerpunkte für eigene Slices. Wer sauber erbt, spart sich viele Einzelregeln und hält die Transparenz hoch.
Delegation für Container und dynamische Workloads
Wenn ich Unterbäume an Container-Runtimes oder benutzergetriebene Tools übergebe, setze ich Delegate=yes bewusst und nur an den Stellen, wo Kontrolle benötigt wird. So bleibt die Oberhoheit bei systemd, während der Delegationsempfänger innerhalb seines Subtrees eigene cgroups anlegen darf. In Kombination mit Scopes kann ich kurzlebige Prozesse (z. B. CI-Jobs) sauber einsammeln, begrenzen und wieder freigeben, ohne die Slices zu verwässern.
[Service]
# Erlaubt Untersteuerung des cgroup-Subtrees (z. B. durch Container-Runtime)
Delegate=yes
Slice=machine.slice
MemoryMax=4G
CPUWeight=300
Monitoring und Fehlersuche: Status, cgtop, cgls
Ich prüfe zuerst mit systemctl status NAME.service, welche Limits aktiv sind und wie der Dienst läuft. Mit systemd-cgtop sehe ich CPU- und Speicherverbrauch je cgroup in Echtzeit. systemd-cgls zeigt mir die Baumstruktur und macht Vererbung sichtbar. Bei Auffälligkeiten lese ich die Dateien in /sys/fs/cgroup, um gesetzte Werte der Controller zu verifizieren. Danach passe ich Quoten schrittweise an, beobachte Metriken und dokumentiere jede Änderung.
Monitoring vertiefen: Accounting, PSI und schnelle Tests
Für aussagekräftige Metriken aktiviere ich CPUAccounting, MemoryAccounting und IOAccounting auf Units oder per Default. Ich beobachte Lastspitzen zusätzlich über Pressure-Informationen (PSI) im Kernel, um zu erkennen, ob Drosselungen (memory.high) anziehen oder I/O dauerhaft knapp ist. Für reproduzierbare Tests starte ich Workloads mit systemd-run als Scope und weise temporär Limits zu, bevor ich sie in ein persistentes Drop-in gieße.
# Temporärer Scope mit I/O- und CPU-Gewicht
systemd-run --scope -p IOWeight=400 -p CPUWeight=300 --unit test-batch -- dd if=/dev/zero of=/tmp/out bs=1M count=1024
# Accounting auf einer bestehenden Unit aktivieren
systemctl set-property NAME.service CPUAccounting=yes MemoryAccounting=yes IOAccounting=yes
Troubleshooting und typische Stolpersteine
- Harsh Caps vs. Burst: Zu enge CPUQuota ohne angepasste Periode erzeugt Stottern. Ich erhöhe CPUQuotaPeriodSec oder senke die Quote nur moderat und arbeite stärker mit CPUWeight.
- Speicher-Drossel greift zu früh: MemoryHigh zu knapp gewählt? Ich hebe es an oder definiere MemoryLow, damit kritische Pfade nicht zu aggressiv reclaimt werden.
- I/O-Geräte falsch adressiert: IO*-Direktiven erwarten Blockgeräte. Ich prüfe mit lsblk den Device-Pfad und setze Regeln pro Gerät, nicht pro Mountpoint.
- Threads laufen ins Limit: TasksMax zu niedrig bremst Worker-Pools aus. Ich dimensioniere nach Peak-Threadzahl plus Puffer und überwache mit systemd-cgtop die Tasks-Spalte.
- Drop-ins ohne Wirkung: Nach Änderungen führe ich systemctl daemon-reload aus und verifiziere mit systemctl show, ob die Properties tatsächlich gesetzt sind.
Best Practices für Prioritäten und Grenzen
Ich gruppiere Services nach Rolle, vergebe CPUWeight und IOWeight nach Wichtigkeit und setze harte Speicherlimits per MemoryMax. Kritische Datenbanken erhalten hohes Gewicht und weniger strikte Quoten, während Reports und Batch-Jobs stärker beschränkt werden. TasksMax setze ich, wenn Anwendungen viele Worker nutzen oder das Risiko von Thread-Explosionen besteht. Jede Anpassung landet versioniert im Repository, damit ich sie nachvollziehen und bei Bedarf zurückrollen kann. In Staging kalibriere ich Werte auf Lastprofile und übernehme dann konservativ in die Produktion.
Tabellarische Übersicht wichtiger Direktiven
Diese kompakte Tabelle fasst typische Einstellungen zusammen und hilft mir, passende Werte zu wählen.
| Zweck | Direktive | Beispielwert | Wirkung |
|---|---|---|---|
| CPU-Anteil | CPUWeight | 200 | Erhöht Priorität gegenüber Units mit kleinerem Weight; verteilt CPU fair. |
| CPU-Quote | CPUQuota | 50% | Deckelt nutzbare Kernzeit; ideal für dauerlastige Jobs. |
| Speicher hart | MemoryMax | 1G | Absolutes Limit; verhindert OOM durch Ausreißer im gleichen Slice. |
| Speicher weich | MemoryHigh | 800M | Drosselt vor Max; reduziert Druck auf das System. |
| I/O-Priorität | IOWeight | 500 | Bevorzugt zentrale Dienste auf geteilten Disks. |
| PIDs/Threads | TasksMax | 512 | Begrenzt Prozesse/Threads; schützt vor Forks-Lawinen. |
Anwendungsfälle im Hosting und Serverbetrieb
Ich setze für Mandanten eigene Slices auf und hinterlege pro Kunde CPU- und RAM-Budgets. In Microservice-Setups bekommen API- und Auth-Services höhere Gewichte, während Reporting asynchron arbeitet. Für CI/CD-Runner lege ich einen Batch-Slice an, damit Builds nie Frontends verdrängen. In Container- und VM-Umgebungen kapsle ich Workloads im machine.slice und halte Budgets pro Mandant klar. Diese Aufteilung reduziert Noisy-Neighbor-Effekte und sorgt für reproduzierbare Latenzen in Spitzenzeiten.
Zusammenfassung
Ich steuere Linux-Dienste mit systemd und cgroups v2 auf Einheit statt auf Einzelprozesse. CPUQuota, CPUWeight, MemoryMax, MemoryHigh, IOWeight und TasksMax bilden mein Kernset für faire Verteilung und klare Obergrenzen. Slices schaffen Ordnung, bündeln Richtlinien und erleichtern Betrieb sowie Onboarding neuer Services. Monitoring mit systemctl status, cgtop und cgls zeigt früh, wo ich nachstellen muss. So bleiben Performance und Verfügbarkeit planbar, und ich halte Ressourcenkonflikte unter Kontrolle.


