Algunas cargas de trabajo (trading, servidores de juego, VoIP, control industrial) no se miden por la latencia media sino por la peor: un pico de varios milisegundos cada pocos segundos basta para estropearlas. Esos picos suelen venir del propio sistema operativo: cambios de frecuencia de la CPU, interrupciones, el tick del planificador u otros procesos que se cuelan en el mismo núcleo. En este tutorial medirás la latencia de planificación de Ubuntu 24.04 con cyclictest, aplicarás un perfil de tuned, reservarás núcleos de CPU solo para tu aplicación, sacarás de ellos las interrupciones y ejecutarás la aplicación con prioridad de tiempo real, comprobando la mejora después de cada cambio.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS y un usuario no root con privilegios sudo.
  • Al menos 4 CPU, idealmente 8 o más: vas a reservar varias para la aplicación y dejar el resto para el sistema.
  • Acceso a la consola del servidor por si un parámetro de arranque impide el acceso por SSH.
  • Una aplicación que quieras optimizar. En los ejemplos se llama /opt/miapp/bin/miapp; sustitúyela por la tuya.

Los ejemplos suponen 8 CPU (0 a 7), reservando las CPU 4 a 7 para la aplicación. Adapta los rangos a tu hardware.

Paso 1: Instalar las herramientas de medición

Instala rt-tests (que incluye cyclictest), tuned para aplicar perfiles de sistema y numactl para ver la topología de memoria:

sudo apt update
sudo apt install rt-tests tuned numactl

Revisa la topología de la CPU. Te interesa saber qué CPU lógicas son hilos del mismo núcleo físico (Hyper-Threading), porque conviene reservar núcleos completos:

lscpu -e=CPU,CORE,SOCKET,NODE
CPU CORE SOCKET NODE
  0    0      0    0
  1    1      0    0
  2    2      0    0
  3    3      0    0
  4    0      0    0
  5    1      0    0
  6    2      0    0
  7    3      0    0

En este ejemplo las CPU 0 y 4 son el mismo núcleo físico. Si reservas la 4 pero dejas la 0 para el sistema, la carga del sistema seguirá afectando a tu aplicación a través del núcleo compartido. En ese caso, reserva pares completos (por ejemplo 2, 3, 6 y 7) o desactiva Hyper-Threading en la BIOS.

Paso 2: Medir la latencia de partida

Antes de cambiar nada, mide cuánto tarda el sistema en despertar un hilo de tiempo real. cyclictest crea un hilo por CPU, le pide dormir 200 microsegundos una y otra vez y registra cuánto se retrasa cada despertar:

sudo cyclictest -m -S -p 80 -i 200 -D 5m -q

Las opciones hacen lo siguiente: -m bloquea la memoria en RAM, -S lanza un hilo fijado a cada CPU, -p 80 usa prioridad de tiempo real 80, -i 200 fija el intervalo en microsegundos, -D 5m hace que la prueba dure cinco minutos y -q muestra solo el resumen final. Genera una carga parecida a la real mientras se ejecuta, porque en reposo casi cualquier sistema da buenos números.

# /dev/cpu_dma_latency set to 0us
T: 0 ( 2310) P:80 I:200 C:1499874 Min:      3 Act:    6 Avg:    5 Max:     412
T: 1 ( 2311) P:80 I:200 C:1499851 Min:      3 Act:    5 Avg:    5 Max:     389
...
T: 7 ( 2317) P:80 I:200 C:1499803 Min:      3 Act:    7 Avg:    6 Max:     531

Los valores están en microsegundos. Fíjate sobre todo en Max: es la peor latencia observada y la que quieres reducir. Guarda esta salida para compararla al final.

Paso 3: Aplicar el perfil network-latency de tuned

tuned agrupa en perfiles los ajustes típicos de rendimiento. El perfil network-latency incluye latency-performance (gobernador de CPU en performance e impide estados de reposo profundos mediante /dev/cpu_dma_latency) y añade ajustes de red y memoria para baja latencia, como el sondeo activo de sockets y la desactivación del balanceo automático NUMA.

Activa el servicio y el perfil:

sudo systemctl enable --now tuned
sudo tuned-adm profile network-latency

Comprueba que el perfil está activo y aplicado correctamente:

tuned-adm active
sudo tuned-adm verify
Current active profile: network-latency
Verification succeeded, current system settings match the preset profile.

Comprueba algunos de los valores que ha cambiado:

sysctl kernel.numa_balancing net.core.busy_poll net.core.busy_read
kernel.numa_balancing = 0
net.core.busy_poll = 50
net.core.busy_read = 50

En un VPS es normal que tuned no pueda cambiar el gobernador de CPU, porque la máquina virtual no controla la frecuencia real del procesador.

