El programador de E/S (I/O scheduler) es la parte del kernel que decide en qué orden se envían al disco las peticiones de lectura y escritura. En discos mecánicos, reordenar y agrupar peticiones marca una gran diferencia; en NVMe y en discos virtuales, casi cualquier trabajo extra solo añade latencia. En este tutorial verás qué programadores ofrece Ubuntu 24.04, cambiarás el de un disco en caliente, medirás el efecto con fio y dejarás la elección fija con una regla de udev.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS o un servidor bare metal de CubePath.
  • Un usuario no root con privilegios sudo.
  • Unos 4 GB libres en el disco que vas a probar, para el archivo de pruebas de fio.

Paso 1: Identificar los discos y el programador actual

Lista los discos físicos o virtuales del servidor. La columna ROTA vale 1 para discos mecánicos y 0 para SSD, NVMe y la mayoría de discos virtuales:

lsblk -d -o NAME,ROTA,SIZE,MODEL
NAME    ROTA   SIZE MODEL
sda        1   7.3T ST8000NM017B
nvme0n1    0 894.3G SAMSUNG MZ1L2960HCJR

En un VPS verás normalmente un disco vda (virtio-blk) o sda (virtio-scsi) con ROTA a 0 o 1 según lo que informe el hipervisor.

Consulta el programador de cada disco. El que está entre corchetes es el activo:

