Algunos problemas de rendimiento son obvios. Una CPU al 100%. Un enlace de red paralizado. El almacenamiento es más cruel: falla silenciosamente, en los huecos entre microsegundos. Tu VM “tiene un disco NVMe”, tus paneles dicen que las IOPS están bien, y sin embargo la latencia extremo (tail latency) arruina tu base de datos, tu granja de compilación o tu canal de registros.
El debate habitual se plantea como un combate: NVMe passthrough (VFIO) vs VirtIO. La verdad incómoda: el ganador a menudo es ninguno de los dos. El ganador es eliminar esa capa extra que no sabías que tenías, y afinar la capa que no puedes eliminar. Por eso los equipos pierden esta pelea en producción mientras la ganan en benchmarks.
Qué cambia realmente entre passthrough y VirtIO
A grandes rasgos:
- NVMe passthrough (VFIO PCIe passthrough) da a la VM invitada acceso directo a un controlador NVMe físico. El invitado carga el controlador nativo
nvme, posee las colas, emite comandos administrativos y “ve” algo cercano al metal desnudo. - VirtIO ofrece al invitado un dispositivo paravirtual (virtio-blk o virtio-scsi). El invitado envía peticiones a una virtqueue; el host (QEMU + vhost + capa de bloques del kernel) las completa mediante un dispositivo o archivo subyacente.
Pero la historia de rendimiento trata principalmente de longitud de ruta y puntos de planificación:
- ¿Cuántas veces cruza la solicitud de E/S la frontera usuario/kernel?
- ¿Cuántas colas existen y están mapeadas de forma sensata a los núcleos de CPU?
- ¿Dónde puede la solicitud retrasarse por contención: bloqueo, interrupción, limitación por cgroup, caché de páginas del host, journaling del sistema de archivos, o un hilo del hipervisor que fue desplanificado?
Las rutas reales de E/S (simplificadas)
VirtIO (virtio-blk común con QEMU):
- La aplicación invitada genera E/S (syscall).
- La capa de bloques del kernel invitado la programa.
- El controlador VirtIO publica descriptores en una virtqueue.
- Salida al host (virtio kick / interrupción), QEMU/vhost procesa la petición.
- La capa de bloques del kernel del host la envía al dispositivo físico (o al sistema de archivos si usas archivos de imagen).
- La finalización vuelve; el invitado recibe una interrupción; la aplicación reanuda.
NVMe passthrough:
- La aplicación invitada genera E/S (syscall).
- El controlador NVMe del invitado la envía directamente a la cola del hardware (vía PCIe MMIO/DMA).
- El hardware completa; interrupción al invitado (MSI-X), la finalización la gestiona el invitado.
El passthrough elimina la pila de bloques del host del camino crítico. Eso puede ser enorme para la latencia. También elimina puntos de control del host que quizá necesites, como cachés del host, snapshots, migración en caliente flexible y multiplexación de almacenamiento sencilla.
Esta es la regla que uso en producción: si tu carga es sensible a la latencia y predecible, el passthrough es atractivo. Si tu carga es mixta, multi-inquilino y operativamente desordenada, VirtIO suele ser el compromiso correcto, a menos que lo ejecutes en la configuración por defecto y luego te preguntes por qué no es magia.
Hechos interesantes y contexto histórico (lo que explica el presente)
La virtualización de almacenamiento no se volvió “complicada” por diversión. Se volvió compleja porque los sistemas reales lo son. Algunos puntos de contexto concretos que importan al elegir entre passthrough y VirtIO:
- VirtIO nació del dolor: la emulación completa de dispositivos temprana (como IDE) era lenta porque cada E/S se parecía a una fiesta de interrupciones de hardware en software.
- NVMe estandarizó multi-queue desde el primer día, diseñado para paralelismo. Eso encaja con las CPUs modernas; también significa que el mapeo de colas y el enrutamiento de interrupciones importan mucho más que con el viejo SATA.
- MSI-X hizo posibles las altas IOPS al soportar múltiples interrupciones por dispositivo. Es por eso que “un disco” puede escalar entre núcleos, y por qué una mala afinidad de interrupciones puede arruinar tu día.
- blk-mq en Linux cambió las reglas: la capa de bloque multi-cola redujo la contención por locks y mejoró el escalado, pero también añadió nuevos ajustes y formas de configurarlo mal.
- vhost se creó para sacar a QEMU del camino: mover el datapath al kernel redujo cambios de contexto y mejoró el rendimiento para VirtIO de red y almacenamiento.
- Los planificadores de I/O pasaron de “escoge uno” a “escoge ninguno” para NVMe: en dispositivos rápidos, los planificadores pueden añadir latencia con poco beneficio, así que “none” suele ganar.
- La gente solía hacer benchmarks con 4k random read y declarar victoria. Los servicios modernos suelen mezclar lecturas, escrituras, fsyncs, operaciones de metadata y ráfagas—así que la cultura de benchmark antigua sigue engañando.
- Los hipervisores en la nube normalizaron VirtIO: las características operativas (migración, snapshots, controles de tenancy) importaban tanto como la velocidad bruta, así que VirtIO ganó por ser utilizable.
- El passthrough se volvió práctico a escala cuando IOMMU dejó de ser un problema: VFIO e IOMMU hicieron el aislamiento menos aterrador, pero “menos aterrador” no es lo mismo que “sin compromisos”.
El ganador de rendimiento que nadie menciona: menos capas, menos mentiras
Cuando los equipos discuten “passthrough vs VirtIO”, a menudo se saltan la pregunta real: ¿qué capas extra estás añadiendo accidentalmente y estás midiendo lo correcto?
Ejemplo: una VM usa VirtIO, respaldada por una imagen QCOW2, sobre un sistema de archivos ext4 en LVM encima de un controlador RAID con caché de escritura que no verificaste. Luego alguien compara eso con NVMe passthrough y lo llama ciencia. Eso no es ciencia; es una tarta de capas con una guarnición de latencia.
El ganador “que nadie menciona” suele ser uno de estos:
- VirtIO con el modo correcto: virtio-scsi con múltiples colas, iothreads, modo de caché correcto y LUNs directos en lugar de archivos de imagen, puede acercarse mucho al passthrough para muchas cargas.
- Topología correcta de CPU e interrupciones: fijar vCPUs, alinear colas con cores y sacar interrupciones de tus vecinos ruidosos puede reducir drásticamente la latencia extremo sin cambiar el dispositivo de almacenamiento.
- Eliminar la capa de caché equivocada: la caché de páginas del host, la caché del invitado y la caché de escritura del dispositivo pueden interactuar de formas que parecen rápidas hasta que ocurre un fallo o una avalancha de flushes.
Una verdad seca: puedes comprar una unidad NVMe que haga millones de IOPS y aun así tener picos de 50 ms porque el hilo de finalización de E/S de tu VM está hambriento. El almacenamiento no es solo un problema de dispositivo; es un problema de planificación con máscara de disco.
Broma #1: Los benchmarks de almacenamiento son como los currículums: técnicamente ciertos, estratégicamente incompletos y, por lo general, faltando la parte donde se cae bajo presión.
Cuándo gana NVMe passthrough (y por qué)
El passthrough gana cuando tu cuello de botella es la sobrecarga de software en la capa de virtualización, y la carga es sensible a ello. Señales típicas:
- Te importa la latencia extremo (p99, p999) más que la latencia media.
- Realizas frecuentes fsync o escrituras síncronas pequeñas (bases de datos, colas de mensajes, sistemas de archivos con journal bajo carga).
- Tienes altas IOPS con tamaños de bloque pequeños, y VirtIO añade coste de CPU por E/S medible.
- Puedes dedicar un dispositivo a una VM sin llorar por la utilización.
Por qué es más rápido
NVMe ya es un protocolo basado en colas y de baja latencia. El passthrough permite al invitado enviar directamente a las colas del controlador. No hay hilo de QEMU que programar. No hay metadatos del sistema de archivos del host. Menos cambios de contexto. Menos locks. El invitado ve un dispositivo NVMe real, por lo que el kernel puede aplicar optimizaciones y funciones específicas de NVMe.
Dónde te complica
El passthrough no es una característica de “configurar y olvidar”. Cambia la física operativa:
- La migración en vivo se vuelve difícil o imposible (en el sentido habitual). Un dispositivo ligado a una VM no quiere teletransportarse.
- Los reinicios y errores del dispositivo son visibles en el invitado. Si el controlador NVMe se enfada, tu VM tiene asientos en primera fila.
- Compartir un dispositivo no es trivial a menos que tengas SR-IOV o namespaces NVMe diseñados para ello (y aun así, la gestión se complica).
- La seguridad/aislamiento depende de IOMMU. Si tu configuración de IOMMU es errónea, no estás haciendo passthrough; estás haciendo ingeniería de confianza.
Cuándo gana VirtIO (y por qué)
VirtIO gana cuando el sistema es más grande que una sola VM y un solo disco. Eso es la mayoría de los entornos de producción.
Funciones operativas que echarás de menos
- Migración en vivo es mucho más fácil con discos virtuales.
- Snapshots, backups, replicación son más sencillos cuando el almacenamiento es un artefacto gestionado (LV de LVM, RBD de Ceph, ZVOL, etc.).
- Overcommit y pooling se vuelven posibles. No siempre recomendable, pero a menudo requerido económicamente.
- Aplicación de políticas: puedes aplicar limitación, priorización de I/O y fronteras de inquilinos en el host o en el backend de almacenamiento.
El rendimiento no es automáticamente malo
El rendimiento de VirtIO suele ser excelente cuando evitas heridas autoinfligidas:
- Usa dispositivos de bloque crudos (LV de LVM, namespace NVMe expuesto como
/dev/…en el host) en lugar de QCOW2 sobre un sistema de archivos para cargas de alto rendimiento. - Usa virtio-scsi con múltiples colas si necesitas escala y concurrencia. virtio-blk puede estar bien, pero virtio-scsi suele ofrecer mayor flexibilidad con el encolado y el comportamiento del modelo de dispositivo.
- Añade iothreads para que la finalización de I/O no quede atrapada detrás del hilo principal de QEMU haciendo algo no relacionado.
- Elige el modo de caché correcto según tus requisitos de durabilidad, no por sensaciones.
Broma #2: Los valores por defecto de VirtIO son como las contraseñas por defecto: existen para que empieces, no para mantenerte seguro en producción.
Por qué los microbenchmarks mienten y la producción te castiga
Los benchmarks son necesarios. También frecuentemente mala praxis.
Los microbenchmarks suelen:
- Correr en un host inactivo con cachés calientes.
- Usar un único trabajo, profundidad de cola única y localidad perfecta.
- Medir la latencia media, no la latencia extremo.
- Ignorar el tiempo de CPU consumido por E/S, que es donde VirtIO puede “pagar” por su flexibilidad.
Las cargas en producción suelen:
- Tener operaciones mixtas de lectura/escritura y operaciones de metadata.
- Tener ráfagas que generan encolamiento.
- Competir por CPU con otros invitados y demonios del host.
- Experimentar flushes periódicos, commits de journal, trim/discard y mantenimiento en segundo plano.
El ganador de rendimiento que buscas suele ser una mejora en percentiles de latencia, no un número de IOPS llamativo. El passthrough puede mejorar p99 al reducir la incertidumbre del software. VirtIO puede mantener p99 estable al hacer el comportamiento del sistema manejable—si está afinado y si el backend no hace algo traicionero.
Una cita relevante aquí, porque las fallas de almacenamiento rara vez son “una sola cosa”:
“Everything fails, all the time.” — Werner Vogels
Tareas prácticas: comandos, salidas y decisiones (12+)
Estas son las tareas que realmente ejecuto cuando alguien dice: “el almacenamiento está lento.” Cada una incluye: comando, qué significa la salida y la decisión que tomas.
Task 1: Confirmar qué disco cree el invitado que tiene
cr0x@server:~$ lsblk -o NAME,MODEL,TRAN,ROTA,SIZE,TYPE,MOUNTPOINT
NAME MODEL TRAN ROTA SIZE TYPE MOUNTPOINT
vda Virtio Block Dev 0 200G disk
├─vda1 0 512M part /boot
└─vda2 0 199.5G part /
Significado: vda con modelo “Virtio Block Dev” es VirtIO. Si ves nvme0n1 y un modelo de proveedor real, probablemente sea passthrough.
Decisión: Si las expectativas de rendimiento asumen “NVMe”, alinea a los interesados: VirtIO no es NVMe, y “parecido a SSD” no es una especificación.
Task 2: Comprobar si el passthrough NVMe es realmente NVMe dentro del invitado
cr0x@server:~$ sudo nvme list
Node SN Model Namespace Usage Format FW Rev
/dev/nvme0n1 S5XXXXXXXXXXXX SAMSUNG MZVLB1T0HALR-00000 1 200.04 GB / 200.04 GB 512 B + 0 B EXF7
Significado: El invitado ve un dispositivo NVMe real y su modelo; el passthrough es plausible.
Decisión: Si necesitas características administrativas (SMART log, firmware, namespaces), el passthrough te da visibilidad; VirtIO a menudo lo oculta.
Task 3: En el host, verificar el binding VFIO para passthrough
cr0x@server:~$ lspci -nnk | grep -A3 -i nvme
5e:00.0 Non-Volatile memory controller [0108]: Samsung Electronics Co Ltd NVMe SSD Controller [144d:a808]
Subsystem: Samsung Electronics Co Ltd Device [144d:a801]
Kernel driver in use: vfio-pci
Kernel modules: nvme
Significado: “Kernel driver in use: vfio-pci” indica que el host entregó el dispositivo a VFIO, no al controlador nvme del host.
Decisión: Si el host sigue usando nvme, no estás haciendo passthrough; corrige el binding antes de discutir rendimiento.
Task 4: Identificar modelo de dispositivo VirtIO y colas desde el invitado
cr0x@server:~$ grep -H . /sys/block/vda/queue/nr_requests /sys/block/vda/queue/scheduler /sys/block/vda/queue/nr_hw_queues 2>/dev/null
/sys/block/vda/queue/nr_requests:128
/sys/block/vda/queue/scheduler:[none] mq-deadline kyber bfq
/sys/block/vda/queue/nr_hw_queues:1
Significado: Una cola de hardware significa paralelismo limitado; las opciones del scheduler muestran lo disponible, con none seleccionado actualmente.
Decisión: Si estás gestionando concurrencia y ves nr_hw_queues:1, considera virtio-scsi multi-queue o configurar multi-queue de virtio-blk en el host.
Task 5: Revisar scheduler y profundidad de cola del dispositivo de bloque en el host
cr0x@server:~$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq
Significado: Para NVMe, none suele ser correcto. Si ves BFQ en un backend NVMe de altas IOPS, puedes estar pagando latencia extra por una equidad que no pediste.
Decisión: Para NVMe dedicado que respalda una VM, prefiere none o mq-deadline según la carga; prueba con latencia p99.
Task 6: Medir distribución de latencia con fio (invitado)
cr0x@server:~$ fio --name=randread --filename=/dev/vda --direct=1 --ioengine=libaio --rw=randread --bs=4k --iodepth=32 --numjobs=4 --time_based --runtime=30 --group_reporting
randread: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=32
...
read: IOPS=180k, BW=703MiB/s (737MB/s)(20.6GiB/30001msec)
slat (nsec): min=900, max=150000, avg=4200, stdev=1900
clat (usec): min=70, max=22000, avg=680, stdev=1200
lat (usec): min=75, max=22010, avg=684, stdev=1200
clat percentiles (usec):
| 1.00th=[ 140], 5.00th=[ 180], 10.00th=[ 210], 50.00th=[ 410]
| 90.00th=[ 1500], 95.00th=[ 2600], 99.00th=[ 6000], 99.90th=[16000]
Significado: La media parece aceptable (avg=680us), pero p99/p99.9 es terrible. Ese es el tipo de “está bien” que quema bases de datos.
Decisión: Si la latencia extremo es alta, investiga planificación de CPU, iothreads, afinidad de IRQ, contención del host, modos de caché y comportamiento de writeback del backend. No persigas solo las IOPS.
Task 7: Determinar si estás pagando por la caché de páginas del host (host)
cr0x@server:~$ ps -eo pid,comm,%cpu,%mem,args | grep -E 'qemu-system|qemu-kvm' | head -n 1
2143 qemu-system-x86 175.2 8.1 /usr/bin/qemu-system-x86_64 ... -drive file=/var/lib/libvirt/images/vm01.qcow2,if=virtio,cache=writeback ...
Significado: cache=writeback implica que la caché de páginas del host puede estar involucrada. Eso puede ser rápido, hasta que la presión de memoria desencadena tormentas de writeback.
Decisión: Para cargas críticas de latencia, prefiere cache=none con O_DIRECT para reducir la interferencia de la caché del host (y asegura que entiendas el modelo de durabilidad).
Task 8: Comprobar presión de memoria del host y riesgo de writeback (host)
cr0x@server:~$ grep -E 'Dirty:|Writeback:|MemAvailable:' /proc/meminfo
MemAvailable: 2148320 kB
Dirty: 842112 kB
Writeback: 90112 kB
Significado: Dirty/Writeback altos en relación con la memoria disponible sugieren que el kernel está haciendo muchos flushes. Eso puede correlacionar con picos de latencia.
Decisión: Si Dirty es persistentemente alto, ajusta el comportamiento de writeback de las VMs, reduce el uso de caché del host o provisiona más RAM/margen. También verifica si tu backend de almacenamiento es lento absorbiendo escrituras.
Task 9: Identificar steal de CPU y problemas de planificación (invitado)
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (vm01) 02/04/2026 _x86_64_ (8 CPU)
02:10:01 AM CPU %usr %nice %sys %iowait %irq %soft %steal %idle
02:10:02 AM all 12.5 0.0 6.4 4.1 0.0 0.8 9.7 66.5
02:10:02 AM 0 15.0 0.0 8.0 2.0 0.0 0.5 18.0 56.5
Significado: %steal indica que el hipervisor está quitando tiempo de CPU. Steal alto significa que la ruta de finalización de I/O puede retrasarse incluso si el disco está bien.
Decisión: Si steal es notable durante picos de latencia, arregla la contención de CPU (pinning, reservas, reducir overcommit). Cambiar controladores de almacenamiento no curará el hambre del planificador.
Task 10: Inspeccionar interrupciones y confirmar distribución (host o invitado)
cr0x@server:~$ cat /proc/interrupts | egrep 'nvme|virtio|vfio' | head
45: 1203941 0 0 0 IR-PCI-MSI 327680-edge nvme0q0
46: 893220 0 0 0 IR-PCI-MSI 327681-edge nvme0q1
47: 901144 0 0 0 IR-PCI-MSI 327682-edge nvme0q2
48: 876990 0 0 0 IR-PCI-MSI 327683-edge nvme0q3
Significado: Existen múltiples colas NVMe (q0..q3). Si todas las interrupciones incrementan solo en CPU0, tienes un problema de afinidad.
Decisión: Si las interrupciones están concentradas, configura la afinidad de IRQ o activa irqbalance con una política que no sabotee CPUs sensibles a la latencia.
Task 11: Diferenciar espera de I/O vs saturación real del dispositivo (invitado)
cr0x@server:~$ iostat -x 1 3
Linux 6.5.0 (vm01) 02/04/2026 _x86_64_ (8 CPU)
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s w_await wareq-sz aqu-sz %util
vda 4500.0 18000.0 0.0 0.0 1.20 4.00 1200.0 9600.0 2.40 8.00 4.10 92.0
Significado: %util cercano a 100% sugiere que la cola del dispositivo está ocupada. r_await/w_await muestran tiempo en la cola del dispositivo, no solo CPU.
Decisión: Si %util es alto y las awaits suben, estás cerca del límite del dispositivo o backend; considera más colas, un backend más rápido o fragmentar la carga entre dispositivos. Si %util es bajo pero la latencia es alta, busca problemas de planificación/caché/bloqueo.
Task 12: Confirmar comportamiento de discard/TRIM (invitado)
cr0x@server:~$ lsblk -D
NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
vda 0 512B 2G 0
Significado: La granularidad y el máximo de discard indican si TRIM/discard está soportado y en qué tamaños.
Decisión: Si discard está habilitado sin querer en un backend que lo maneja mal (algunos sistemas thin-provisioned o en red), puede causar picos de latencia. Programa fstrim en horas bajas o desactiva la opción discard en el montaje.
Task 13: Comprobar modelo de hilos de QEMU y presencia de iothreads (host)
cr0x@server:~$ ps -T -p $(pgrep -n qemu-system) -o spid,comm,pcpu | head
2143 qemu-system-x86 98.4
2160 IO iothread0 35.2
2161 CPU 0 12.1
2162 CPU 1 11.7
Significado: La presencia de IO iothread0 sugiere que tienes un hilo de I/O dedicado; si falta, las E/S pueden compartir el bucle principal de eventos de QEMU.
Decisión: Para altas IOPS o VirtIO sensible a latencia, añade iothreads y aísla su colocación de CPU para que no compitan con trabajo ruidoso.
Task 14: Verificar que el backend sea bloque crudo, no QCOW2 sobre sistema de archivos (host)
cr0x@server:~$ virsh domblklist vm01
Target Source
------------------------------------------------
vda /var/lib/libvirt/images/vm01.qcow2
Significado: QCOW2 añade sobrecarga de metadata y riesgo de fragmentación; puede estar bien para muchos usos, pero no es gratis a altas tasas de escritura.
Decisión: Si buscas latencia baja consistente, mueve discos calientes a LVs crudos o dispositivos de bloque dedicados; deja QCOW2 para discos de arranque y conveniencia cuando proceda.
Guía rápida de diagnóstico
Esta es la secuencia “tienes 20 minutos antes de que la llamada de incidentes se ponga fea”. El objetivo es localizar la capa cuello de botella rápidamente, no crear un benchmark perfecto.
Primero: decide si es saturación del dispositivo o jitter de planificación
- Revisa iostat del invitado: si
%utiles alto yawaitsube con la carga, sospecha saturación del backend. - Revisa
%stealdel invitado: si%stealse dispara durante picos de latencia, sospecha contención de CPU del host. - Revisa percentiles de fio: si la media está bien pero p99/p99.9 es horrible, sospecha encolamiento y contención, no velocidad bruta del dispositivo.
Segundo: identifica la ruta de I/O y quita la capa más sospechosa
- Si estás en VirtIO con QCOW2: ese es tu primer sospechoso. Cambia el disco caliente a bloque crudo/LV como prueba.
- Si estás en VirtIO sin iothreads: añade iothreads y vuelve a medir p99.
- Si estás en passthrough y aún lento: deja de culpar a VirtIO; mira afinidad de IRQ, ajustes del kernel invitado y características de energía/latencia de NVMe.
Tercero: confirma la salud del backend del host
- dmesg del host para errores NVMe, reinicios, timeouts.
- Presión de memoria del host (Dirty/Writeback), que puede crear tormentas de flush.
- Uso de CPU del host por los hilos de QEMU; si el hilo de I/O está saturado o hambriento, encontraste al villano.
Si haces solo una cosa: mide la latencia p99 y el %steal de CPU mientras reproduces el problema. Ese par te dice si luchas contra el almacenamiento o contra la planificación.
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: “VirtIO es lento, así que necesitamos passthrough”
Causa raíz: Disco VirtIO respaldado por QCOW2 en un sistema de archivos con caché de host y picos de writeback; además sin iothreads.
Solución: Pone discos calientes en bloque crudo (LV, RBD o dispositivo directo), usa cache=none cuando proceda, habilita iothreads y valida la configuración de colas. Luego compara de nuevo.
2) Síntoma: Latencia media excelente, p99.9 terrible en picos
Causa raíz: Contención de CPU: hilo de I/O de QEMU o vCPU desplanificado; IRQs se acumulan en un solo core; reclamación/writeback del host.
Solución: Fija iothreads y vCPUs, arregla afinidad de IRQ, reduce overcommit y asegura margen de memoria en el host. Verifica con mpstat y /proc/interrupts.
3) Síntoma: VM con passthrough es rápida hasta que deja de serlo; luego se cae en picado
Causa raíz: Reinicio del controlador NVMe, quirk de firmware o error PCIe propagado directamente al invitado; la recuperación es visible y brutal para el invitado.
Solución: Valida firmware, revisa logs AER de PCIe, monitoriza contadores de error NVMe y diseña para fallo (replicación, clustering). El passthrough reduce capas; también reduce la amortiguación.
4) Síntoma: Picos de latencia cada pocos minutos como reloj
Causa raíz: Flush/journal commit periódico, tareas fstrim/discard, o umbrales de writeback del host provocando ráfagas.
Solución: Programa trims, ajusta parámetros de writeback, evita doble-caché y asegura que las opciones de durabilidad coincidan con las expectativas de la base de datos (no “optimices” los flushes a menos que te guste perder datos).
5) Síntoma: “Aumentamos la profundidad de cola y empeoró”
Causa raíz: Demasiado encolamiento incrementa la latencia; estás simplemente construyendo una sala de espera mayor. También es posible contención por locks o saturación del backend amplificada por la concurrencia.
Solución: Ajusta iodepth para que coincida con el dispositivo y la carga; mide latencia extremo. Para bases de datos, una iodepth menor suele mejorar p99 aunque las IOPS máximas bajen.
6) Síntoma: La migración en vivo funciona, pero el rendimiento varía entre hosts
Causa raíz: Modelos de CPU diferentes, topología NUMA, políticas de balanceo de IRQ o modelos/firmware NVMe distintos en el clúster.
Solución: Estandariza perfiles de host, fija interrupciones/hilos de forma consistente y trata “mismo tipo de instancia” como un contrato operativo, no como marketing.
Tres microhistorias corporativas desde las trincheras del almacenamiento
Microhistoria 1: El incidente causado por una suposición equivocada
Una empresa mediana operaba una flota de VMs con agentes de compilación y cachés de artefactos. Las compilaciones eran “aleatoriamente lentas”, ese tipo de síntoma que hace que todos culpen la red primero y el almacenamiento después. Alguien notó que los hosts tenían brillantes NVMe y concluyó que las VMs estaban “en NVMe”. Esa frase sobrevivió el tiempo suficiente para convertirse en un sistema de creencias.
Durante una semana de lanzamiento, los tiempos de compilación se duplicaron. El equipo on-call no vio saturación evidente del disco. Las IOPS estaban bien. La latencia media estaba bien. Pero la cola larga era desagradable. La herramienta de compilación pasaba mucho tiempo esperando operaciones de metadata en archivos pequeños y fsyncs síncronos, el tipo de carga que castiga la jitter.
La suposición equivocada fue simple: las VMs usaban discos VirtIO respaldados por imágenes QCOW2 en un ext4. La caché de páginas del host hizo que el sistema pareciera rápido con poca carga, y luego la presión de memoria forzó writeback en los peores momentos. El “NVMe” era real, pero estaba enterrado bajo una pila de sobrecarga y contención.
La solución no fue heroica. Movieron los discos calientes a LVs crudos, fijaron el modo de caché deliberadamente y añadieron iothreads. La latencia extremo mejoró lo suficiente como para que la ralentización de las compilaciones desapareciera. La corrección cultural también importó: dejaron de usar “en NVMe” como promesa de rendimiento y empezaron a especificar la ruta real.
Microhistoria 2: La optimización que salió mal
Un equipo financiero tenía una VM de base de datos que luchaba constantemente con la latencia p99. Eligieron NVMe passthrough porque los benchmarks eran hermosos. Y durante semanas, fue así. Menor latencia, menos CPU, menos misterios.
Entonces un host experimentó un pequeño tropiezo PCIe. Nada dramático: un error transitorio, un reentrenamiento de enlace, el tipo de evento que ocurre en centros de datos reales cuando apilas hardware de alta densidad y finges que la física es opcional. El controlador NVMe se reinició. En metal desnudo, el OS se recuperó tras una pausa. En la VM con passthrough, la pausa pareció que el almacenamiento desaparecía en medio de la frase.
La base de datos reaccionó exactamente como está diseñada: entró en pánico por errores de I/O, marcó dispositivos sospechosos y desencadenó un failover. El failover funcionó, pero fue ruidoso: tormentas de reconexión, calentamiento de caché y una cascada de tickets “por qué todo está lento”. La optimización había movido el límite de fallo de “lo absorbe el host” a “lo experimenta el invitado”.
Conservaron el passthrough, pero solo después de añadir resiliencia aburrida: replicación de base de datos afinada para failover rápido, alertas en contadores de reset NVMe y un runbook que asumía que el dispositivo podía desaparecer. La lección no fue “passthrough es malo”. Fue “passthrough es honesto”. Los sistemas honestos te muestran los cantos afilados que antes ignorabas.
Microhistoria 3: La práctica aburrida pero correcta que salvó el día
Una compañía SaaS ejecutaba cargas mixtas en un clúster de virtualización. Estandarizaron en VirtIO para la mayoría de VMs porque necesitaban migración y flexibilidad operativa. Nada exótico. Pero hicieron algo que suena aburrido y por eso es raro: mantuvieron un perfil de rendimiento de almacenamiento por host.
Cada host tenía una ejecución base de fio al ser comisionado, capturando no solo IOPS sino percentiles de latencia a varios valores de iodepth. La repetían después de actualizaciones de firmware, upgrades de kernel y cambios de hardware. Los resultados se guardaban junto con la metadata del host, así que “este host es raro” podía demostrarse en minutos.
Un día, tras un ciclo de mantenimiento rutinario, un subconjunto de hosts empezó a mostrar picos intermitentes de p99 en VMs normales. El equipo comparó los baselines y vio inmediatamente un cambio: el comportamiento de writeback del host y el consumo CPU de hilos QEMU eran distintos. No “roto”, solo diferente lo suficiente para causar problemas de latencia extremo a ciertos inquilinos.
Revirtieron una combinación específica de ajustes del kernel y ajustaron su política de pinning de iothreads. El incidente no se convirtió en una sala de guerra de una semana porque el equipo tenía un baseline aburrido y trataba el rendimiento como una propiedad monitorizada, no como una anécdota. La acción salvadora no fue tuning ingenioso. Fue hacer medible lo “normal”.
Listas de verificación / plan paso a paso
Checklist de decisión: ¿debería esta VM obtener NVMe passthrough?
- ¿La carga es crítica en latencia en p99/p99.9? Si no, no te molestes.
- ¿Puedes dedicar un dispositivo (o namespace) a esta VM? Si no, el passthrough se convertirá en una pelea por asignación de recursos.
- ¿Puedes vivir sin migración en vivo? Si no, VirtIO gana por defecto.
- ¿Tienes madurez operativa con IOMMU y VFIO? Si no, aprenderás en medio de un incidente. Ese es el peor momento para aprender.
- ¿Tienes un diseño para manejar fallos? El passthrough hace visibles al invitado los reinicios y errores de dispositivo. Planifica para ello.
Checklist de “hacer bien” con VirtIO (valores prácticos por defecto)
- Respaldar discos calientes con bloque crudo cuando sea posible (LV, ZVOL, RBD) en lugar de QCOW2 sobre sistema de archivos.
- Usar virtio-scsi cuando necesites multi-queue y flexibilidad; valida el número de colas en el invitado.
- Añadir iothreads y fijarlos a CPUs estables. No dejes que la ruta de I/O compita con tareas de emulación.
- Elegir el modo de caché intencionalmente:
- cache=none para I/O directo y menor jitter de caché del host.
- cache=writeback solo cuando entiendas la compensación entre durabilidad y presión de memoria.
- Validar la latencia extremo con percentiles de fio, no solo la media.
Checklist de “usar passthrough de forma segura”
- Confirma que el dispositivo está ligado a
vfio-pcien el host. - Verifica que IOMMU esté habilitado y tengas grupos IOMMU sensatos (sin compartir dispositivos inesperados).
- Fija vCPUs y maneja la afinidad de IRQ para que las interrupciones NVMe no se concentren en un solo core.
- Monitoriza logs de error NVMe y reinicios; trátalos como señales predictivas.
- Diseña la aplicación/cluster para fallos a nivel de dispositivo (replicación, pruebas de failover).
Preguntas frecuentes (FAQ)
1) ¿El passthrough NVMe siempre es más rápido que VirtIO?
No. A menudo es más rápido para I/O pequeño sensible a latencia porque elimina la sobrecarga del host. Pero VirtIO puede igualar o superar el rendimiento de throughput en algunas configuraciones, y suele ganar operativamente.
2) ¿Cuál es el cambio más simple en VirtIO que aporta mejoras reales de rendimiento?
Deja de poner discos de alta escritura y altas IOPS en imágenes QCOW2 sobre un sistema de archivos general. Mueve discos calientes a bloque crudo y añade iothreads.
3) ¿VirtIO-blk o virtio-scsi?
virtio-scsi suele ser la mejor opción cuando quieres escalado multi-queue y un camino maduro para comportamientos complejos de almacenamiento. virtio-blk puede ser más simple y rápido, pero verifica límites de colas y las opciones de configuración de tu hipervisor.
4) ¿Una iodepth “tan alta como sea posible” hace que NVMe sea más rápido?
Hace las colas más profundas. Eso no es lo mismo. Alta iodepth puede aumentar throughput pero destruir la latencia extremo. Ajusta iodepth según p99, no por ego.
5) ¿Por qué la latencia p99 es mala incluso cuando %util es bajo?
Porque tu cuello de botella probablemente sea planificación o contención: steal de CPU, afinidad de IRQ, un hilo principal de QEMU ocupado, reclaim de memoria del host o contención por locks en la ruta de I/O. Baja utilización del dispositivo no implica baja latencia extremo.
6) ¿Puedo migrar en vivo una VM con passthrough NVMe?
No en la forma normal de “mover la VM en funcionamiento a otro host”. Existen enfoques especializados, pero si la migración en vivo es un requisito firme, VirtIO es la respuesta práctica.
7) ¿Es seguro el passthrough en entornos multi-inquilino?
Pueden serlo, si el aislamiento por IOMMU es correcto y controlas operativamente la asignación de dispositivos. Pero reduce la capacidad del host para aplicar políticas y aumenta el radio de impacto cuando un dispositivo falla.
8) ¿Y usar NVMe-oF o dispositivos de bloque en red?
El almacenamiento en red puede ser excelente, pero introduce jitter de red y modos de fallo diferentes. También cambia quién gestiona el problema de encolamiento. Mide la latencia p99 de extremo a extremo y decide según la sensibilidad de tu carga.
9) Si solo me importa el throughput (MB/s), ¿qué debo elegir?
VirtIO con un backend bien configurado suele ofrecer suficiente throughput, a veces limitado más por CPU que por disco. Si haces I/O secuencial grande, la diferencia entre passthrough y VirtIO puede ser menor de lo que imaginas.
10) ¿Cuál era el “verdadero ganador” otra vez?
El verdadero ganador es eliminar la complejidad accidental: capas de imagen innecesarias, modos de caché malos, iothreads faltantes y topologías CPU/IRQ desalineadas. El passthrough es una forma de quitar capas. Afinar VirtIO es otra.
Próximos pasos prácticos
- Elige un perfil fio representativo para tu carga (mezcla lecturas/escrituras, incluye fsync donde sea relevante) y registra latencias p50/p95/p99/p99.9.
- Mapea tu ruta real de I/O: tipo de dispositivo en el invitado, modo de caché en el host, tipo de backing store (raw vs QCOW2) y scheduler del dispositivo backend.
- Arregla las victorias fáciles primero: bloque crudo para discos calientes, iothreads, modo de caché correcto y colocación de CPU/IRQ. Vuelve a medir.
- Solo entonces considera NVMe passthrough para las pocas VMs que realmente lo necesitan—y solo si aceptas los compromisos operativos.
- Institucionaliza baselines: guarda perfiles de rendimiento de almacenamiento de host y VM para detectar regresiones antes que tus clientes.
Si quieres una directiva tajante: trata la virtualización de almacenamiento como un problema de presupuesto de latencia, no como un problema de selección de controlador. El presupuesto se gasta en capas. Gástalo con intención.