Paso 4: Aislar núcleos de CPU para la aplicación

Por defecto el planificador reparte cualquier proceso en cualquier CPU, y cada CPU recibe un tick periódico del temporizador y trabajo del subsistema RCU. Tres parámetros de arranque del kernel eliminan esas fuentes de ruido en las CPU reservadas:

ParámetroEfecto
isolcpus=managed_irq,domain,4-7El planificador no coloca procesos en las CPU 4-7 salvo que se fijen de forma explícita, y las interrupciones gestionadas por el kernel las evitan
nohz_full=4-7Detiene el tick del temporizador en esas CPU cuando solo ejecutan una tarea
rcu_nocbs=4-7Mueve los callbacks de RCU a las CPU del sistema

Además, el kernel genérico de Ubuntu 24.04 admite preempt=full, que hace el kernel totalmente expropiable y reduce el tiempo que una tarea de alta prioridad espera a que el kernel termine lo que estaba haciendo.

Edita la configuración de GRUB:

sudo nano /etc/default/grub

Añade los parámetros a GRUB_CMDLINE_LINUX_DEFAULT, conservando los que ya hubiera:

GRUB_CMDLINE_LINUX_DEFAULT="isolcpus=managed_irq,domain,4-7 nohz_full=4-7 rcu_nocbs=4-7 preempt=full"

Regenera la configuración y reinicia:

sudo update-grub
sudo reboot

Tras el reinicio, comprueba que el kernel ha aplicado el aislamiento:

cat /sys/devices/system/cpu/isolated /sys/devices/system/cpu/nohz_full
4-7
4-7

Y que el modo de expropiación es full:

sudo dmesg | grep -i 'dynamic preempt'
[    0.051234] Dynamic Preempt: full

Paso 5: Sacar las interrupciones de los núcleos aislados

Las interrupciones de la tarjeta de red y del disco interrumpen lo que esté ejecutando la CPU que las atiende. irqbalance reparte las interrupciones entre CPU; en versiones recientes ya evita las CPU de isolcpus y nohz_full, pero conviene prohibirlas de forma explícita.

Edita su configuración:

sudo nano /etc/default/irqbalance

Añade esta línea:

IRQBALANCE_BANNED_CPULIST=4-7

Reinicia el servicio:

sudo systemctl restart irqbalance

Comprueba a qué CPU va cada interrupción de la red. Primero localiza sus números de IRQ en /proc/interrupts (en un VPS suelen llamarse virtio0-input.0 o similar):

grep -E 'virtio|eth|ens|enp|mlx' /proc/interrupts | awk '{print $1, $NF}'
24: virtio0-config
25: virtio0-input.0
26: virtio0-output.0

Consulta la afinidad efectiva de una de ellas:

cat /proc/irq/25/effective_affinity_list
1

Ninguna interrupción de dispositivo debería apuntar a las CPU 4 a 7. En la tabla de /proc/interrupts, los contadores de esas columnas deberían quedarse prácticamente quietos salvo en líneas como LOC o RES, que son internas del kernel.

Paso 6: Ejecutar la aplicación en los núcleos reservados con prioridad de tiempo real

Con las CPU aisladas, solo se ejecuta en ellas lo que fijes expresamente. La forma más fiable de hacerlo en producción es un servicio de systemd, que aplica afinidad, política de planificación y límites cada vez que arranca la aplicación.

Crea la unidad:

sudo nano /etc/systemd/system/miapp.service

Añade este contenido, ajustando la ruta y el usuario:

[Unit]
Description=Aplicación de baja latencia
After=network-online.target
Wants=network-online.target

[Service]
User=miapp
ExecStart=/opt/miapp/bin/miapp
CPUAffinity=4-7
CPUSchedulingPolicy=fifo
CPUSchedulingPriority=80
LimitMEMLOCK=infinity
Restart=on-failure

[Install]
WantedBy=multi-user.target

CPUSchedulingPolicy=fifo usa SCHED_FIFO: el hilo no cede la CPU hasta que se bloquea o aparece otro de mayor prioridad. LimitMEMLOCK=infinity permite a la aplicación llamar a mlockall() para que su memoria no se pagine a disco, lo que evitaría fallos de página en mitad de una operación.

Carga la unidad y arranca el servicio:

sudo systemctl daemon-reload
sudo systemctl enable --now miapp

Comprueba en qué CPU se ejecutan los hilos y con qué política:

ps -L -o tid,psr,cls,rtprio,comm -p "$(systemctl show -p MainPID --value miapp)"
    TID PSR CLS RTPRIO COMMAND
   1843   4  FF     80 miapp
   1851   5  FF     80 miapp