grep . /sys/block/*/queue/scheduler
/sys/block/nvme0n1/queue/scheduler:[none] mq-deadline
/sys/block/sda/queue/scheduler:[mq-deadline] none

Ubuntu 24.04 usa por defecto none en dispositivos con varias colas de hardware (NVMe, la mayoría de discos virtio) y mq-deadline en el resto. Solo aparecen los programadores cuyo módulo está cargado; bfq y kyber se cargan bajo demanda.

Paso 2: Conocer los programadores disponibles

Los programadores antiguos (noop, deadline, cfq) desaparecieron en el kernel 5.0 junto con la capa de bloques de una sola cola. Si una guía te pide escribir cfq o noop, no funcionará en Ubuntu 24.04. Los actuales son:

ProgramadorQué haceIndicado para
noneEnvía las peticiones en orden de llegada, sin reordenar. Mínimo uso de CPU.NVMe, SSD rápidos, discos virtuales y RAID por hardware con caché.
mq-deadlineAgrupa peticiones y asigna a cada una un plazo, dando prioridad a las lecturas.Discos mecánicos y SSD SATA con cargas de servidor.
bfqReparte el ancho de banda de forma justa entre procesos y prioriza la interactividad. Consume más CPU por petición.Discos lentos compartidos por muchos procesos, escritorios.
kyberAjusta la profundidad de las colas para cumplir un objetivo de latencia de lectura y escritura.Dispositivos rápidos donde importa la latencia de cola bajo carga mixta.

Para cargar los módulos de bfq y kyber y que aparezcan en la lista:

sudo modprobe bfq
sudo modprobe kyber-iosched
cat /sys/block/sda/queue/scheduler
[mq-deadline] kyber bfq none

Paso 3: Medir con fio

Cambiar de programador sin medir es adivinar. fio genera cargas de E/S reproducibles e informa de IOPS y percentiles de latencia:

sudo apt update
sudo apt install fio

Crea un archivo de trabajo en /etc/fio/ que simule una carga típica de base de datos: 70 % lecturas y 30 % escrituras aleatorias de 4 KB, saltándose la caché de página con direct=1 para medir el disco y no la RAM:

sudo mkdir -p /etc/fio
sudo nano /etc/fio/mixto.fio
[global]
ioengine=libaio
direct=1
bs=4k
size=4G
runtime=60
time_based=1
group_reporting=1
filename=/var/tmp/fio.test

[mixto]
rw=randrw
rwmixread=70
iodepth=32
numjobs=4

Cambia filename si el disco que quieres probar no contiene /var/tmp (por ejemplo, /srv/datos/fio.test para un disco montado en /srv/datos). Ejecuta la prueba con el programador actual:

sudo fio /etc/fio/mixto.fio

De toda la salida, fíjate en las líneas de IOPS y en los percentiles de latencia de lectura:

  read: IOPS=11.8k, BW=46.2MiB/s (48.4MB/s)(2771MiB/60001msec)
  write: IOPS=5071, BW=19.8MiB/s (20.8MB/s)(1189MiB/60001msec)
    clat percentiles (usec):
     |  1.00th=[  202],  5.00th=[  297], 10.00th=[  375], 20.00th=[  553],
     | 50.00th=[ 1336], 90.00th=[ 4555], 95.00th=[ 6456],
     | 99.00th=[12125], 99.50th=[15270], 99.90th=[24249]

Anota los IOPS y los percentiles 50 y 99. Estos valores son un ejemplo; los tuyos dependerán del disco.

Paso 4: Cambiar el programador en caliente y comparar

Escribe el nombre del programador en el archivo scheduler del disco. El cambio es inmediato, no requiere desmontar nada y se pierde al reiniciar:

echo bfq | sudo tee /sys/block/sda/queue/scheduler
cat /sys/block/sda/queue/scheduler
bfq
mq-deadline kyber [bfq] none

Repite la prueba del paso 3 con cada candidato (none, mq-deadline, bfq, kyber) y compara las cifras. Como orientación, en pruebas así suelen salir estos patrones:

  • NVMe: none da los mejores IOPS. mq-deadline y kyber quedan cerca; bfq pierde claramente por el coste de CPU.
  • Disco mecánico: mq-deadline y bfq superan a none porque ordenan las peticiones y reducen los movimientos del cabezal. bfq mejora la latencia cuando varios procesos compiten por el disco.
  • Disco virtual: las diferencias suelen ser pequeñas y none gana o empata.

Si dos programadores dan cifras similares, quédate con el más simple.

Ajustar los parámetros de mq-deadline

Cada programador expone parámetros en /sys/block/<disco>/queue/iosched/. Para mq-deadline, los más útiles son read_expire y write_expire (plazo máximo en milisegundos antes de atender una lectura o escritura, 500 y 5000 por defecto) y writes_starved (cuántas tandas de lecturas se atienden antes de dar paso a las escrituras pendientes, 2 por defecto):

echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
grep . /sys/block/sda/queue/iosched/{read_expire,write_expire,writes_starved,fifo_batch}
/sys/block/sda/queue/iosched/read_expire:500
/sys/block/sda/queue/iosched/write_expire:5000
/sys/block/sda/queue/iosched/writes_starved:2
/sys/block/sda/queue/iosched/fifo_batch:16

Por ejemplo, para reducir la latencia de lectura en un disco mecánico con una base de datos, prueba a bajar read_expire:

echo 250 | sudo tee /sys/block/sda/queue/iosched/read_expire

Vuelve a medir con fio. Si el percentil 99 de lectura no mejora, restaura el valor por defecto.

Revisar la lectura anticipada

Independientemente del programador, read_ahead_kb controla cuántos datos lee el kernel por adelantado en accesos secuenciales. El valor por defecto es 128 KB:

cat /sys/block/sda/queue/read_ahead_kb

Subirlo (por ejemplo a 1024) ayuda en discos mecánicos que sirven archivos grandes de forma secuencial, como vídeo o copias de seguridad. En cargas aleatorias de base de datos no aporta nada o empeora, porque lee datos que nadie va a usar.

Paso 5: Hacer el cambio persistente con udev

El antiguo parámetro de arranque elevator= ya no tiene efecto en kernels con la capa de bloques multicola, así que la forma correcta de fijar un programador es una regla de udev. Crea el archivo:

sudo nano /etc/udev/rules.d/60-ioscheduler.rules

Este ejemplo asigna mq-deadline a los discos mecánicos y none a los NVMe y demás discos no rotacionales. Adáptalo al resultado de tus pruebas:

# Discos mecánicos
ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline"

# SSD SATA y discos virtuales no rotacionales
ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="sd[a-z]*|vd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none"

# NVMe
ACTION=="add|change", SUBSYSTEM=="block", ENV{DEVTYPE}=="disk", KERNEL=="nvme[0-9]*n[0-9]*", ATTR{queue/scheduler}="none"

La condición ENV{DEVTYPE}=="disk" excluye las particiones, que no tienen programador propio. Si elegiste bfq o kyber, añade también el módulo a /etc/modules-load.d/ para que esté cargado cuando se aplique la regla:

echo bfq | sudo tee /etc/modules-load.d/bfq.conf

Recarga las reglas y aplícalas a los discos existentes sin reiniciar:

sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=block --action=change

Comprueba el resultado:

grep . /sys/block/*/queue/scheduler
/sys/block/nvme0n1/queue/scheduler:[none] mq-deadline kyber bfq
/sys/block/sda/queue/scheduler:[mq-deadline] kyber bfq none

