Kernel I/O Scheduler Tuning: Optimierung für Hosting Performance

Mit I/O Scheduler Tuning optimiere ich gezielt den Kernel-Pfad für Speicherzugriffe und senke Latenz in Hosting-Umgebungen. Der Beitrag zeigt praxisnah, wie ich Linux-Disk-Scheduling an Hardware und Workload anpasse, um hosting performance sicher zu steigern.

Zentrale Punkte

Die folgenden Stichpunkte geben dir einen schnellen Überblick über die Inhalte dieses Beitrags.

  • Scheduler-Wahl: Noop/none, mq-deadline, BFQ, Kyber je nach Hardware und Workload
  • Messstrategie: Fio, iostat, P95/P99, IOPS und Durchsatz vor/nach Änderungen
  • Feineinstellungen: Readahead, RQ-Affinity, Cgroups, ionice für QoS
  • Persistenz: udev-Regeln und GRUB-Parameter für dauerhafte Profile
  • Praxis: Troubleshooting bei Latenzspitzen, Fairness und NVMe-Spezifika

Wie Linux-Disk-Scheduling arbeitet

Ich sehe den I/O-Scheduler als Schaltzentrale, die Anfragen in Queues sortiert, zusammenführt und priorisiert. Bei HDDs vermeide ich teure Kopfbewegungen, indem ich Anfragen nach Blockadressen ordne und so Suchzeiten reduziere. Auf SSDs und NVMe dominiert Parallelität, weshalb das Multi-Queue-Subsystem blk-mq den Pfad breiter macht und auf mehrere CPUs verteilt. Das senkt Latenzen, glättet Peaks und hält den Durchsatz auf Kurs, selbst wenn viele Dienste gleichzeitig schreiben und lesen. Im Hosting treffen Webserver, Datenbanken und Backup-Jobs aufeinander, daher richte ich Scheduling immer an den dominanten Zugriffsmustern aus.

Die gängigen Scheduler kurz erklärt

Für NVMe und moderne SSDs wähle ich häufig none (äquivalent zu Noop im blk-mq-Kontext), weil der Controller intern optimiert und jeder zusätzliche Overhead kostet. mq-deadline setzt feste Fristen für Reads und Writes, priorisiert Lesevorgänge und liefert in gemischten Server-Lasten konstante Antwortzeiten. BFQ verteilt Bandbreite fair über Prozesse und eignet sich in Multi-Tenant-Setups, in denen einzelne VMs sonst die Platte belegen würden. Kyber zielt auf geringe Latenzen und bremst ankommende Requests, wenn Zielzeiten reißen. CFQ gilt als Altlast und passt kaum zu NVMe; ich greife nur zu CFQ, wenn Legacy-Setups es erfordern oder Tests klare Vorteile zeigen; einen ausführlichen Überblick gebe ich hier: I/O-Scheduler Guide.

I/O Scheduler Tuning Schritt für Schritt

Ich starte mit einem klaren Baseline-Messlauf, damit ich Gewinne objektiv zeigen kann. Dazu nutze ich fio für synthetische Muster, iostat für Gerätestatistiken und sammle P95/P99-Latenzen für Reads und Writes. Danach prüfe ich den aktiven Scheduler pro Gerät und ändere ihn zur Laufzeit, um schnell gegenzutesten. Persistente Anpassungen setze ich erst, wenn Messungen stabil zeigen, dass die Wahl passt. So vermeide ich Fehlentscheidungen, die später teure Rollbacks erzwingen.

# Aktuellen Scheduler prüfen
cat /sys/block/<dev>/queue/scheduler

# Laufend wechseln (Beispiel: nvme0n1 auf mq-deadline)
echo mq-deadline | sudo tee /sys/block/nvme0n1/queue/scheduler

# Schneller Vergleich mit fio (Random Reads 4k)
fio --name=rr --rw=randread --bs=4k --iodepth=32 --numjobs=4 --runtime=60 --filename=/dev/nvme0n1

Ich behalte die CPU-Last im Blick, weil ein ungeeigneter Scheduler zusätzliche Context-Switches erzeugt und damit die Nettoleistung drückt. Sobald Latenzen fallen und der Durchsatz steigt, sichere ich die Entscheidung und dokumentiere Testprofile. Jeder Schritt folgt einer Änderung, dann einer Messung, damit ich Ursache und Wirkung sauber trennen kann. Diese Disziplin zahlt sich aus, wenn mehrere Plattenklassen im Server verbaut sind und einzelne Geräte anders reagieren.

