En un servidor con varios sockets (o con procesadores que dividen internamente la memoria, como algunos AMD EPYC), cada grupo de núcleos tiene su propia memoria local. Ese grupo es un nodo NUMA (Non-Uniform Memory Access): leer memoria del propio nodo es rápido, y leerla de otro nodo pasa por el enlace entre sockets y es más lento. En este tutorial detectarás la topología NUMA de un servidor con Ubuntu 24.04, medirás la diferencia entre memoria local y remota, y fijarás un servicio a un nodo concreto con numactl y con las opciones nativas de systemd.
Requisitos previos
- Un servidor dedicado con Ubuntu 24.04 LTS y dos o más nodos NUMA, por ejemplo un servidor bare metal de doble socket de CubePath.
- Un usuario no root con privilegios
sudo. - Un servicio que quieras fijar. El ejemplo usa Redis, pero el procedimiento es el mismo para cualquier unidad de systemd (MySQL, PostgreSQL, una aplicación Java o Go).
Notala mayoría de VPS exponen un único nodo NUMA a la máquina virtual, porque el hipervisor ya coloca sus vCPU y su memoria. Si el paso 1 muestra
available: 1 nodes, no hay nada que fijar y esta guía no aporta mejora.
Paso 1: Detectar la topología NUMA
Instala numactl, que incluye también la herramienta numastat:
sudo apt update
sudo apt install numactl
Muestra los nodos, sus CPU y su memoria:
numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 64232 MB
node 0 free: 58110 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 64507 MB
node 1 free: 60943 MB
node distances:
node 0 1
0: 10 21
1: 21 10
La tabla node distances indica el coste relativo de acceso: 10 es el acceso local y 21 significa que ir al otro nodo cuesta aproximadamente el doble. Fíjate en que las CPU de cada nodo no son siempre consecutivas: aquí los hilos de hyperthreading (16 a 31) se reparten entre ambos nodos, por lo que conviene fijar por nodo y no por números de CPU escritos a mano.
lscpu ofrece el mismo resumen en formato compacto:
lscpu | grep -i numa
NUMA node(s): 2
NUMA node0 CPU(s): 0-7,16-23
NUMA node1 CPU(s): 8-15,24-31
Paso 2: Medir el coste de la memoria remota
Antes de fijar nada, comprueba cuánto pierdes con la memoria remota en este servidor. sysbench incluye una prueba de ancho de banda de memoria:
sudo apt install sysbench
Ejecuta la prueba con las CPU y la memoria en el mismo nodo:
numactl --cpunodebind=0 --membind=0 sysbench memory --memory-block-size=1M --memory-total-size=50G --threads=8 run | grep transferred
51200.00 MiB transferred (21430.12 MiB/sec)
Ahora repítela con las CPU en el nodo 0 y la memoria en el nodo 1:
numactl --cpunodebind=0 --membind=1 sysbench memory --memory-block-size=1M --memory-total-size=50G --threads=8 run | grep transferred
51200.00 MiB transferred (12877.54 MiB/sec)
La diferencia exacta depende del procesador y de la configuración de la BIOS, pero en servidores de doble socket es habitual ver entre un 30 % y un 50 % menos de ancho de banda en remoto, además de mayor latencia. Esa es la penalización que evitas al fijar un servicio a un nodo.
Paso 3: Entender las políticas de numactl
numactl lanza un programa con una política de CPU y memoria. Las opciones que usarás son:
| Opción | Efecto |
|---|---|
--cpunodebind=N | El proceso solo se ejecuta en las CPU del nodo N. |
--membind=N | La memoria solo se asigna en el nodo N. Si se agota, el proceso sufre el OOM killer en lugar de usar el otro nodo. |
--preferred=N | Asigna en el nodo N mientras haya memoria libre y después usa el resto. |
--interleave=all | Reparte las páginas entre todos los nodos por turnos. Útil para procesos que ocupan más memoria de la que tiene un nodo. |
--localalloc | Asigna siempre en el nodo de la CPU que pide la memoria (el comportamiento por defecto del kernel). |
Puedes comprobar la política resultante lanzando el propio numactl --show con esas opciones:
numactl --cpunodebind=1 --preferred=1 numactl --show
policy: preferred
preferred node: 1
physcpubind: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
cpubind: 1
nodebind: 1
membind: 0 1
Una regla práctica para elegir:
- Servicio que cabe en un nodo (una instancia de Redis, un worker de aplicación):
--cpunodebindmás--preferreden el mismo nodo. Obtienes memoria local sin riesgo de OOM si el nodo se llena. - Servicio que necesita más memoria de la que tiene un nodo (una base de datos grande con un buffer pool enorme):
--interleave=all. No evita el acceso remoto, pero reparte el ancho de banda y evita que un nodo se quede sin memoria mientras el otro está libre. - Varias instancias del mismo servicio: una instancia por nodo, cada una fijada a su nodo.
Paso 4: Fijar un servicio con systemd
Para servicios gestionados por systemd no hace falta envolver ExecStart con numactl. systemd 255, incluido en Ubuntu 24.04, admite directamente CPUAffinity=, NUMAPolicy= y NUMAMask=. Instala Redis como ejemplo:
sudo apt install redis-server
Crea un archivo de override para la unidad. systemctl edit abre un editor y guarda el resultado en /etc/systemd/system/redis-server.service.d/override.conf:
sudo systemctl edit redis-server
Añade este bloque entre las líneas de comentario indicadas por el editor. Usa las CPU del nodo 0 que mostró lscpu en el paso 1:
[Service]
CPUAffinity=0-7 16-23
NUMAPolicy=preferred
NUMAMask=0
NUMAPolicy acepta default, preferred, bind, interleave y local, con el mismo significado que las opciones de numactl. NUMAMask indica los nodos a los que se aplica.
Guarda, cierra el editor y reinicia el servicio para que arranque con la nueva política:
sudo systemctl restart redis-server
Comprueba la afinidad de CPU del proceso:
taskset -cp "$(pidof redis-server)"
pid 4182's current affinity list: 0-7,16-23
Paso 5: Verificar dónde está la memoria
numastat -p muestra en qué nodo reside la memoria de un proceso. Llena Redis con datos de prueba para que la memoria crezca. redis-benchmark, incluido en el paquete redis-tools, escribe hasta un millón de claves aleatorias de 100 bytes:
redis-benchmark -t set -n 1000000 -r 1000000 -d 100 -q
SET: 98231.34 requests per second, p50=0.263 msec
Notasi el servidor no es de pruebas, borra después esas claves (todas empiezan por
key:) o usa una base de datos de Redis aparte con la opción--dbnum.
Ahora consulta la distribución por nodo:
numastat -p redis-server
Per-node process memory usage (in MBs) for PID 4182 (redis-server)
Node 0 Node 1 Total
--------------- --------------- ---------------
Huge 0.00 0.00 0.00
Heap 112.45 0.00 112.45
Stack 0.03 0.00 0.03
Private 6.61 0.08 6.69
---------------- --------------- --------------- ---------------
Total 119.09 0.08 119.17
Casi toda la memoria está en el nodo 0, que es lo que buscabas. Para ver los contadores globales del sistema, ejecuta numastat sin argumentos:
numastat
node0 node1
numa_hit 8123456 7012345
numa_miss 1520 2210
numa_foreign 2210 1520
interleave_hit 21034 20988
local_node 8120001 7009876
other_node 4975 4679
numa_miss y other_node cuentan asignaciones que acabaron en un nodo distinto del deseado. Si crecen rápido respecto a numa_hit, algún proceso está usando memoria remota con frecuencia.
Paso 6: Revisar el balanceo automático del kernel
El kernel incluye balanceo NUMA automático: mueve páginas y tareas hacia el nodo donde más se usan. Está activo por defecto:
sysctl kernel.numa_balancing
kernel.numa_balancing = 1
Para la mayoría de cargas conviene dejarlo activado, porque ayuda a los procesos que no has fijado. Si has fijado a mano todos los servicios importantes y ves consumo de CPU en migraciones (líneas numa_pages_migrated en /proc/vmstat que crecen sin parar), puedes probar a desactivarlo con un archivo en /etc/sysctl.d/ y medir de nuevo antes de dejarlo así.
Deja también vm.zone_reclaim_mode en su valor por defecto, 0. Con otros valores el kernel prefiere liberar caché del nodo local antes que usar memoria libre del otro nodo, lo que suele perjudicar a servidores de archivos y bases de datos.
Solución de problemas
numactl --hardware muestra No NUMA available on this system: el kernel no ve nodos NUMA. En un VPS es lo normal. En un servidor físico, revisa en la BIOS opciones como "Node Interleaving" (debe estar desactivada para exponer NUMA) o "NUMA nodes per socket" en AMD.
El servicio muere con Out of memory tras fijarlo: usaste NUMAPolicy=bind (o --membind) y el nodo se quedó sin memoria. Cambia a preferred o a interleave.
taskset sigue mostrando todas las CPU: el override no se aplicó. Comprueba con systemctl cat redis-server que aparece el bloque [Service] con tus opciones y reinicia el servicio (un reload no cambia la afinidad).
Conclusión
Has identificado los nodos NUMA del servidor, medido la penalización de la memoria remota y fijado un servicio a un nodo con opciones nativas de systemd, verificando la ubicación de su memoria con numastat. El mismo patrón sirve para repartir varias instancias de un servicio, una por nodo.
Como siguientes pasos, puedes combinar la afinidad NUMA con el ajuste del gobernador de frecuencia de la CPU, revisar la configuración de páginas enormes de tu base de datos o fijar también las interrupciones de la tarjeta de red al mismo nodo que la aplicación que más tráfico recibe.