Para confirmar que la regla sobrevive a un reinicio, reinicia el servidor en una ventana de mantenimiento y vuelve a ejecutar el mismo comando.

Los ajustes de iosched/ como read_expire se pueden persistir de la misma forma, añadiendo por ejemplo ATTR{queue/iosched/read_expire}="250" al final de la regla del disco correspondiente, siempre después de la asignación del programador.

Paso 6: Vigilar el disco en producción

Las pruebas sintéticas no sustituyen a la carga real. iostat, del paquete sysstat, muestra la latencia media y la ocupación de cada disco:

sudo apt install sysstat
iostat -dx 5

La salida real tiene más columnas; estas son las relevantes:

Device   r/s     w/s   rkB/s   wkB/s  r_await  w_await  aqu-sz  %util
nvme0n1  812.4  305.2  3249.6  4880.0    0.18     0.05    0.17   14.20
sda       96.0   41.8  6144.0  2140.0    7.95    12.40    1.28   78.60
  • r_await y w_await: tiempo medio en milisegundos de cada lectura y escritura, incluyendo el tiempo en cola. Es la cifra que el programador influye directamente.
  • aqu-sz: número medio de peticiones en cola.
  • %util: porcentaje de tiempo con alguna petición en curso. En discos mecánicos, valores cercanos al 100 % indican saturación; en NVMe no es fiable porque atiende muchas peticiones en paralelo.

Compara estas cifras antes y después de cambiar el programador, en horas de carga similares.

Solución de problemas

tee: /sys/block/sda/queue/scheduler: Invalid argument: el nombre no existe o su módulo no está cargado. Ejecuta sudo modprobe bfq o sudo modprobe kyber-iosched y vuelve a intentarlo. Recuerda que cfq, noop y deadline ya no existen.

La regla de udev no se aplica tras reiniciar: comprueba la sintaxis con udevadm test /sys/block/sda 2>&1 | grep -i scheduler y revisa que el nombre del disco coincide con el patrón KERNEL==. En discos virtio el nombre es vda, no sda.

Los resultados de fio varían mucho entre ejecuciones: aumenta runtime a 120 segundos y repite cada prueba tres veces. En un VPS, otras máquinas del mismo host también afectan a las cifras.

Conclusión

Has identificado el programador de E/S de cada disco, comparado los disponibles con una carga reproducible de fio y fijado la elección con una regla de udev, sin depender del obsoleto parámetro elevator=. En la mayoría de servidores modernos la respuesta será none para NVMe y discos virtuales y mq-deadline para discos mecánicos, pero ahora tienes datos para confirmarlo.

Como siguientes pasos, puedes borrar el archivo de pruebas con sudo rm /var/tmp/fio.test, ajustar las opciones de montaje del sistema de archivos (noatime) o revisar los parámetros vm.dirty_* del kernel para controlar cómo se vuelcan las escrituras al disco.