Feineinstellungen: Readahead, RQ-Affinity, Cgroups

Nach der Scheduler-Wahl justiere ich die Queue-Parameter für die Last. Für sequenzielle Backups hebe ich Readahead an, bei Random-IO senke ich ihn, damit ich keine unnötigen Seiten lade. Mit RQ-Affinity sorge ich dafür, dass Completions auf dem Kern landen, der die Anfrage erzeugt hat, was Latenz und Cache-Locality verbessert. Prozesse wie Backups und Indizierung stufe ich mit ionice ab, damit Webanfragen nicht leiden. In Multi-Tenant-Umgebungen regle ich Bandbreite und IOPS über Cgroups v2, um harte Grenzen pro Kunde zu setzen.

# Readahead für sequenzielle Muster
echo 128 | sudo tee /sys/block/<dev>/queue/read_ahead_kb

# RQ-Affinity: 2 = Completion auf erzeugendem Kern
echo 2 | sudo tee /sys/block/<dev>/queue/rq_affinity

# Backup-Prozess absenken
ionice -c2 -n7 -p <pid>

# Cgroup v2: Gewicht und Limit (Beispielmajor:minor 8:0)
echo 1000 | sudo tee /sys/fs/cgroup/hosting/io.weight
echo "8:0 rbps=50M wbps=25M" | sudo tee /sys/fs/cgroup/hosting/io.max

Welche Wahl passt für Hosting-Profile?

Ich entscheide die Scheduler-Wahl nach Hardwareklasse, Zugriffsmuster und Zielgröße (Latenz vs. Durchsatz vs. Fairness). NVMe-SSDs in Single-Tenant-VMs profitieren meist von none, weil der Controller umfangreiche Optimierung übernimmt und jede Software-Schicht zählt. Für gemischte Lese-/Schreiblast auf SSDs setze ich oft auf mq-deadline, da es Leseanfragen priorisiert und so Antwortzeiten schützt. In Shared-Hosting-Umgebungen wähle ich BFQ, um Fairness zwischen Kunden sicherzustellen und Bandbreitenmonopole zu verhindern. Kyber ziehe ich heran, wenn Ziel-Latenzen kritisch sind und ich für bestimmte Workloads harte Grenzen einhalten muss.

Scheduler Geeignete Hardware Typische Workloads Vorteile Hinweise
Noop/none NVMe, moderne SSD Viele parallele Reads/Writes, VMs Minimaler Overhead, hohe Parallelität Controller übernimmt Sortierung; testen in SAN/RAID
mq-deadline SSD, SAS, schnelle HDD Gemischte Web-/DB-Lasten Leselatenzen priorisiert, guter Durchsatz Deadline-Werte konservativ; Feintuning möglich
BFQ SSD/HDD in Multi-Tenant Viele Nutzer, cgroups Klare Fairness und Bandbreitenkontrolle Etwas Verwaltungsaufwand, sauber gewichten
Kyber SSD, NVMe Latenz-kritische Dienste Ziel-Latenzen steuerbar Genau messen, um Throttling richtig zu setzen
CFQ Legacy-Hardware Alt-Workloads Frühere Standardlösung Selten sinnvoll auf moderner NVMe/SSD

Praxisnahe Profile und Messwerte

Für Webserver mit vielen kleinen Reads zählt die P95-Latenz mehr als reine IOPS, daher prüfe ich Get-Requests mit Keep-Alive und TLS im Zusammenspiel. Datenbanken bringen Sync-Writes ins Spiel, weshalb ich hier Flush-Verhalten und Fsync-Kosten mit fio-Jobfiles simuliere. Backup-Fenster haben oft sequentielle Ströme; hier messe ich Durchsatz in MB/s und achte darauf, dass Frontend-Anfragen nicht zu lange warten. In meinen Tests sehe ich je nach Ausgangslage 20–50 % kürzere Antwortzeiten, wenn Scheduler und Readahead zu den Workloads passen. Wer mehr Kontext zur Messung des Plattendurchsatzes braucht, findet hier einen Einstieg: Disk-Durchsatz im Hosting.

Persistente Konfiguration und Automatisierung

