Compraste una caja dual-socket reluciente porque “el doble de CPUs” suena a “el doble de rendimiento”. Luego tu p95 de la base de datos empeoró, tu stack de almacenamiento empezó a tartamudear y el equipo de rendimiento apareció con gráficos y una decepción contenida.
Esto es NUMA: Non-Uniform Memory Access. No es un bug. Es la factura que pagas por pretender que un servidor es un gran bloque feliz de CPUs y RAM.
Qué es realmente NUMA (y qué no es)
NUMA significa que la máquina está físicamente compuesta por múltiples “dominios de localidad” (nodos NUMA). Cada nodo es un conjunto de núcleos de CPU y memoria que están cerca entre sí. Acceder a memoria local es más rápido (más o menos). Acceder a la memoria conectada al otro zócalo es más lento (más o menos). Ese “más o menos” importa porque no es solo latencia; también es contención en la interconexión entre sockets.
En un servidor x86 moderno dual-socket típico:
- Cada zócalo tiene sus propios controladores de memoria y canales. Los módulos DRAM están cableados a ese zócalo.
- Los zócalos están conectados por una interconexión (Intel UPI, AMD Infinity Fabric u otra similar).
- Los dispositivos PCIe también están físicamente conectados a un zócalo mediante root complexes. Una NIC o un dispositivo NVMe está “más cerca” de un zócalo que de otro.
Así que NUMA no es “una opción de ajuste”. Es una topología. Ignorarlo significa dejar que el OS juegue a ser el agente de tráfico mientras tú generas atascos activamente.
Además: NUMA no es automáticamente un desastre. NUMA está bien cuando la carga es compatible con NUMA, el scheduler tiene una oportunidad razonable y no lo saboteas con un mal pinning o colocación de memoria.
Una cita para recordar: “La esperanza no es una estrategia.” — Gral. Gordon R. Sullivan. En el mundo NUMA, “esperar que el kernel lo resuelva” es esperanza.
Por qué doble zócalo no es “dos veces más rápido”
Porque el cuello de botella se desplazó. Añadir un segundo zócalo aumenta la capacidad de cómputo pico y la memoria total, pero también incrementa las formas de volverse lento. No estás duplicando un recurso único; estás cosiendo dos ordenadores y diciéndole a Linux que finja que son uno solo.
1) La localidad de la memoria se convierte en una dimensión de rendimiento
En una máquina de un solo zócalo, la memoria “local” de cada núcleo es prácticamente lo mismo. En una caja dual-socket, la memoria tiene una dirección y esa dirección tiene un hogar. Cuando un hilo en el socket 0 lee y escribe frecuentemente páginas asignadas del nodo 1, cada fallo de caché se convierte en un viaje entre sockets. El acceso entre sockets no es gratis; es una vía de peaje en hora punta.
2) El tráfico de coherencia de caché aumenta
Multi-socket requiere coherencia entre sockets. Si tienes estructuras de datos compartidas con muchas escrituras (locks, colas, contadores calientes, metadatos del allocator), obtienes ping-pong entre sockets. A veces la CPU está “ocupada” pero el trabajo útil por ciclo se desploma.
3) La localidad de PCIe importa más de lo que crees
Tu NIC está conectada a un socket específico. Tu HBA NVMe está conectado a un socket específico. Si la carga se programa principalmente en el otro socket, has inventado un salto interno extra para cada paquete o finalización de IO. En sistemas con alta IOPS o alta tasa de paquetes, ese salto se convierte en un impuesto.
4) Puedes empeorarlo fácilmente con “optimizaciones”
Pinnear hilos suena disciplinado hasta que atas CPUs pero no la memoria, o pinneas las interrupciones al socket equivocado, o forzas todo al nodo 0 porque “funcionó en staging”. La mala configuración NUMA es el raro problema donde hacer algo suele ser peor que no hacer nada.
Broma #1 (breve, relevante): Los servidores dual-socket son como espacios de oficina abiertos: ganaste “capacidad”, pero ahora todo lo importante implica caminar al otro lado de la sala.
Hechos e historia que puedes usar en el trabajo
Estos no son datos para una noche de trivia. Son los que usas para frenar una mala decisión de compra o ganar una discusión con alguien que maneja una hoja de cálculo.
- NUMA es anterior a tu nube. Diseños comerciales NUMA existían hace décadas en sistemas de gama alta; la idea es más antigua que la mayoría de las herramientas modernas de rendimiento.
- SMP dejó de escalar “gratis”. El acceso uniforme a memoria era más simple, pero tocó límites físicos y eléctricos al aumentar los conteos de núcleos mientras el ancho de banda de memoria no crecía linealmente.
- Los controladores de memoria integrados lo cambiaron todo. Mover los controladores de memoria al CPU mejoró la latencia, pero también convirtió en inevitable la pregunta “qué CPU posee la memoria”.
- Los enlaces entre sockets han evolucionado, pero siguen siendo más lentos que la DRAM local. UPI/Infinity Fabric son rápidos, pero no reemplazan a los canales locales. Además transportan tráfico de coherencia.
- NUMA no es solo memoria. Linux usa la misma topología para pensar sobre dispositivos PCIe, interrupciones y dominios de scheduling. La localidad es una propiedad de toda la máquina.
- La virtualización no eliminó NUMA; la hizo más fácil de ocultar. Los hypervisors pueden exponer NUMA virtual, pero también puedes construir accidentalmente una VM que abarque sockets y luego preguntarte por qué tiene jitter.
- Transparent Huge Pages interactúa con NUMA. THP puede reducir la sobrecarga de la TLB, pero también encarecer la colocación y migración de páginas cuando el sistema está bajo presión.
- Las decisiones de colocación en el arranque importan. Muchos allocators y servicios asignan mucha memoria al inicio; “dónde cae” puede determinar el rendimiento por horas.
- Los stacks de almacenamiento son sensibles a NUMA con alto throughput. Las colas NVMe, el procesamiento en softirq y los bucles de sondeo en espacio de usuario aman la localidad; cruzar sockets añade jitter y reduce el techo.
Cómo falla en producción: modos de fallo
Los problemas NUMA no suelen parecer “problema NUMA”. Parecen:
- p95 y p99 de latencia aumentan mientras el throughput promedio parece “bien”.
- CPU alta pero IPC bajo y la máquina se siente como si fuera a través del lodo.
- Un socket está ocupado, el otro está ocioso porque el scheduler hizo lo que pediste, no lo que querías.
- El acceso a memoria remota sube y ahora pagas cruzar sockets por cada fallo de caché.
- Desequilibrio de IRQ hace que el procesamiento de red o de finalización NVMe se acumule en los cores “equivocados”.
- Rendimiento inestable bajo carga porque la migración de páginas y reclaim se activan cuando la memoria está desequilibrada entre nodos.
Los problemas de rendimiento NUMA a menudo son multiplicativos. El acceso remoto a memoria añade latencia; eso aumenta los tiempos de retención de locks; eso aumenta la contención; eso incrementa los cambios de contexto; eso aumenta los fallos de caché; eso aumenta el acceso remoto. Esa espiral es por qué “ayer estaba bien” es una apertura común.
Guion de diagnóstico rápido (primero/segundo/tercero)
Cuando el sistema está lento y sospechas NUMA, no empieces con benchmarks heroicos. Empieza con topología y colocación. Estás tratando de responder una pregunta: ¿están las CPUs, la memoria y las rutas de IO en el mismo nodo para el trabajo caliente?
Primero: confirma la topología y si estás abarcando sockets
- ¿Cuántos nodos NUMA existen?
- ¿Qué CPUs pertenecen a cada nodo?
- ¿Tu carga está pinneada o limitada por cgroups/cpuset?
Segundo: revisa la localidad de memoria y el acceso remoto
- ¿La mayor parte de la memoria está asignada en el nodo 0 mientras los hilos corren en el nodo 1 (o al revés)?
- ¿Los contadores NUMA “miss” y “foreign” están subiendo?
- ¿El kernel está migrando páginas mucho?
Tercero: revisa la localidad de PCIe e interrupciones
- ¿Dónde está conectada la NIC/NVMe (nodo NUMA)?
- ¿Sus interrupciones aterrizan en CPUs del mismo nodo?
- ¿Las colas están distribuidas sensatamente entre cores cercanos al dispositivo?
Si haces esos tres pasos, encontrarás la causa de una gran fracción de regresiones “misteriosas” en dual-socket en menos de 20 minutos. No todas. Las suficientes para salvar tu fin de semana.
Manos a la obra: tareas prácticas NUMA con comandos
Estas son las tareas que realmente ejecuto cuando alguien dice “los nuevos servidores dual-socket son más lentos”. Cada tarea incluye el comando, salida de ejemplo, qué significa y la decisión que tomas a partir de ello.
Task 1: Ver nodos NUMA, mapeo de CPU y tamaño de memoria
cr0x@server:~$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 257728 MB
node 0 free: 18240 MB
node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 1 size: 257728 MB
node 1 free: 214912 MB
node distances:
node 0 1
0: 10 21
1: 21 10
Qué significa: Dos nodos. La matriz de distancias muestra que el acceso remoto cuesta más que el local. Nota el desequilibrio de memoria libre: el nodo 0 está apretado; el nodo 1 está principalmente libre.
Decisión: Si tu carga está mayormente en CPUs 0–15 y el nodo 0 está casi lleno, corres alto riesgo de asignaciones remotas o reclaim. Planea reequilibrar la colocación de memoria o la ubicación de CPUs.
Task 2: Comprobar a qué nodo es local un dispositivo PCIe
cr0x@server:~$ cat /sys/class/net/ens5f0/device/numa_node
1
Qué significa: La NIC está conectada al nodo NUMA 1.
Decisión: Para cargas con alta tasa de paquetes, prefiere ejecutar los hilos intensivos de red en las CPUs del nodo 1 y canalizar las IRQs allí.
Task 3: Mapear dispositivos NVMe a nodos NUMA
cr0x@server:~$ for d in /sys/class/nvme/nvme*; do echo -n "$(basename $d) "; cat $d/device/numa_node; done
nvme0 0
nvme1 0
Qué significa: Ambos controladores NVMe son locales al nodo 0.
Decisión: Mantén tus hilos de envío/completado de IO más calientes en el nodo 0 si buscas baja latencia, o al menos alinea los cores que procesan IO con el nodo 0.
Task 4: Ver asignaciones de memoria por nodo y contadores NUMA hit/miss
cr0x@server:~$ numastat
node0 node1
numa_hit 1245067890 703112340
numa_miss 85467123 92133002
numa_foreign 92133002 85467123
interleave_hit 10234 11022
local_node 1231123456 688000112
other_node 100000000 110000000
Qué significa: Contadores no triviales numa_miss y other_node indican uso de memoria remota. Algo de remoto es normal; mucho es señal de mala colocación.
Decisión: Si el tráfico remoto crece durante la ventana de lentitud, enfócate en binding de CPU/memoria o presión de reclaim/migración antes de tocar el código de la aplicación.
Task 5: Inspeccionar el mapa de memoria NUMA de un proceso (RSS por nodo)
cr0x@server:~$ pidof postgres
2481
cr0x@server:~$ numastat -p 2481
Per-node process memory usage (in MBs) for PID 2481 (postgres)
Node 0 Node 1 Total
--------------- --------------- ---------------
Private 62048.1 1892.0 63940.1
Heap 41000.0 256.0 41256.0
Stack 32.0 16.0 48.0
Huge 0.0 0.0 0.0
---------------- --------------- --------------- ---------------
Total 62080.1 1924.0 64004.1
Qué significa: La memoria de este proceso está abrumadoramente en el nodo 0.
Decisión: Asegura que los hilos de trabajo más activos de Postgres se programen principalmente en las CPUs del nodo 0, o enlaza explícitamente la asignación de memoria al nodo donde pretendes ejecutar.
Task 6: Comprobar afinidad de CPU de un proceso (¿alguien lo pinneó?)
cr0x@server:~$ taskset -cp 2481
pid 2481's current affinity list: 16-31
Qué significa: El proceso está pinneado a las CPUs del nodo 1, pero en la Tarea 5 su memoria está en el nodo 0. Eso es un clásico dolor por accesos remotos.
Decisión: O mueve el proceso a las CPUs 0–15 o reconstruye la localidad de memoria (reinicia con el binding correcto, o usa migración cuidadosamente si está soportada).
Task 7: Lanzar una carga con binding explícito de CPU + memoria
cr0x@server:~$ numactl --cpunodebind=1 --membind=1 -- bash -c 'echo "bound"; sleep 1'
bound
Qué significa: Este shell (y cualquier cosa que lance) se ejecutará en el nodo 1 y asignará memoria desde el nodo 1.
Decisión: Usa esto para pruebas dirigidas. Si el rendimiento mejora, has confirmado la localidad como cuello de botella y puedes implementar una estrategia de colocación durable.
Task 8: Revisar el estado del balanceo NUMA automático
cr0x@server:~$ sysctl kernel.numa_balancing
kernel.numa_balancing = 1
Qué significa: El kernel puede migrar páginas entre nodos para mejorar la localidad.
Decisión: Para algunas cargas sensibles a latencia, el balanceo automático puede añadir jitter. Si ya gestionas afinidad explícitamente, considera desactivarlo tras las pruebas.
Task 9: Observar actividad de NUMA balancing en vmstat
cr0x@server:~$ vmstat -w 1 5
procs -------------------memory------------------ ---swap-- -----io---- -system-- --------cpu--------
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 18234000 10240 120000 0 0 12 45 610 1200 18 6 74 2 0
4 0 0 18190000 10240 120500 0 0 10 38 640 1600 22 7 68 3 0
6 0 0 18020000 10240 121100 0 0 11 40 780 2400 28 9 58 5 0
7 0 0 17800000 10240 121900 0 0 12 42 900 3000 31 10 52 7 0
8 0 0 17550000 10240 122800 0 0 15 50 1100 4200 35 11 46 8 0
Qué significa: El aumento de cambios de contexto (cs) y la disminución de idle (id) pueden acompañar presión de migración/reclaim. Es una señal tosca, no una prueba definitiva de NUMA.
Decisión: Si esto se correlaciona con estadísticas de memoria remota más altas, trátalo como “colocación de memoria más presión” y mira la memoria libre por nodo y el escaneo de páginas.
Task 10: Consultar memoria libre por nodo directamente
cr0x@server:~$ grep -E 'Node [01] (MemTotal|MemFree|FilePages|Active|Inactive)' /sys/devices/system/node/node*/meminfo
Node 0 MemTotal: 263913472 kB
Node 0 MemFree: 1928396 kB
Node 0 FilePages: 2210040 kB
Node 0 Active: 92233728 kB
Node 0 Inactive: 70542336 kB
Node 1 MemTotal: 263913472 kB
Node 1 MemFree: 198223224 kB
Node 1 FilePages: 5110040 kB
Node 1 Active: 12133728 kB
Node 1 Inactive: 8542336 kB
Qué significa: El nodo 0 está casi sin memoria libre mientras el nodo 1 tiene mucho. El kernel empezará a asignar remotamente o a reclamar agresivamente en el nodo 0.
Decisión: O mueve los hilos de trabajo al nodo 1 y reinicia para que la memoria se asigne allí, o reparte las asignaciones entre nodos intencionalmente (interleave) si la carga lo tolera.
Task 11: Comprobar distribución de IRQ y si las interrupciones están alineadas con la localidad del dispositivo
cr0x@server:~$ grep -E 'ens5f0|nvme' /proc/interrupts | head
132: 1203345 0 0 0 IR-PCI-MSI 524288-edge ens5f0-TxRx-0
133: 0 1189922 0 0 IR-PCI-MSI 524289-edge ens5f0-TxRx-1
134: 0 0 1191120 0 IR-PCI-MSI 524290-edge ens5f0-TxRx-2
135: 0 0 0 1210034 IR-PCI-MSI 524291-edge ens5f0-TxRx-3
Qué significa: Las colas están distribuidas entre CPUs (columnas). Buena señal. Pero aún necesitas verificar que esas CPUs pertenezcan al mismo nodo NUMA que la NIC.
Decisión: Si la NIC está en el nodo 1 pero la mayoría de interrupciones caen en CPUs del nodo 0, ajusta la afinidad de IRQ (o deja que irqbalance lo haga si está haciéndolo bien).
Task 12: Identificar qué CPUs están en qué nodo (para decisiones de afinidad IRQ)
cr0x@server:~$ lscpu | egrep 'NUMA node\(s\)|NUMA node0 CPU\(s\)|NUMA node1 CPU\(s\)'
NUMA node(s): 2
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
Qué significa: Mapeo claro de CPUs a nodos.
Decisión: Al pinnear hilos de la app o IRQs, mantén el camino caliente en un nodo a menos que tengas una buena razón para no hacerlo.
Task 13: Comprobar restricciones de cpuset de cgroup (a los contenedores les encanta esta trampa)
cr0x@server:~$ systemctl is-active kubelet
active
cr0x@server:~$ cat /sys/fs/cgroup/cpuset/kubepods.slice/cpuset.cpus
0-31
cr0x@server:~$ cat /sys/fs/cgroup/cpuset/kubepods.slice/cpuset.mems
0-1
Qué significa: Los pods pueden ejecutarse en todas las CPUs y asignar desde ambos nodos. Eso es flexible, pero también puede convertirse en “colocación aleatoria”.
Decisión: Para pods críticos de latencia, usa una política consciente de topología (Guaranteed QoS, CPU Manager estático y scheduling NUMA-aware) para que CPUs y memoria permanezcan alineadas.
Task 14: Inspeccionar los nodos NUMA permitidos de un proceso en ejecución
cr0x@server:~$ cat /proc/2481/status | egrep 'Cpus_allowed_list|Mems_allowed_list'
Cpus_allowed_list: 16-31
Mems_allowed_list: 0-1
Qué significa: El proceso puede asignar memoria en ambos nodos, pero solo puede ejecutarse en las CPUs del nodo 1. Esa combinación a menudo produce asignaciones remotas al inicio y se “arregla sola” más tarde de formas impredecibles.
Decisión: Para comportamiento predecible, alinea Cpus_allowed_list y Mems_allowed_list a menos que hayas medido un beneficio al intercalar.
Task 15: Usar contadores de rendimiento para una comprobación de sentido común (fallos de LLC y ciclos estancados)
cr0x@server:~$ sudo perf stat -p 2481 -a -e cycles,instructions,cache-misses,stalled-cycles-frontend -I 1000 -- sleep 3
# time counts unit events
1.000993650 2,104,332,112 cycles
1.000993650 1,003,122,400 instructions
1.000993650 42,110,023 cache-misses
1.000993650 610,334,992 stalled-cycles-frontend
2.001976121 2,201,109,003 cycles
2.001976121 1,021,554,221 instructions
2.001976121 47,901,114 cache-misses
2.001976121 702,110,443 stalled-cycles-frontend
3.003112980 2,305,900,551 cycles
3.003112980 1,030,004,992 instructions
3.003112980 55,002,203 cache-misses
3.003112980 801,030,110 stalled-cycles-frontend
Qué significa: El aumento de fallos de caché y stalls en frontend sugiere dolor en el subsistema de memoria. Esto por sí solo no grita “NUMA”, pero apoya la hipótesis de localidad cuando se empareja con numastat.
Decisión: Si los fallos de caché se correlacionan con estadísticas de memoria remota más altas, prioriza soluciones de colocación y reduce el chatter entre sockets antes de micro-optimizar el código.
Task 16: Prueba A/B rápida y sucia: ejecución en un solo nodo vs abarcando
cr0x@server:~$ numactl --cpunodebind=0 --membind=0 -- bash -c 'stress-ng --cpu 8 --vm 2 --vm-bytes 8G --timeout 20s --metrics-brief'
stress-ng: info: [3112] setting timeout to 20s
stress-ng: metrc: [3112] stressor bogo ops real time usr time sys time bogo ops/s
stress-ng: metrc: [3112] cpu 9821 20.00 18.90 1.02 491.0
stress-ng: metrc: [3112] vm 1220 20.00 8.12 2.10 61.0
Qué significa: Tienes una línea base cuando limitas a un nodo. Repite en el nodo 1 y compara. Luego corre sin binding y compara otra vez.
Decisión: Si los resultados de un solo nodo son más estables o más rápidos que “libre para todos”, tu scheduling/colocación por defecto no mantiene la localidad. Arregla la política; no compres más CPUs.
Broma #2 (breve, relevante): La afinación NUMA es el arte de mover el trabajo más cerca de su memoria—porque la teletransportación aún no está en la hoja de ruta del kernel.
Tres micro-historias corporativas (anonimizadas, plausibles, técnicamente precisas)
Mini-historia 1: El incidente causado por una suposición equivocada
Un equipo de pagos migró un servicio sensible a latencia desde máquinas antiguas de un solo zócalo a nuevos servidores dual-socket. Las nuevas cajas tenían más núcleos, más RAM y un precio mucho mayor, así que todos esperaban una victoria fácil. La prueba de carga se veía bien en throughput promedio, así que el cambio fue a producción.
En pocas horas, la latencia p99 empezó a aumentar. No picos—un aumento sostenido. El on-call vio CPU al 60–70%, la red bien, discos bien, y asumió que era un vecino ruidoso en la gráfica de dependencias externas. Hicieron rollback. La latencia volvió a la normalidad. El hardware nuevo fue etiquetado como “inestable”.
Semanas después, la misma migración volvió a intentarse. Esta vez, alguien ejecutó numastat -p y taskset. El servicio estaba pinneado (por un script de despliegue bien intencionado) a “los últimos 16 CPUs” porque así separaban cargas las máquinas antiguas. En las nuevas máquinas, “los últimos 16 CPUs” pertenecían al nodo NUMA 1. La memoria del servicio—asignada temprano en el arranque—cayó mayormente en el nodo 0 debido a dónde corrió el proceso init y cómo estaba estructurado el unit file.
Así que los hilos más calientes corrían en el nodo 1, leyendo y escribiendo memoria en el nodo 0, y además manejaban interrupciones de red de una NIC conectada al nodo 1. La carga hacía lecturas entre sockets para el estado de la aplicación y luego escrituras entre sockets para metadatos del allocator. Fue un cóctel de latencia.
La solución fue aburrida: alinear afinidad de CPU y política de memoria al iniciar el servicio, verificar la localidad de IRQ de la NIC y dejar de usar números de CPU como si fueran semántica estable. La lección real del postmortem: doble zócalo no es “más single-socket”. Es una topología que debes respetar.
Mini-historia 2: La optimización que salió mal
Un equipo de almacenamiento que ejecutaba un servicio NVMe-intenso quería reducir la latencia de cola. Alguien propuso pinnear los hilos de envío de IO a un pequeño conjunto de cores aislados. Lo hicieron. La latencia mejoró en pruebas de baja carga, así que lo desplegaron ampliamente.
Bajo tráfico real, el sistema desarrolló paradas periódicas. No caídas totales—suficiente para causar reintentos y lentitud visible por el usuario. Las gráficas de CPU se veían “bien”: esos cores pinneados estaban ocupados, otros estaban mayormente inactivos. Esa fue la primera pista. No quieres medio servidor inactivo mientras los clientes esperan.
La investigación mostró que los dispositivos NVMe estaban conectados al nodo NUMA 0, pero los hilos IO pinneados estaban en el nodo 1. Peor aún, las interrupciones MSI-X se estaban balanceando entre ambos sockets. Cada completado implicaba saltos entre sockets: interrupción en nodo 0, despertar hilo en nodo 1, acceder a colas asignadas en nodo 0, tocar contadores compartidos, repetir. Cuando aumentó la carga, el tráfico de coherencia y el acceso remoto se amplificaron mutuamente. La configuración pinneada impidió que el scheduler “arreglara accidentalmente” moviendo procesos más cerca del dispositivo.
El rollback no fue “dejar de pinnear”. Fue “pinnear correctamente”. Alinearon los hilos IO y la afinidad de IRQ al nodo 0, luego repartieron las colas entre cores de ese nodo. La latencia tail se estabilizó y el throughput aumentó porque la interconexión dejó de hacer trabajo no pagado.
Conclusión práctica: pinnear no es optimización; pinnear es un compromiso. Si no te comprometes con la localidad de extremo a extremo—CPU, memoria y IO—pinnear es solo una forma de hacer consistente un diseño malo.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Un grupo de plataforma de datos ejecutaba cargas mixtas: una base de datos, un servicio tipo Kafka para logs y un conjunto de jobs por lotes. Estandarizaron en servidores dual-socket por capacidad, pero trataron NUMA como parte de la especificación de despliegue, no como curiosidad post-incidente.
Cada servicio tenía un “contrato de colocación” en su runbook: qué nodo NUMA preferir, cómo comprobar la localidad de dispositivos y qué hacer si el nodo no tenía memoria libre suficiente. Usaban un pequeño conjunto de comandos—numactl --hardware, numastat, taskset, /proc/interrupts—y requerían evidencia en las revisiones de cambios para cualquier modificación de pinning de CPU.
Un día, después de una actualización rutinaria del kernel, notaron una regresión leve pero consistente en p95 en la ruta de ingestión de logs. Nada en llamas. Ahí es cuando la práctica aburrida paga: alguien ejecutó las comprobaciones de colocación. Una actualización de firmware de la NIC había provocado un cambio de slot PCIe en un ciclo de mantenimiento, y la NIC terminó conectada al nodo 1 mientras sus hilos de ingestión estaban vinculados al nodo 0. Las IRQs siguieron a la NIC; los hilos no.
Ajustaron la afinidad para coincidir con la nueva topología y recuperaron el rendimiento sin sala de guerra. Sin heroísmos. Sin culpas. Solo operaciones conscientes de la topología. El ticket del incidente se cerró con el tipo de comentario que todos ignoran hasta que lo necesitan: “Los cambios de hardware son cambios de software”.
Errores comunes: síntoma → causa raíz → solución
Esta sección es intencionalmente directa. Estos son los patrones que aparecen repetidamente en producción.
1) Síntoma: un socket está saturado, el otro mayormente inactivo
Causa raíz: Afinidad CPU/cpuset confina la carga a un nodo, o un cuello de botella single-threaded fuerza serialización. A veces es procesamiento de IRQ concentrado en unos pocos cores.
Solución: Si la carga puede escalar, extiéndela entre cores dentro de un nodo primero. Solo abarca sockets cuando lo necesites. Si abarcas, también gestiona la política de memoria y la localidad de IRQ. Valida con taskset -cp, lscpu y /proc/interrupts.
2) Síntoma: p99 peor en dual-socket que en single-socket
Causa raíz: Acceso a memoria entre sockets y churn de coherencia (locks, allocators compartidos, contadores calientes). A menudo desencadenado por hilos en el nodo A con memoria en el nodo B.
Solución: Alinea hilos y memoria vía numactl --cpunodebind + --membind (o via el gestor de servicios/política de cgroup). Reduce el compartir entre sockets haciendo sharding de colas y contadores por hilo. Verifica con numastat -p y contadores de acceso remoto.
3) Síntoma: el throughput está bien, pero el jitter es terrible
Causa raíz: Balanceo NUMA automático y migración de páginas bajo carga, o presión de memoria por nodo causando picos de reclaim. THP puede amplificar el coste de migración.
Solución: Asegura memoria libre adecuada en el nodo donde corre la carga. Considera desactivar el balanceo NUMA automático para cargas pinneadas tras pruebas. Vigila meminfo por nodo y numastat en el tiempo.
4) Síntoma: rendimiento de red se queda corto o cae bajo carga
Causa raíz: Interrupciones de la NIC y procesamiento de la red en el socket equivocado; hilos de la app en el otro socket; steering de paquetes que pelea con el CPU pinning.
Solución: Confirma el nodo NUMA de la NIC vía sysfs. Alinea la afinidad de IRQ y los cores de la aplicación. Usa multi-queue sensatamente. Revisa la distribución en /proc/interrupts y el mapeo de CPUs a nodos.
5) Síntoma: picos de latencia NVMe durante IO intensivo incluso con CPU ociosa en otro lado
Causa raíz: Hilos de envío/completado de IO lejos del controlador NVMe; memoria de colas en nodo remoto; interrupciones aterrizando en ambos sockets.
Solución: Mantén la ruta de IO en el nodo del dispositivo. Valida el numa_node del controlador. Ajusta la colocación de hilos y la afinidad de IRQ. No “optimices” pinneando sin localidad.
6) Síntoma: añadir más hilos trabajadores lo empeora
Causa raíz: Cruzaste un límite NUMA y convertiste la contención local en contención remota; locks y cachés compartidas se volvieron más caros.
Solución: Escala dentro de un socket primero. Si necesitas más, haz sharding por nodo NUMA (dos pools independientes) en lugar de un pool global. Trata el compartir entre sockets como caro.
7) Síntoma: los contenedores se comportan diferente en nodos idénticos
Causa raíz: Diferente topología de slots PCIe, ajustes de BIOS o políticas del kubelet CPU/memory manager. “SKU idéntico” no significa “topología idéntica”.
Solución: Estandariza ajustes de BIOS, valida lscpu y los nodos NUMA de dispositivos durante el aprovisionamiento, y usa scheduling consciente de topología para pods críticos.
Listas de verificación / plan paso a paso
Estos son los pasos que evitan que las máquinas dual-socket roben silenciosamente tu presupuesto de latencia.
Checklist A: Antes de desplegar una carga sensible a latencia en dual-socket
- Mapea la topología: registra la salida de
numactl --hardwareylscpu. Quieres que esté en el ticket, no en la memoria de alguien. - Mapea la localidad de dispositivos: para NICs y NVMe, captura
/sys/class/net/*/device/numa_nodey/sys/class/nvme/nvme*/device/numa_node. - Decide un modelo de colocación: nodo único (preferido), sharding por nodo, o cross-node (solo si es necesario).
- Elige una estrategia de afinidad: o “sin pinning, deja que el scheduler funcione” u “pin + bind de memoria + alinear IRQs”. Nunca “solo pinnear”.
- Revisa capacidad por nodo: asegura que el nodo elegido tenga suficiente margen de memoria para page cache + heap + picos.
Checklist B: Cuando ya tienes una caja lenta y necesitas una solución hoy
- Ejecuta
numactl --hardware; busca desequilibrio de memoria libre por nodo. - Ejecuta
numastat; busca altos y crecientesnuma_miss/other_node. - Escoge tu PID más caliente; ejecuta
numastat -pytaskset -cp. Comprueba si el nodo CPU y el nodo de memoria coinciden. - Revisa nodo NUMA del dispositivo y
/proc/interrupts. Confirma la localidad de IRQ para NIC/NVMe. - Si el desajuste es obvio: corrige la colocación (reinicia con el binding correcto) en lugar de perseguir micro-optimizaciónes.
Checklist C: Un modelo operativo sensato a largo plazo
- Haz de la topología parte del inventario: guarda el mapeo NUMA y la info de conexión PCIe por host.
- Estandariza ajustes de BIOS: evita modos sorpresa que alteren la exposición NUMA (y documenta lo que eliges).
- Construye “smoke tests” NUMA: ejecuta pruebas rápidas de localidad A/B durante el aprovisionamiento para detectar rarezas temprano.
- Capacita a los equipos: “dual-socket ≠ dos veces más rápido” debería ser conocimiento común, no tradición oral.
- Revisa cambios de pinning como cambios de código: requiere evidencia, plan de rollback y validación post-cambio.
Preguntas frecuentes
Q1: ¿Debería evitar siempre servidores dual-socket?
No. Dual-socket es excelente para capacidad de memoria, líneas PCIe y throughput agregado. Evítalo cuando tu carga sea crítica en latencia y muy compartida, y puedas encajar en una SKU sólida de un solo zócalo.
Q2: ¿NUMA es solo un problema de bases de datos?
No. Afecta mucho a las bases de datos por grandes huellas de memoria y estructuras compartidas, pero también impacta en redes, NVMe, servicios JVM, analítica y cualquier cosa que genere muchos fallos de caché.
Q3: Si Linux tiene balanceo NUMA automático, ¿por qué debo preocuparme?
Porque el balanceo es reactivo y no es gratis. Puede mejorar el throughput para cargas generales, pero puede añadir jitter para servicios sensibles a latencia—especialmente cuando además pinneas CPUs.
Q4: ¿Qué es mejor: intercalar memoria entre nodos o ligar a un nodo?
Ligar suele ser mejor para latencia y predictibilidad cuando puedes mantener la carga dentro de un nodo. Intercalar puede ayudar cuando realmente necesitas ancho de banda de ambos controladores de memoria y la carga está bien paralelizada.
Q5: ¿Cómo sé si mi carga está cruzando sockets?
Usa taskset -cp (dónde corre) y numastat -p (dónde vive su memoria). Si el nodo CPU y el nodo de memoria no coinciden, estás cruzando sockets de la peor manera.
Q6: ¿Puedo arreglar problemas NUMA sin reiniciar el servicio?
A veces puedes mitigar con cambios de afinidad CPU y direccionamiento de IRQ, pero la colocación de memoria a menudo se decide en el momento de la asignación. La solución limpia suele ser reiniciar con el binding correcto y suficiente memoria libre por nodo.
Q7: ¿El uso de huge pages ayuda o perjudica NUMA?
Puede ayudar reduciendo la presión de la TLB, lo que baja la sobrecarga CPU. Puede perjudicar si hace la colocación y migración de memoria más costosa bajo presión. Mide; no asumas.
Q8: ¿El hyperthreading cambia el comportamiento NUMA?
Hyperthreading no cambia qué memoria es local. Cambia cuánto contención tienes por core. Para algunas cargas, usar menos hilos por core mejora la latencia y reduce la contención entre sockets.
Q9: En Kubernetes, ¿cuál es la trampa NUMA más común?
Pods Guaranteed con CPU pinneada que caen en un nodo mientras las asignaciones de memoria se dispersan o se hacen por defecto en otro lado, además de NIC IRQs sentadas en el nodo opuesto. La alineación importa.
Q10: Si necesito todos los cores en ambos sockets, ¿cuál es el diseño menos malo?
Haz sharding por nodo NUMA. Ejecuta dos pools de trabajo, dos allocators/arenas cuando sea posible, colas por nodo y solo intercambia datos entre sockets cuando sea necesario. Trata la interconexión como un recurso escaso.
Siguientes pasos que deberías hacer realmente
Los servidores dual-socket no están malditos. Solo son honestos. Te dicen, muy directamente, si el diseño de tu sistema respeta la localidad.
- Elige un host de producción y registra topología + localidad de dispositivos:
numactl --hardware,lscpuy los archivosnuma_nodepara NIC/NVMe. - Elige tus 3 servicios más sensibles a latencia y captura:
numastat -p,taskset -cpy/proc/<pid>/statusmem/CPU allowances durante el pico. - Decide una política por servicio: bind a nodo único, sharding por nodo o interleave medido. Escríbelo. Ponlo en el runbook.
- Detén los cambios “solo pinning” a menos que el cambio también especifique política de memoria y localidad de interrupciones. Si no puedes explicar toda la ruta de datos, no has terminado.
- Valida con una prueba A/B (vinculado a un nodo vs por defecto) antes y después del próximo refresh de hardware. Haz de NUMA parte de la aceptación, no del postmortem.