La migración en vivo mueve una máquina virtual en ejecución de un host físico a otro sin apagarla: libvirt copia la memoria de la VM mientras sigue funcionando, repite la copia de las páginas que cambian y, en el último instante, pausa la VM unos milisegundos para transferir el estado final y reanudarla en el destino. En este tutorial prepararás dos hosts KVM con Ubuntu 24.04, compartirás el almacenamiento de las VMs por NFS y migrarás una VM de prueba con virsh migrate, además de ver cómo hacerlo sin almacenamiento compartido.

Requisitos previos

  • Dos servidores físicos o bare metal con Ubuntu 24.04 LTS y virtualización por hardware activada (Intel VT-x o AMD-V). En esta guía se llaman host-a (origen) y host-b (destino).
  • CPUs del mismo fabricante en ambos hosts (Intel con Intel, AMD con AMD). Entre fabricantes distintos la migración en vivo no es posible.
  • Un usuario no root con privilegios sudo en los dos hosts, con el mismo nombre (en los ejemplos, your_user).
  • Conectividad de red entre ambos hosts, idealmente una red privada de 10 Gbit/s. Con 1 Gbit/s funciona, pero las VMs con mucha memoria tardan más en converger.
  • Para la parte de almacenamiento compartido, un servidor NFS. En los ejemplos está en 10.0.0.30; también puede ser un tercer servidor o un NAS.

Sustituye 10.0.0.10 (host-a), 10.0.0.20 (host-b) y 10.0.0.30 (NFS) por las IPs de tu red privada.

Paso 1: Instalar KVM y libvirt en ambos hosts

Ejecuta estos comandos en host-a y en host-b. Comprueba primero que la CPU expone la virtualización por hardware:

sudo apt update
sudo apt install cpu-checker
kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used

Instala QEMU/KVM, libvirt y las herramientas de gestión:

sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virtinst nfs-common

Añade tu usuario al grupo libvirt para poder usar virsh contra qemu:///system sin sudo, y vuelve a iniciar sesión para que el cambio surta efecto:

sudo usermod -aG libvirt $USER

Verifica que libvirt responde:

virsh -c qemu:///system list --all
 Id   Name   State
--------------------

Para no tener que escribir -c qemu:///system en cada comando, exporta la URI por defecto en tu ~/.bashrc:

echo 'export LIBVIRT_DEFAULT_URI=qemu:///system' >> ~/.bashrc
source ~/.bashrc

Paso 2: Configurar resolución de nombres y SSH entre hosts