Ich verankere die Wahl dauerhaft per udev-Regel, damit Geräte nach Reboots direkt im passenden Modus starten. Für NVMe setze ich oft none, für SSDs mq-deadline und für rotierende Medien BFQ, wenn Fairness im Vordergrund steht. Über GRUB lege ich optional einen globalen Default, falls ich ein homogenes Setup betreibe. Die Regeln halte ich knapp und dokumentiere sie im Konfigurations-Repository, damit das Team sie nachverfolgen kann. Für tiefergehende Kernel-Optimierung ergänzt dieser Beitrag das Setup: Kernel-Performance im Hosting.

# /etc/udev/rules.d/60-ioschedulers.rules

# NVMe: none
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"

# SSDs: mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]|vd[a-z]", ATTR{rotational}=="0", ATTR{queue/scheduler}="mq-deadline"

# HDDs: BFQ
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{rotational}=="1", ATTR{queue/scheduler}="bfq"

# Regeln neu laden/testen
udevadm control --reload
udevadm trigger
# Optionaler globaler Default über GRUB
# /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="elevator=mq-deadline"
update-grub

QoS mit Cgroups v2 und ionice

Damit kein Job die Platte blockiert, setze ich auf QoS-Regeln mit Cgroups v2 und ergänze Prioritäten über ionice. Für Premium-Tenants hebe ich io.weight an, während ich für laute Nachbarn harte Grenzen mit io.max setze. Systemd-Units binde ich direkt an Cgroups, sodass Dienste beim Start automatisch in die richtige Klasse rutschen. Kurzfristige Wartungsarbeiten drossele ich temporär, damit Kundenanfragen weiter flüssig laufen. Dieses Zusammenspiel aus Gewichtung, Limits und Prozess-Priorität schafft berechenbare Antwortzeiten auch unter Last.

# Cgroup anlegen und Limits setzen
mkdir -p /sys/fs/cgroup/hosting
echo 1000 | tee /sys/fs/cgroup/hosting/io.weight
echo "8:0 rbps=100M wbps=60M" | tee /sys/fs/cgroup/hosting/io.max

# Prozess in Cgroup verschieben
echo <pid> | tee /sys/fs/cgroup/hosting/cgroup.procs

# Niedrige IO-Priorität für Nebenjobs
ionice -c2 -n7 -p <pid>

Monitoring und Troubleshooting

Ich halte Telemetrie stets nah an den Workloads, sonst verfehle ich Entscheidungen. Mit iostat lese ich Service-Zeiten und Queue-Tiefen, mit blktrace analysiere ich Request-Flüsse, und mit sar/dstat sehe ich Systemlast im Zeitverlauf. Für Latenzen schaue ich nicht nur auf Mittelwerte, sondern immer auf P95/P99, weil spürbare Haker dort sichtbar werden. Wenn P95 gut ist, P99 aber kippt, justiere ich Queue-Tiefe oder RQ-Affinity und prüfe konkurrierende Jobs. Nach jeder Korrektur vergleiche ich dieselben Kennzahlen, damit die Wirkung belastbar bleibt.

Typische Stolpersteine und Abhilfe

Hohe Latenz auf SSDs deutet oft auf einen unpassenden Scheduler hin; ich teste dann sofort mq-deadline und prüfe, ob Reads schneller werden. Unfaire Verteilung in Multi-Tenant-Setups löse ich mit BFQ und klaren Cgroup-Gewichten, damit starke Kunden schwächere nicht verdrängen. NVMe-Timeouts deuten auf Firmware oder zu aggressives Polling; in solchen Fällen deaktiviere ich io_poll und senke die Tiefe, bis Stabilität zurückkehrt. Schwankender Durchsatz in Backup-Fenstern lässt sich häufig mit angepasstem Readahead glätten, besonders wenn große Dateien dominieren. Drehen mehr Faktoren gleichzeitig, gehe ich schrittweise vor: eine Änderung, dann Messen, dann die nächste.

Scheduler-Tunables im Detail

Nachdem die Grundwahl sitzt, drehe ich an den Stellschrauben der jeweiligen Scheduler. Ich beginne immer damit, mir die verfügbaren Parameter je Gerät anzusehen, da sie je nach Kernel und Distro variieren.

