...

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

Con I/O Scheduler Tuning optimizo específicamente el Núcleo-path para el acceso a memoria y reducir la latencia en entornos de alojamiento. El artículo muestra de forma práctica cómo adapto la programación de discos de Linux al hardware y a la carga de trabajo para alojamiento rendimiento de forma segura.

Puntos centrales

Los siguientes puntos clave le darán una rápida visión general del contenido de este artículo.

  • Selección del programadorNoop/none, mq-deadline, BFQ, Kyber en función del hardware y la carga de trabajo.
  • estrategia de mediciónFio, iostat, P95/P99, IOPS y rendimiento antes y después de los cambios
  • Ajustes finosReadahead, RQ-Affinity, Cgroups, ionice para QoS
  • Persistencia: reglas udev y parámetros GRUB para perfiles persistentes
  • PrácticaSolución de problemas de picos de latencia, equidad y aspectos específicos de NVMe

Cómo funciona la programación de discos en Linux

Veo el programador de E/S como un centro de control que convierte las peticiones en Cues clasifica, fusiona y prioriza. Con los HDD, evito los costosos movimientos de cabezal organizando las peticiones según las direcciones de bloque y reduciendo así los tiempos de búsqueda. En los SSD y NVMe, predomina el paralelismo, por lo que el subsistema de colas múltiples blk-mq ensancha la ruta y prioriza las solicitudes múltiples. CPUs distribuido. Así se reducen las latencias, se suavizan los picos y se mantiene el rendimiento, aunque muchos servicios escriban y lean al mismo tiempo. En el alojamiento, los servidores web, las bases de datos y los trabajos de copia de seguridad se juntan, así que siempre alineo la programación con los patrones de acceso dominantes.

Breve explicación de los programadores habituales

Para NVMe y las unidades SSD modernas, suelo elegir ninguno (equivalente a Noop en el contexto blk-mq), porque el controlador está optimizado internamente y cualquier sobrecarga adicional cuesta dinero. mq-deadline establece plazos fijos para lecturas y escrituras, prioriza las operaciones de lectura y ofrece tiempos de respuesta constantes en cargas de servidor mixtas. BFQ distribuye el ancho de banda de forma equitativa entre los procesos y es adecuado para configuraciones multiinquilino en las que, de otro modo, las máquinas virtuales individuales ocuparían el disco. Kyber busca bajas latencias y ralentiza las peticiones entrantes si se superan los tiempos objetivo. CFQ se considera una solución heredada y apenas se adapta a NVMe; yo sólo utilizo CFQ cuando las configuraciones heredadas lo requieren o las pruebas muestran claras ventajas; aquí ofrezco una descripción detallada: Guía del Programador de E/S.

Ajuste del programador de E/S paso a paso

Empiezo con una clara Línea de base-para poder mostrar las ganancias de forma objetiva. Utilizo fio para patrones sintéticos, iostat para estadísticas de dispositivos y recojo latencias P95/P99 para lecturas y escrituras. Después compruebo el planificador activo por dispositivo y lo cambio en tiempo de ejecución para realizar rápidamente contrapruebas. Sólo hago ajustes persistentes cuando las mediciones estables muestran que la elección es correcta. De este modo, evito decisiones erróneas que luego obliguen a costosas reversiones.

# Comprobar planificador actual
cat /sys/block//queue/scheduler

# Cambiar sobre la marcha (ejemplo: nvme0n1 a mq-deadline)
echo mq-deadline | sudo tee /sys/block/nvme0n1/queue/scheduler

# Comparación rápida con fio (Lecturas aleatorias 4k)
fio --name=rr --rw=randread --bs=4k --iodepth=32 --numjobs=4 --runtime=60 --filename=/dev/nvme0n1

Vigilo la carga de la CPU porque una programador crea conmutaciones de contexto adicionales y, por tanto, reduce el rendimiento neto. En cuanto disminuyen las latencias y aumenta el rendimiento, guardo la decisión y documento los perfiles de prueba. Cada paso va seguido de un cambio y luego de una medición, para que pueda separar claramente causa y efecto. Esta disciplina merece la pena cuando se instalan varias clases de discos en el servidor y los distintos dispositivos reaccionan de forma diferente.

Ajustes finos: Readahead, RQ-Affinity, Cgroups

Después de seleccionar el programador, ajusto el Cola-para la carga. Para las copias de seguridad secuenciales aumento el readahead, para las IO aleatorias lo reduzco para no cargar páginas innecesarias. Con la afinidad RQ, me aseguro de que las finalizaciones aterricen en el núcleo que generó la solicitud, lo que mejora la latencia y la localidad de la caché. Utilizo ionice para degradar procesos como las copias de seguridad y la indexación, de modo que las peticiones web no se resientan. En entornos multi-tenant, regulo el ancho de banda y las IOPS mediante Cgroups v2 para establecer límites estrictos por cliente.

# Readahead para patrones secuenciales
echo 128 | sudo tee /sys/block//queue/read_ahead_kb

# Afinidad RQ: 2 = finalización en núcleo generador
echo 2 | sudo tee /sys/block//queue/rq_affinity

# Proceso de copia de seguridad inferior
ionice -c2 -n7 -p 

# Cgroup v2: peso y límite (ejemplo major: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

¿Cuál es la mejor opción para alojar perfiles?

Yo decido el programador-Elija en función de la clase de hardware, el patrón de acceso y el tamaño de destino (latencia frente a rendimiento frente a equidad). Las unidades SSD NVMe en máquinas virtuales de un solo inquilino no suelen beneficiarse de ninguna porque el controlador realiza una optimización exhaustiva y cada capa de software cuenta. Para cargas mixtas de lectura/escritura en unidades SSD, suelo utilizar mq-deadline, ya que prioriza las solicitudes de lectura y protege así los tiempos de respuesta. En entornos de alojamiento compartido, elijo BFQ para garantizar la equidad entre clientes y evitar monopolios de ancho de banda. Utilizo Kyber cuando las latencias objetivo son críticas y tengo que respetar límites estrictos para determinadas cargas de trabajo.

programador Hardware adecuado Cargas de trabajo típicas Ventajas Notas
Noop/ninguno NVMe, SSD modernas Muchas lecturas/escrituras paralelas, máquinas virtuales Mínima sobrecarga, alto paralelismo El controlador se encarga de la clasificación; prueba en SAN/RAID
mq-fecha límite SSD, SAS, disco duro rápido Cargas mixtas web/DB Latencias de lectura prioritarias, buen rendimiento Valores límite conservadores; posibilidad de ajustes
BFQ SSD/HDD en multiusuario Muchos usuarios, cgroups Control claro de la equidad y el ancho de banda Cierto esfuerzo administrativo, ponderación limpia
Kyber SSD, NVMe Servicios de latencia crítica Latencias objetivo controlables Mida con precisión para ajustar correctamente el estrangulamiento
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