...

Ajuste del programador de E/S del kernel: optimización del rendimiento del alojamiento

Mit I/O Scheduler Tuning optimiere ich gezielt den Núcleo-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 alojamiento performance sicher zu steigern.

Puntos centrales

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
  • estrategia de medición: Fio, iostat, P95/P99, IOPS und Durchsatz vor/nach Änderungen
  • Feineinstellungen: Readahead, RQ-Affinity, Cgroups, ionice für QoS
  • Persistencia: udev-Regeln und GRUB-Parameter für dauerhafte Profile
  • Práctica: Troubleshooting bei Latenzspitzen, Fairness und NVMe-Spezifika

Wie Linux-Disk-Scheduling arbeitet

Ich sehe den I/O-Scheduler als Schaltzentrale, die Anfragen in Cues 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 ninguno (ä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

Empiezo con una clara Línea de base-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 programador 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 Cola-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?

Yo decido el programador-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.

programador Geeignete Hardware Typische Workloads Ventajas Notas
Noop/ninguno NVMe, moderne SSD Viele parallele Reads/Writes, VMs Minimaler Overhead, hohe Parallelität Controller übernimmt Sortierung; testen in SAN/RAID
mq-fecha límite 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 Hardware heredado Cargas de trabajo heredadas Antigua solución patrón Raramente útil en NVMe/SSD modernas

Perfiles prácticos y valores medidos

Para servidores web con muchos Lee la latencia P95 cuenta más que los IOPS puros, por lo que pruebo las peticiones get con keep-alive y TLS en combinación. En las bases de datos entran en juego las escrituras sincronizadas, por lo que simulo el comportamiento de descarga y los costes de fsync con archivos de trabajo fio. Las ventanas de copia de seguridad suelen tener flujos secuenciales; aquí mido el rendimiento en MB/s y me aseguro de que las peticiones frontales no esperen demasiado. En mis pruebas, veo tiempos de respuesta entre 20 y 50 % más cortos, dependiendo de la situación inicial, si el planificador y el readahead se ajustan a las cargas de trabajo. Si necesitas más contexto sobre la medición del rendimiento del disco, puedes encontrar una introducción aquí: Rendimiento del disco en el alojamiento.

Configuración persistente y automatización

Anclo el Elección permanentemente a través de la regla udev para que los dispositivos se inicien directamente en el modo apropiado después de los reinicios. A menudo establezco ninguno para NVMe, mq-deadline para SSDs y BFQ para medios rotatorios si la equidad es primordial. Opcionalmente establezco un valor predeterminado global a través de GRUB si estoy ejecutando una configuración homogénea. Mantengo las reglas cortas y las documento en el repositorio de configuración para que el equipo pueda seguirlas. Para una optimización más profunda del kernel, este artículo complementa la configuración: Rendimiento del núcleo en el alojamiento.

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

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

# SSD: 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"

# Recargar/probar reglas
udevadm control --reload
udevadm trigger
# Por defecto global opcional mediante GRUB
# /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="elevator=mq-deadline"
update-grub

QoS con Cgroups v2 e ionice

Para que ningún trabajo salga del plato bloqueado, Me baso en reglas de calidad de servicio con Cgroups v2 y añado prioridades a través de ionice. Para los inquilinos premium, aumento io.weight, mientras que establezco límites estrictos para los vecinos ruidosos con io.max. Vinculo unidades systemd directamente a Cgroups para que los servicios entren automáticamente en la clase correcta al arrancar. Acelero temporalmente el trabajo de mantenimiento a corto plazo para que las consultas de los clientes sigan funcionando sin problemas. Esta interacción de ponderación, límites y prioridad de procesos crea tiempos de respuesta predecibles incluso bajo carga.

# Crear cgroup y establecer límites
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

Mover el proceso # a Cgroup
echo  | tee /sys/fs/cgroup/hosting/cgroup.procs

# Baja prioridad IO para trabajos secundarios
ionice -c2 -n7 -p

Supervisión y resolución de problemas

Siempre guardo la telemetría cerrar sobre las cargas de trabajo, de lo contrario se me escapan decisiones. Utilizo iostat para leer los tiempos de servicio y la profundidad de las colas, blktrace para analizar los flujos de peticiones y sar/dstat para ver la carga del sistema a lo largo del tiempo. En cuanto a las latencias, no me fijo sólo en los valores medios, sino siempre en P95/P99, porque ahí se hacen visibles los hipo perceptibles. Si P95 es bueno, pero P99 no lo es, ajusto la profundidad de la cola o la afinidad RQ y compruebo los trabajos en competencia. Después de cada corrección, comparo los mismos ratios para que el efecto siga siendo fiable.

Tropiezos típicos y soluciones

Alta Latencia en unidades SSD suele indicar un planificador inadecuado; entonces pruebo inmediatamente mq-deadline y compruebo si las lecturas son más rápidas. Resuelvo la distribución injusta en configuraciones multi-tenant con BFQ y borro los pesos de Cgroup para que los clientes fuertes no desplacen a los más débiles. Los tiempos de espera de NVMe indican firmware o un sondeo demasiado agresivo; en estos casos, desactivo io_poll y reduzco la profundidad hasta que vuelve la estabilidad. Las fluctuaciones de rendimiento en las ventanas de copia de seguridad a menudo pueden suavizarse con una readahead personalizada, especialmente cuando predominan los archivos grandes. Si hay más factores girando al mismo tiempo, procedo paso a paso: un cambio, luego medir, luego el siguiente.

Ajustes del programador en detalle

Una vez hecha la selección básica, giro los tornillos de ajuste del programador correspondiente. Siempre empiezo mirando los parámetros disponibles para cada dispositivo, ya que varían en función del kernel y la distro.

# Mostrar sintonizables disponibles
ls -1 /sys/block//queue/iosched
cat /sys/block//queue/iosched/*

# Ejemplo: mq-deadline más conservador para trabajos con mucha escritura
echo 100 | sudo tee /sys/block//queue/iosched/read_expire
echo 500 | sudo tee /sys/block//queue/iosched/write_expire
echo 1 | sudo tee /sys/block//queue/iosched/front_merges

# Ejemplo: BFQ para una equidad más estricta y menores tiempos de espera
echo 1 | sudo tee /sys/block//queue/iosched/low_latency
echo 0 | sudo tee /sys/block//queue/iosched/slice_idle

En mq-deadline regulo principalmente read_expire/write_expire (en milisegundos) y front_merges para fusionar las solicitudes pendientes. Con BFQ, dependiendo de la densidad de inquilinos, cambio baja latencia y slice_inactivo, para reducir los tiempos de espera entre flujos. Documento cada cambio con valores medidos, ya que los vencimientos incorrectos pueden desencadenar picos de latencia no deseados bajo carga de ráfagas.

Sistema de archivos y opciones de montaje

El ajuste del programador sólo tiene sentido cuando el sistema de archivos coincide. Presto atención a:

  • relatiempo/notiempoEvitar accesos innecesarios de escritura de metadatos.
  • descarte vs. fstrim: En SSDs/NVMe suelo usar fstrim periódico en lugar de descarte en línea para evitar picos de latencia.
  • DiarioPara ext4, se han probado los siguientes datos=ordenados (por defecto) y un comprometer=-intervalo (por ejemplo, 10-30s en función de la tolerancia a la pérdida de datos).
  • BarrerasLas barreras de escritura permanecen activas; no las desactivo a menos que el hardware garantice la protección contra fallos de alimentación (batería/capacitor).
# Ejemplo /etc/fstab para ext4
UUID= /data ext4 defaults,noatime,commit=20 0 2

# Habilitar TRIM periódico en lugar de la opción de descarte
systemctl enable fstrim.timer
systemctl start fstrim.timer

Para XFS también configuré noatime y prefiero fstrim.timer. Las opciones de diario o barrera dependen de la distribución; yo siempre pruebo la combinación kernel/FS específica y mido P95/P99.

RAID, LVM, DM-crypt y Multipath

En las configuraciones apiladas (Device Mapper, LVM, mdraid, Multipath) defino el programador donde la aplicación ve la E/S, es decir, en el Dispositivo de nivel superior - y evitar la doble clasificación por debajo.

# Establecer planificador en el nivel superior (por ejemplo, dm-0)
echo mq-deadline | sudo tee /sys/block/dm-0/queue/scheduler

# Dispositivos NVMe/SAS subyacentes "none" para evitar la doble programación
for d in /sys/block/nvme*n1 /sys/block/sd*; do echo none | sudo tee $d/queue/scheduler; done

# mdraid: Optimizar readahead y stripe cache (RAID5/6)
sudo blockdev --setra 4096 /dev/md0
echo 4096 | sudo tee /sys/block/md0/md/stripe_cache_size

Con los volúmenes cifrados (dm-crypt/LUKS), presto atención a la descarga de la CPU (AES-NI) y me aseguro de que la ruta de E/S no deambule innecesariamente por las colas de trabajo. En concreto, mido las latencias de sincronización y escritura, ya que pueden aumentar debido a la capa criptográfica. En entornos multipath (SAN/iSCSI), configuro el planificador en el dispositivo multipath (dm-X) y compruebo que la conmutación por error de la ruta no genera valores atípicos.

Virtualización y contenedores: host vs. guest

En la pila KVM, hago una distinción consciente entre host e invitado. En la pila Invitado Suelo usar para dispositivos virtio ninguno, para que el hipervisor se encargue de la optimización. En el Anfitrión A continuación, selecciono el programador para cada dispositivo físico que coincida con el hardware (a menudo none/mq-deadline en SSD/NVMe).

# Guest (virtio-blk/virtio-scsi): Establecer planificador a "ninguno
echo none | sudo tee /sys/block/vda/queue/scheduler

# Host: QEMU con iothreads y multiqueue para 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

Vinculo contenedores directamente a Cgroups v2 y uso propiedades systemd (IOWeight, IOReadBandwidthMax/IOWriteBandwidthMax) para que los servicios se inicien automáticamente con los presupuestos de E/S correctos. Importante: Solo prioriza a un nivel - ya sea en el contenedor o en el servicio host - para evitar reglas conflictivas.

Optimización de NUMA, IRQ y polling

En los sistemas multisocket, considero que la E/S y la CPU Cerca de NUMA. Compruebo la distribución de las interrupciones NVMe y las ajusto si es necesario si irqbalance está funcionando de forma subóptima. También utilizo las opciones blk-mq para mantener las terminaciones locales.

# Comprobar interrupciones NVMe y establecer máscaras de núcleo (ejemplo)
grep -i nvme /proc/interrupts
echo  | sudo tee /proc/irq//smp_affinity

# blk-mq: Finalizaciones en la generación del núcleo
echo 2 | sudo tee /sys/block//queue/rq_affinity

# Opcional: Prueba de sondeo de E/S dependiendo de la carga de trabajo (usar con cuidado)
echo 0 | sudo tee /sys/block//queue/io_poll

En el caso de NVMe, puedo utilizar las funciones del controlador para ajustar los parámetros de coalescencia de interrupciones con el fin de suavizar la relación entre la carga de la CPU y la latencia. Aquí doy pequeños pasos y compruebo si P99 se mantiene estable o si la coalescencia provoca una lentitud visible.

Ejemplo de perfiles de puestos de trabajo de la fio y plan de medición

Creo archivos de trabajo reproducibles y tomo nota del kernel, el programador, los parámetros de cola y los montajes del sistema de archivos. Esto me permite comparar los resultados a lo largo de semanas.

# db-sync.fio - Escrituras sincronizadas tipo DB (ext4/XFS)
[global]
ioengine=libaio
directo=1
filename=/dev/
time_based=1
tiempo de ejecución=90
hilo=1
numjobs=8
iodepth=1

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

# web-randread.fio - Lecturas tipo web
[global]
ioengine=libaio
directo=1
filename=/dev/
time_based=1
tiempo de ejecución=90
hilo=1
número de trabajos=8
iodepth=32

[randread-4k]
rw=randread
bs=4k
# Marco de medición
# 1) Calentamiento 60s, 2) Medición 90s, 3) Enfriamiento 30s
# Paralelo: ejecutar iostat, pidstat y blktrace

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

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

Sólo cambio una variable, por ejemplo, scheduler o read_ahead_kb, y vuelvo a comparar los archivos de trabajo idénticos. Sólo cuando las mejoras son consistentes en varias ejecuciones, confirmo los ajustes.

Gestión de cambios: introducción y retroceso seguros

En entornos de alojamiento productivos, despliego cambios de E/S escalonado Empiezo con un host canario, luego un pequeño lote AZ/cluster, seguido de un amplio despliegue. Versiono las reglas de Udev y adjunto cada cambio a un ticket con valores medidos. Para el rollback, tengo preparado un script que reproduce los valores anteriores (scheduler, read_ahead_kb, límites de Cgroup). De este modo, las intervenciones siguen siendo reversibles si las cargas de trabajo cambian a corto plazo.

Resumen: Así es como procedo

Empiezo con una clara Valor real, Mido las latencias y el rendimiento y documento la configuración. A continuación, selecciono un planificador adecuado para cada dispositivo: ninguno para NVMe/SSD virtuales, mq-deadline para cargas de servidor mixtas, BFQ para entornos compartidos con muchos usuarios. A continuación, modifico la readahead, la afinidad RQ y las prioridades de proceso para dar prioridad a las cargas de trabajo frontales. Si las mediciones muestran sistemáticamente que la elección funciona, la corrijo mediante udev/GRUB y escribo los parámetros. La monitorización permanece activa porque las cargas de trabajo cambian, y con pequeñas correcciones mantengo la Actuación permanentemente alta.

Artículos de actualidad