# Verfügbare Tunables anzeigen
ls -1 /sys/block/<dev>/queue/iosched
cat /sys/block/<dev>/queue/iosched/*

# Beispiel: mq-deadline konservativer für Write-lastige Jobs
echo 100 | sudo tee /sys/block/<dev>/queue/iosched/read_expire
echo 500 | sudo tee /sys/block/<dev>/queue/iosched/write_expire
echo 1   | sudo tee /sys/block/<dev>/queue/iosched/front_merges

# Beispiel: BFQ für striktere Fairness und geringere Idle-Zeiten
echo 1   | sudo tee /sys/block/<dev>/queue/iosched/low_latency
echo 0   | sudo tee /sys/block/<dev>/queue/iosched/slice_idle

Bei mq-deadline reguliere ich vor allem read_expire/write_expire (in Millisekunden) und front_merges für das Zusammenführen anliegender Requests. Bei BFQ schalte ich je nach Tenant-Dichte low_latency und slice_idle, um Wartezeiten zwischen Flows zu reduzieren. Ich dokumentiere jede Änderung mit Messwerten, denn falsche Expires können bei Burst-Last ungewollte Latenzspitzen auslösen.

Dateisystem- und Mount-Optionen

Scheduler-Tuning entfaltet sich erst richtig, wenn das Dateisystem dazu passt. Ich achte auf:

  • relatime/noatime: unnötige Metadaten-Schreibzugriffe vermeiden.
  • discard vs. fstrim: Auf SSDs/NVMe nutze ich meist periodisches fstrim statt Online-Discard, um Latenzspitzen zu vermeiden.
  • Journaling: Für ext4 bewähren sich data=ordered (Default) und ein passendes commit=-Intervall (z. B. 10–30s je nach Datenverlust-Toleranz).
  • Barriers: Schreibe-Barrieren bleiben aktiv; ich deaktiviere sie nicht, außer die Hardware garantiert Stromausfallschutz (Batterie/Capacitor).
# Beispiel /etc/fstab für ext4
UUID=<uuid> /data ext4 defaults,noatime,commit=20 0 2

# Periodisches TRIM statt discard-Option aktivieren
systemctl enable fstrim.timer
systemctl start fstrim.timer

Für XFS setze ich ebenfalls noatime und bevorzuge fstrim.timer. Journal- oder Barrier-Optionen sind distributionsabhängig; ich teste immer die konkrete Kernel/FS-Kombination und messe P95/P99.

RAID, LVM, DM-crypt und Multipath

In gestapelten Setups (Device Mapper, LVM, mdraid, Multipath) lege ich den Scheduler dort fest, wo die Applikation I/O sieht – also am Top-Level-Device – und verhindere doppelte Sortierung darunter.

# Scheduler auf dem Top-Level (z. B. dm-0) setzen
echo mq-deadline | sudo tee /sys/block/dm-0/queue/scheduler

# Unterliegenden NVMe/SAS-Devices "none", um Doppel-Scheduling zu vermeiden
for d in /sys/block/nvme*n1 /sys/block/sd*; do echo none | sudo tee $d/queue/scheduler; done

# mdraid: Readahead und Stripe-Cache (RAID5/6) optimieren
sudo blockdev --setra 4096 /dev/md0
echo 4096 | sudo tee /sys/block/md0/md/stripe_cache_size

Bei verschlüsselten Volumes (dm-crypt/LUKS) achte ich auf CPU-Offload (AES-NI) und darauf, dass der I/O-Pfad nicht unnötig über Workqueues wandert. Ich messe gezielt Sync-Write-Latenzen, da diese durch die Crypto-Schicht stärker ansteigen können. In Multipath-Umgebungen (SAN/iSCSI) setze ich den Scheduler auf dem Multipath-Gerät (dm-X) und überprüfe, ob Pfad-Failover keine Ausreißer erzeugt.

Virtualisierung und Container: Host vs. Gast

Im KVM-Stack trenne ich bewusst zwischen Host und Gast. Im Gast setze ich für virtio-Devices meist none, damit der Hypervisor die Optimierung übernimmt. Auf dem Host wähle ich dann pro physischem Device den Scheduler passend zur Hardware (häufig none/mq-deadline auf SSD/NVMe).

# Gast (virtio-blk/virtio-scsi): Scheduler "none" setzen
echo none | sudo tee /sys/block/vda/queue/scheduler

# Host: QEMU mit iothreads und Multiqueue für virtio-blk
qemu-system-x86_64 \
  -drive if=none,id=vd0,file=/var/lib/libvirt/images/guest.qcow2,cache=none,aio=native \
  -object iothread,id=ioth0 \
  -device virtio-blk-pci,drive=vd0,num-queues=8,iothread=ioth0

Container binde ich direkt an Cgroups v2 und nutze systemd-Properties (IOWeight, IOReadBandwidthMax/IOWriteBandwidthMax), damit Dienste automatisch mit den richtigen I/O-Budgets starten. Wichtig: Nur auf einer Ebene priorisieren – entweder im Container oder im Host-Service – um sich widersprechende Regeln zu vermeiden.

NUMA, IRQ- und Polling-Optimierung

Auf Multi-Socket-Systemen halte ich I/O und CPU NUMA-nah. Ich prüfe die Verteilung der NVMe-Interrupts und passe sie bei Bedarf an, wenn irqbalance suboptimal arbeitet. Zusätzlich nutze ich blk-mq-Optionen, um Completions lokal zu halten.

# NVMe-Interrupts prüfen und Kernmasken setzen (Beispiel)
grep -i nvme /proc/interrupts
echo <hex-mask> | sudo tee /proc/irq/<irq>/smp_affinity

# blk-mq: Completions auf erzeugendem Kern
echo 2 | sudo tee /sys/block/<dev>/queue/rq_affinity

# Optional: I/O-Polling je nach Workload testen (vorsichtig einsetzen)
echo 0 | sudo tee /sys/block/<dev>/queue/io_poll

Für NVMe kann ich über Controller-Features die Interrupt-Coalescing-Parameter anpassen, um das Verhältnis aus CPU-Last und Latenz zu glätten. Ich taste mich hier in kleinen Schritten vor und prüfe, ob P99 stabil bleibt oder ob Coalescing zu sichtbarer Trägheit führt.

Beispielhafte fio-Jobfiles und Messplan

Ich lege reproduzierbare Jobfiles an und notiere Kernel, Scheduler, Queue-Parameter und Filesystem-Mounts. So lassen sich Ergebnisse über Wochen vergleichen.

# db-sync.fio – DB-ähnliche Sync-Writes (ext4/XFS)
[global]
ioengine=libaio
direct=1
filename=/dev/<dev>
time_based=1
runtime=90
thread=1
numjobs=8
iodepth=1

[randwrite-sync4k]
rw=randwrite
bs=4k
fsync=1

# web-randread.fio – Web-ähnliche Reads
[global]
ioengine=libaio
direct=1
filename=/dev/<dev>
time_based=1
runtime=90
thread=1
numjobs=8
iodepth=32

[randread-4k]
rw=randread
bs=4k
# Messrahmen
# 1) Warmup 60s, 2) Messung 90s, 3) Cooldown 30s
# Parallel: iostat, pidstat und blktrace laufen lassen

iostat -x 1 | tee iostat.log &
pidstat -dl 1 | tee pidstat.log &
blktrace -d /dev/<dev> -o - | blkparse -i - -d trace.dump &

# Nachlauf: P95/P99 aus fio-JSON ziehen
fio --output-format=json --output=fio.json db-sync.fio
jq '.jobs[].lat_ns["percentile"]|{p95:.["95.000000"],p99:.["99.000000"]}' fio.json

Ich ändere immer nur eine Variable, z. B. Scheduler oder read_ahead_kb, und vergleiche die identischen Jobfiles erneut. Erst wenn die Verbesserungen über mehrere Läufe konsistent sind, schreibe ich die Einstellungen fest.

Change-Management: sicher einführen und zurückrollen

In produktiven Hosting-Umgebungen rolle ich I/O-Änderungen gestaffelt aus: ein Canary-Host, dann eine kleine AZ/Cluster-Partie, erst danach der breite Rollout. Udev-Regeln versioniere ich und hänge jede Änderung an ein Ticket mit Messwerten. Für das Rollback halte ich ein Skript bereit, das die vorherigen Werte ausspielt (Scheduler, read_ahead_kb, Cgroup-Limits). So bleiben Eingriffe reversibel, wenn sich Workloads kurzfristig ändern.

Zusammenfassung: So gehe ich vor

Ich starte mit einem klaren Istwert, messe Latenzen und Durchsatz und dokumentiere das Setup. Danach wähle ich je Gerät einen passenden Scheduler: none für NVMe/virtuelle SSDs, mq-deadline für gemischte Server-Lasten, BFQ für geteilte Umgebungen mit vielen Nutzern. Anschließend feile ich an Readahead, RQ-Affinity und Prozess-Prioritäten, damit Frontend-Workloads Vorrang behalten. Wenn Messungen konsistent zeigen, dass die Wahl trägt, fixiere ich sie über udev/GRUB und schreibe die Parameter fest. Monitoring bleibt aktiv, denn Workloads ändern sich, und mit kleinen Korrekturen halte ich die Performance dauerhaft hoch.

Aktuelle Artikel