virsh migrate se conecta al destino por SSH (qemu+ssh://) y, por defecto, el host de destino anuncia su propio nombre para el canal de datos de la migración. Si el origen no puede resolver ese nombre, la migración falla. Añade ambos hosts a /etc/hosts en los dos servidores:

sudo nano /etc/hosts
10.0.0.10 host-a
10.0.0.20 host-b

Comprueba que hostname devuelve host-a y host-b respectivamente. Después, en host-a, genera una clave SSH (si no tienes una) y cópiala a host-b:

ssh-keygen -t ed25519
ssh-copy-id your_user@host-b

Verifica que puedes abrir una conexión de libvirt remota desde host-a sin contraseña:

virsh -c qemu+ssh://your_user@host-b/system list --all
 Id   Name   State
--------------------

Repite la operación en sentido contrario (de host-b a host-a) si quieres poder devolver las VMs a su host original.

Paso 3: Abrir los puertos de migración

Con qemu+ssh, la conexión de control va por SSH (puerto 22), pero la memoria de la VM viaja por una conexión TCP directa entre los dos QEMU, en un puerto del rango 49152-49215 del host de destino. Si usas UFW, permite ese rango solo desde la IP del otro host. En host-b:

sudo ufw allow from 10.0.0.10 to any port 49152:49215 proto tcp

Y en host-a, para las migraciones de vuelta:

sudo ufw allow from 10.0.0.20 to any port 49152:49215 proto tcp

Asegúrate también de que SSH está permitido (sudo ufw allow OpenSSH) antes de activar UFW.

Paso 4: Montar el almacenamiento compartido por NFS

Con almacenamiento compartido, los discos de la VM están en la misma ruta en ambos hosts y solo hay que mover la memoria, que es lo más rápido. En el servidor NFS (10.0.0.30), instala el servidor y crea el directorio exportado:

sudo apt install nfs-kernel-server
sudo mkdir -p /srv/vm-images

Edita /etc/exports para exportar el directorio solo a los dos hosts KVM:

sudo nano /etc/exports
/srv/vm-images 10.0.0.10(rw,sync,no_subtree_check,no_root_squash) 10.0.0.20(rw,sync,no_subtree_check,no_root_squash)

no_root_squash es necesario porque libvirt cambia el propietario de las imágenes de disco al arrancar cada VM. Por eso la exportación debe limitarse a las IPs de los hosts de virtualización. Aplica la configuración y compruébala:

sudo exportfs -ra
sudo exportfs -v
/srv/vm-images  10.0.0.10(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)
/srv/vm-images  10.0.0.20(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)

Ahora, en host-a y en host-b, monta el recurso en la misma ruta. La ruta tiene que ser idéntica en los dos hosts, porque la definición XML de la VM hace referencia a ella:

sudo mkdir -p /var/lib/libvirt/images/shared
sudo nano /etc/fstab

Añade esta línea al final:

10.0.0.30:/srv/vm-images /var/lib/libvirt/images/shared nfs defaults,_netdev 0 0

Monta y verifica:

sudo systemctl daemon-reload
sudo mount -a
df -h /var/lib/libvirt/images/shared
Filesystem                Size  Used Avail Use% Mounted on
10.0.0.30:/srv/vm-images  1.8T  1.2G  1.8T   1% /var/lib/libvirt/images/shared

Crea un archivo en un host y comprueba que aparece en el otro para confirmar que ambos ven el mismo almacenamiento.

Paso 5: Elegir un modelo de CPU compatible

La VM no puede cambiar de CPU a mitad de ejecución: todas las instrucciones que vea en el origen tienen que existir también en el destino. El modo host-passthrough expone la CPU física exacta y solo es seguro si ambos hosts tienen el mismo modelo de procesador y el mismo microcódigo. Para hosts distintos, usa un modelo con nombre que ambos soporten.

Lista en cada host los modelos de CPU que QEMU puede usar en esa máquina:

virsh domcapabilities | grep "usable='yes'"
      <model usable='yes' vendor='Intel' canonical='Skylake-Server-v1'>Skylake-Server</model>
      <model usable='yes' vendor='Intel' canonical='Cascadelake-Server-v1'>Cascadelake-Server</model>
      <model usable='yes' vendor='Intel' canonical='Icelake-Server-v1'>Icelake-Server</model>

Elige el modelo más moderno que aparezca como usable='yes' en los dos hosts. Si los dos servidores son idénticos, puedes usar host-model, que libvirt traduce a un modelo concreto y lo transmite al destino durante la migración.

Paso 6: Crear una VM de prueba en el almacenamiento compartido

Para probar, crea una VM a partir de la imagen cloud oficial de Ubuntu 24.04. En host-a, descárgala al almacenamiento compartido y crea un disco de 10 GB basado en ella:

cd /var/lib/libvirt/images/shared
sudo wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
sudo qemu-img convert -O qcow2 noble-server-cloudimg-amd64.img testvm.qcow2
sudo qemu-img resize testvm.qcow2 10G

Crea la VM con virt-install. Sustituye Skylake-Server por el modelo que elegiste en el paso anterior:

sudo virt-install \
  --name testvm \
  --memory 2048 \
  --vcpus 2 \
  --cpu Skylake-Server \
  --disk path=/var/lib/libvirt/images/shared/testvm.qcow2,format=qcow2,bus=virtio,cache=none \
  --import \
  --osinfo linux2022 \
  --network network=default,model=virtio \
  --cloud-init root-ssh-key=$HOME/.ssh/id_ed25519.pub \
  --graphics none \
  --noautoconsole

Dos detalles importan para la migración:

  • cache=none: libvirt rechaza la migración en vivo de discos en almacenamiento compartido con otros modos de caché, porque podrían quedar datos pendientes en la caché de página del origen.
  • La red: el destino debe tener una red o bridge con el mismo nombre. Aquí se usa la red NAT default que crea libvirt en ambos hosts, suficiente para la prueba. En producción usarás un bridge (--network bridge=br0) presente con el mismo nombre en los dos hosts, para que la VM conserve su IP pública o privada al migrar.

Comprueba que la VM está en marcha y obtén su IP:

virsh list
virsh domifaddr testvm
 Id   Name     State
------------------------
 1    testvm   running

 Name       MAC address          Protocol     Address
-------------------------------------------------------------
 vnet0      52:54:00:3a:5c:81    ipv4         192.168.122.57/24

Paso 7: Migrar la VM en vivo

Lanza la migración desde host-a:

virsh migrate --live --persistent --undefinesource --verbose \
  testvm qemu+ssh://your_user@host-b/system

Qué hace cada opción:

  • --live: la VM sigue funcionando durante la copia de memoria.
  • --persistent: define la VM de forma permanente en host-b, para que sobreviva a un reinicio de libvirt.
  • --undefinesource: elimina la definición de host-a al terminar, así la VM no queda duplicada.
  • --verbose: muestra el progreso.
Migration: [100 %]

Mientras la migración está en curso, puedes seguir su progreso desde otra terminal en host-a:

virsh domjobinfo testvm
Job type:         Unbounded
Operation:        Outgoing migration
Time elapsed:     4213         ms
Data processed:   1.105 GiB
Data remaining:   512.320 MiB
Memory bandwidth: 268.410 MiB/s

Paso 8: Verificar la migración

En host-b, la VM debe aparecer en ejecución:

virsh list --all
 Id   Name     State
------------------------
 1    testvm   running

Y en host-a ya no debe existir:

virsh list --all
 Id   Name   State
--------------------

Para comprobar que la migración no interrumpe el servicio, repite la migración (de vuelta a host-a) mientras mantienes un ping continuo a la VM desde otra máquina de la misma red. Lo normal es no perder ningún paquete o, como mucho, uno.

Migrar sin almacenamiento compartido

Si los hosts no comparten almacenamiento, libvirt puede copiar también el disco durante la migración con --copy-storage-all. Es mucho más lento, porque transfiere el disco completo, pero no requiere NFS.

Antes de migrar, crea en el destino una imagen vacía del mismo tamaño virtual, formato y ruta que el disco original. En host-b:

sudo qemu-img create -f qcow2 /var/lib/libvirt/images/testvm.qcow2 10G

Después, desde host-a:

virsh migrate --live --persistent --undefinesource --copy-storage-all --verbose \
  testvm qemu+ssh://your_user@host-b/system

Consulta el tamaño virtual exacto del disco original con qemu-img info antes de crear la imagen de destino; si no coincide, la migración falla.

Ajustar migraciones de VMs con mucha carga

Una VM que escribe en memoria más deprisa de lo que la red puede copiarla no converge nunca. Tienes varias opciones:

  • Limitar el tiempo máximo de pausa aceptable en milisegundos. Un valor más alto permite terminar antes:

    virsh migrate-setmaxdowntime testvm 500
    
  • Usar --auto-converge, que frena progresivamente las vCPUs de la VM hasta que la copia alcanza a las escrituras:

    virsh migrate --live --auto-converge --persistent --undefinesource --verbose \
      testvm qemu+ssh://your_user@host-b/system
    
  • Usar post-copy (--postcopy): la VM arranca en el destino antes de tener toda la memoria y pide las páginas que faltan bajo demanda. Converge siempre, pero si la red falla en esa fase la VM se pierde, así que úsalo solo en redes fiables.

Solución de problemas

the CPU is incompatible with host CPU: Host CPU does not provide required features: el modelo de CPU de la VM usa instrucciones que el destino no tiene. Cambia la VM a un modelo común (paso 5) y reiníciala.

Unsafe migration: Migration without shared storage is unsafe: libvirt no detecta que el disco esté en almacenamiento compartido. Comprueba que la ruta es un montaje NFS en ambos hosts y que la VM usa cache=none, o usa --copy-storage-all.

unable to connect to server at 'host-b:49152': No route to host: el origen no resuelve el nombre del destino o el firewall bloquea el rango 49152-49215. Revisa /etc/hosts y UFW, o indica la IP explícitamente con --migrateuri tcp://10.0.0.20.

Error sobre una red o un bridge inexistente (por ejemplo br0): la red que usa la VM no existe en el destino. Créala con el mismo nombre en host-b o comprueba con virsh net-list --all que la red default está activa.

Revisa los registros de libvirt en ambos hosts para cualquier otro error:

sudo journalctl -u libvirtd --since "10 minutes ago"

Conclusión

Has preparado dos hosts KVM con almacenamiento compartido por NFS, un modelo de CPU común y acceso SSH entre ellos, y has migrado una VM en marcha sin apagarla. Como siguientes pasos, puedes dedicar una red separada al tráfico de migración para no competir con el de las VMs, sustituir NFS por almacenamiento distribuido como Ceph para eliminar el punto único de fallo, o automatizar el vaciado de un host antes de un mantenimiento migrando todas sus VMs con un bucle sobre virsh list --name.