PSR es la CPU donde se está ejecutando el hilo y FF indica SCHED_FIFO. Si tu aplicación tiene varios hilos con funciones distintas, lo ideal es que ella misma fije cada hilo crítico a una CPU concreta (con pthread_setaffinity_np()) en lugar de dejar que el planificador los mueva entre la 4 y la 7.

Para pruebas rápidas sin crear un servicio, puedes lanzar un proceso con la misma configuración usando taskset y chrt:

sudo taskset -c 4-7 chrt -f 80 /opt/miapp/bin/miapp

Paso 7: Reducir la latencia de la tarjeta de red (opcional)

Muchas tarjetas agrupan varias interrupciones de recepción para ahorrar CPU (interrupt coalescing), lo que añade decenas de microsegundos a cada paquete. Consulta la configuración actual, sustituyendo eth0 por el nombre de tu interfaz (ip -br link lo muestra):

sudo ethtool -c eth0

Desactiva el ajuste adaptativo y la espera en recepción:

sudo ethtool -C eth0 adaptive-rx off rx-usecs 0

Si el comando responde Operation not supported, el driver no permite cambiar este valor, algo habitual en tarjetas virtuales. Si funciona, el cambio se pierde al reiniciar; para hacerlo permanente, crea una unidad de systemd:

sudo nano /etc/systemd/system/nic-coalescing.service

Añade este contenido:

[Unit]
Description=Desactivar coalescing de interrupciones en eth0
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -C eth0 adaptive-rx off rx-usecs 0

[Install]
WantedBy=multi-user.target

Carga la unidad y actívala:

sudo systemctl daemon-reload
sudo systemctl enable --now nic-coalescing

En la aplicación, desactiva el algoritmo de Nagle en los sockets TCP con la opción TCP_NODELAY, que evita que el kernel retenga mensajes pequeños esperando a juntar más datos.

Paso 8: Medir de nuevo en los núcleos aislados

Detén tu aplicación para que no compita con la prueba y lanza cyclictest solo en las CPU reservadas, con un hilo por CPU:

sudo systemctl stop miapp
sudo cyclictest -m -p 80 -i 200 -D 5m -q -t 4 -a 4-7
# /dev/cpu_dma_latency set to 0us
T: 0 ( 2604) P:80 I:200 C:1499990 Min:      2 Act:    3 Avg:    3 Max:     14
T: 1 ( 2605) P:80 I:200 C:1499989 Min:      2 Act:    3 Avg:    3 Max:     12
T: 2 ( 2606) P:80 I:200 C:1499987 Min:      2 Act:    3 Avg:    3 Max:     15
T: 3 ( 2607) P:80 I:200 C:1499986 Min:      2 Act:    2 Avg:    3 Max:     11

Compara el Max con el del paso 2, con la misma carga en el resto del sistema. En un servidor dedicado bien ajustado es normal pasar de cientos de microsegundos a valores de un solo dígito o decenas bajas. En un VPS la mejora suele ser menor y los picos que quedan vienen del host.

Cuando termines, vuelve a arrancar la aplicación:

sudo systemctl start miapp

Solución de problemas

  • Max sigue alto en un VPS. Comprueba el tiempo robado por el hipervisor con vmstat 1 (columna st). Si no es cero de forma sostenida, la latencia viene del host y solo la resuelve un servidor dedicado.
  • Picos periódicos cada pocos segundos. Busca procesos que se ejecuten en las CPU aisladas con ps -eLo psr,comm | awk '$1>=4 && $1<=7'. Además de tu aplicación, solo deberían aparecer hilos del kernel por CPU como ksoftirqd/4, migration/4 o cpuhp/4.
  • El sistema va lento tras el aislamiento. Todo lo demás se ejecuta ahora en las CPU 0 a 3. Si se quedan cortas, reserva menos CPU para la aplicación.
  • tuned-adm verify falla. Revisa /var/log/tuned/tuned.log: suele indicar qué ajuste no se ha podido aplicar, normalmente el gobernador de CPU en máquinas virtuales.
  • La aplicación no arranca con SCHED_FIFO. Revisa journalctl -u miapp. Si el error es de permisos al fijar la política, asegúrate de que la usas desde la unidad de systemd y no desde el propio programa sin privilegios.

Conclusión

Has medido la latencia de planificación de Ubuntu 24.04, has aplicado el perfil network-latency, has reservado núcleos para la aplicación sin tick ni interrupciones y la ejecutas con prioridad de tiempo real, comprobando la mejora con cyclictest. Como siguientes pasos, reserva huge pages para reducir fallos de TLB, fija los hilos críticos de la aplicación a CPU concretas desde el código y, si tu carga es de red, valora el bypass del kernel con DPDK.