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).

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ónEfecto
--cpunodebind=NEl proceso solo se ejecuta en las CPU del nodo N.
--membind=NLa 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=NAsigna en el nodo N mientras haya memoria libre y después usa el resto.
--interleave=allReparte las páginas entre todos los nodos por turnos. Útil para procesos que ocupan más memoria de la que tiene un nodo.
--localallocAsigna 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): --cpunodebind más --preferred en 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

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.