--- title: "CPU pinning y NUMA para VMs KVM con libvirt en Ubuntu 24.04" description: "Analiza la topología NUMA de tu host, fija las vCPUs de una VM KVM a núcleos físicos, ata su memoria a un nodo NUMA y reserva CPUs para el host con libvirt." category: "Virtualization VPS" language: es source: https://cubepath.com/es/docs/virtualization-vps/cpu-pinning-y-configuracion-numa updated: 2026-09-25 --- # CPU pinning y NUMA para VMs KVM con libvirt en Ubuntu 24.04 Por defecto, el planificador de Linux mueve los hilos de las vCPUs de una máquina virtual entre todos los núcleos del host. En cargas sensibles a la latencia (bases de datos, trading, juegos, audio o vídeo en tiempo real) eso provoca pérdidas de caché y accesos a memoria de otro socket. En este tutorial analizarás la topología de CPU y NUMA de un host KVM con Ubuntu 24.04, fijarás las vCPUs de una VM a núcleos físicos concretos (CPU pinning), atarás su memoria al mismo nodo NUMA y reservarás núcleos para el propio host. ## Requisitos previos - Un servidor físico con Ubuntu 24.04 LTS, KVM y libvirt instalados (`qemu-kvm`, `libvirt-daemon-system`, `libvirt-clients`). - Un usuario no root con privilegios `sudo` y miembro del grupo `libvirt`. - Una VM ya creada con libvirt. En los ejemplos se llama `db01` y tiene 4 vCPUs y 8 GB de RAM. - Idealmente un servidor con dos o más sockets, que es donde NUMA tiene más impacto. En un servidor de un solo socket el pinning sigue siendo útil, pero la parte de NUMA no aplica. > Nota: el pinning tiene sentido cuando reservas núcleos para VMs concretas. Si fijas muchas VMs a los mismos núcleos, el rendimiento será peor que dejando trabajar al planificador. ## Paso 1: Analizar la topología de CPU del host Antes de fijar nada tienes que saber qué CPUs lógicas comparten núcleo físico (hilos hermanos de Hyper-Threading o SMT) y a qué nodo NUMA pertenece cada una. `lscpu` da el resumen: ```bash lscpu | grep -E '^(CPU\(s\)|Thread|Core|Socket|NUMA)' ``` ```output CPU(s): 16 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 2 NUMA node(s): 2 NUMA node0 CPU(s): 0-3,8-11 NUMA node1 CPU(s): 4-7,12-15 ``` Con `lscpu -e` ves el detalle por CPU lógica. La columna `CORE` indica el núcleo físico: dos CPUs lógicas con el mismo número de `CORE` son hilos hermanos del mismo núcleo: ```bash lscpu -e=CPU,NODE,SOCKET,CORE ``` ```output CPU NODE SOCKET CORE 0 0 0 0 1 0 0 1 2 0 0 2 3 0 0 3 4 1 1 4 5 1 1 5 6 1 1 6 7 1 1 7 8 0 0 0 9 0 0 1 10 0 0 2 11 0 0 3 12 1 1 4 13 1 1 5 14 1 1 6 15 1 1 7 ``` En este ejemplo, las CPUs 0 y 8 son el mismo núcleo físico, 1 y 9 otro, etc. Tu numeración puede ser distinta: toma siempre los valores de tu propio servidor. ## Paso 2: Revisar la memoria por nodo NUMA Instala `numactl`, que incluye las utilidades `numactl` y `numastat`: ```bash sudo apt install numactl numactl --hardware ``` ```output available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 8 9 10 11 node 0 size: 64215 MB node 0 free: 41022 MB node 1 cpus: 4 5 6 7 12 13 14 15 node 1 size: 64508 MB node 1 free: 52870 MB node distances: node 0 1 0: 10 21 1: 21 10 ``` La tabla de distancias indica el coste relativo de acceder a la memoria: 10 es memoria local y 21 la del otro socket, aproximadamente el doble de lenta. El objetivo es que cada VM use CPUs y memoria del mismo nodo, y que ese nodo tenga memoria libre suficiente para toda la RAM de la VM. ## Paso 3: Planificar la asignación Con la topología del ejemplo, un reparto razonable para una VM de 4 vCPUs en el nodo 0 es: | Uso | CPUs del host | |-----|---------------| | Sistema operativo del host | 0, 8 (núcleo físico 0) | | Hilos del emulador QEMU (E/S) de `db01` | 1, 9 (núcleo físico 1) | | vCPU 0 y vCPU 1 de `db01` | 2, 10 (núcleo físico 2) | | vCPU 2 y vCPU 3 de `db01` | 3, 11 (núcleo físico 3) | | Resto de VMs | 4-7, 12-15 (nodo 1) | Las vCPUs se asignan por pares de hilos hermanos y la VM verá una topología de 2 núcleos con 2 hilos cada uno, igual que el hardware real. Así el sistema operativo invitado sabe qué vCPUs comparten núcleo y puede planificar mejor. ## Paso 4: Aplicar el pinning a la VM Apaga la VM y edita su definición XML: ```bash virsh shutdown db01 virsh edit db01 ``` Localiza la línea `` y añade después los bloques `` y ``. Sustituye también el bloque `` existente: ```xml 4 ``` ```xml ``` Qué hace cada elemento: - `vcpupin`: fija cada vCPU a una CPU lógica del host. El orden (0 y 1 en el núcleo 2, 2 y 3 en el núcleo 3) coincide con la topología declarada en ``, en la que las vCPUs consecutivas son hilos hermanos. - `emulatorpin`: fija los hilos auxiliares de QEMU (E/S de disco y red, gestión) a otros núcleos, para que no interrumpan a las vCPUs. - `numatune` con `mode='strict'`: toda la memoria de la VM se reserva en el nodo 0. Si el nodo no tiene memoria suficiente, la VM no arranca en lugar de usar memoria remota en silencio. - `host-passthrough`: expone la CPU física tal cual, con todas sus instrucciones y cachés. Es la opción más rápida, pero dificulta la migración en vivo a hosts con otra CPU. El producto `sockets x dies x cores x threads` debe ser igual al número de vCPUs (aquí 1 x 1 x 2 x 2 = 4). Al guardar, `virsh edit` valida el XML y rechaza los cambios si hay errores. Arranca la VM: ```bash virsh start db01 ``` Si prefieres no editar el XML, puedes fijar vCPUs con `virsh vcpupin`. La opción `--config` guarda el cambio y `--live` lo aplica también a la VM en marcha: ```bash virsh vcpupin db01 0 2 --config --live virsh emulatorpin db01 1,9 --config --live ``` ## Paso 5: Verificar el pinning y la memoria Comprueba la afinidad de cada vCPU: ```bash virsh vcpupin db01 ``` ```output VCPU CPU Affinity ---------------------- 0 2 1 10 2 3 3 11 ``` `virsh vcpuinfo` muestra además en qué CPU física se está ejecutando cada vCPU en este momento; siempre debe coincidir con su afinidad: ```bash virsh vcpuinfo db01 | grep -E '^(VCPU|CPU):' ``` ```output VCPU: 0 CPU: 2 VCPU: 1 CPU: 10 VCPU: 2 CPU: 3 VCPU: 3 CPU: 11 ``` Comprueba que la memoria del proceso QEMU está en el nodo 0: ```bash sudo numastat -c qemu-system-x86 ``` ```output Per-node process memory usage (in MBs) PID Node 0 Node 1 Total --------------- ------ ------ ----- 48213 (qemu-syst 8231 2 8233 ``` Prácticamente toda la memoria debe aparecer en `Node 0`. Dentro de la VM, `lscpu` debe mostrar la topología que definiste: ```text Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 ``` ## Paso 6: Reservar CPUs para el host y para las VMs El pinning evita que las vCPUs se muevan, pero no impide que procesos del host (o de otras VMs) usen los núcleos 2, 3, 10 y 11. En Ubuntu 24.04, que usa cgroups v2, puedes limitar con systemd qué CPUs usa cada grupo de procesos, sin tocar parámetros del kernel. Restringe los servicios del sistema y las sesiones de usuario a las CPUs 0 y 8: ```bash sudo systemctl set-property system.slice AllowedCPUs=0,8 sudo systemctl set-property user.slice AllowedCPUs=0,8 sudo systemctl set-property init.scope AllowedCPUs=0,8 ``` libvirt coloca todas las VMs en `machine.slice`. Asígnale el resto de CPUs: ```bash sudo systemctl set-property machine.slice AllowedCPUs=1-7,9-15 ``` `systemctl set-property` guarda los cambios de forma persistente en `/etc/systemd/system.control/`. Verifica el valor efectivo: ```bash cat /sys/fs/cgroup/system.slice/cpuset.cpus.effective cat /sys/fs/cgroup/machine.slice/cpuset.cpus.effective ``` ```output 0,8 1-7,9-15 ``` > Importante: reservar solo un núcleo físico para el host es adecuado para un servidor dedicado a virtualización. Si el host ejecuta otros servicios (almacenamiento Ceph, backups, monitorización), reserva más núcleos o notarás lentitud en el host. Para deshacer la restricción, elimina las propiedades con `sudo systemctl revert system.slice` (y lo mismo para el resto de slices). ## Paso 7 (opcional): Usar hugepages para la memoria de la VM Las páginas grandes (hugepages) reducen los fallos de TLB en VMs con mucha memoria. Reserva 4096 páginas de 2 MB (8 GB) en el nodo 0, el mismo en el que está la VM: ```bash echo 4096 | sudo tee /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages grep -i hugepages_ /proc/meminfo ``` ```output HugePages_Total: 4096 HugePages_Free: 4096 HugePages_Rsvd: 0 HugePages_Surp: 0 ``` Si `HugePages_Total` es menor que el valor pedido, el nodo no tenía suficiente memoria contigua libre; reserva las páginas justo después de arrancar el host. Para que la reserva sobreviva a los reinicios, puedes repetir el comando en una unidad systemd de tipo `oneshot` que se ejecute antes de `libvirtd.service`. Apaga la VM, añade este bloque en su XML (`virsh edit db01`), justo después de ``, y arráncala de nuevo: ```xml ``` Tras arrancarla, `HugePages_Free` debe haber bajado en unas 4096 páginas. ## Solución de problemas **La VM no arranca con `numatune` en modo `strict`**: el nodo NUMA no tiene memoria libre suficiente. Revisa `numactl --hardware`, reduce la RAM de la VM o usa `mode='preferred'`, que prefiere el nodo indicado pero permite usar otro si no hay memoria. **Error sobre `cpuset.cpus` al arrancar la VM**: alguna CPU de `vcpupin` o `emulatorpin` no está incluida en `AllowedCPUs` de `machine.slice`. Las CPUs fijadas deben ser un subconjunto de las permitidas al slice. **El rendimiento empeora tras el pinning**: comprueba que dos VMs no comparten las mismas CPUs con `virsh vcpupin` en cada una, y que las vCPUs no están repartidas entre dos nodos NUMA. ## Conclusión Has analizado la topología de tu host, fijado las vCPUs y los hilos del emulador de una VM a núcleos físicos del mismo nodo NUMA, atado su memoria a ese nodo y separado las CPUs del host de las de las VMs. Como siguientes pasos, mide el efecto con una prueba de carga real (por ejemplo `sysbench` o `pgbench` dentro de la VM) antes y después del cambio, aplica el mismo esquema al resto de VMs críticas repartiéndolas por nodos NUMA, y combina el pinning con virtio-blk o virtio-scsi con `iothread` dedicado para cargas de disco intensivas.