Certaines régressions de performance sont évidentes. Un CPU bloqué à 100%. Un lien réseau qui s’effondre. Le stockage est plus sournois : il échoue silencieusement, dans les interstices entre microsecondes. Votre VM « a un disque NVMe », disent vos tableaux de bord, les IOPS semblent corrects, et pourtant la latence d’extrémité ruine votre base de données, votre ferme de compilation ou votre pipeline de logs.
Le débat habituel se présente comme un match de boxe : NVMe passthrough (VFIO) contre VirtIO. La vérité gênante : le gagnant n’est souvent aucun des deux. Le gagnant, c’est de supprimer la couche supplémentaire que vous n’aviez pas remarquée, et d’ajuster correctement la couche que vous ne pouvez pas supprimer. C’est pourquoi les équipes perdent souvent ce combat en production alors qu’elles le gagnent dans des benchmarks.
Ce qui change réellement entre passthrough et VirtIO
À un niveau élevé :
- NVMe passthrough (VFIO PCIe passthrough) donne à une VM invitée un accès direct à un contrôleur NVMe physique. L’invité charge le pilote natif
nvme, possède les files d’attente, émet des commandes d’administration et « voit » quelque chose de proche du bare metal. - VirtIO donne à l’invité un périphérique paravirtualisé (virtio-blk ou virtio-scsi). L’invité soumet des requêtes à une virtqueue ; l’hôte (QEMU + vhost + couche bloc du noyau) les complète via un périphérique de backing ou un fichier.
Mais l’histoire de la performance concerne surtout la longueur du chemin et les points d’ordonnancement :
- Combien de fois la requête E/S traverse-t-elle la frontière user/kernel ?
- Combien de files d’attente existent, et sont-elles mappées de façon sensée aux cœurs CPU ?
- Où la requête peut-elle être retardée par de la contention : verrou, interruption, limitation cgroup, cache de pages de l’hôte, journalisation du système de fichiers, ou un thread du hyperviseur qui a été désordonnancé ?
Les chemins réels d’E/S (simplifiés)
VirtIO (virtio-blk courant avec QEMU) :
- L’application invitée lance une E/S (syscall).
- La couche bloc du noyau invité l’ordonnance.
- Le pilote VirtIO poste des descripteurs dans une virtqueue.
- Sortie vers l’hôte (virtio kick / interruption), QEMU/vhost traite la requête.
- La couche bloc du noyau hôte soumet au périphérique physique (ou au système de fichiers si vous utilisez des fichiers image).
- La complétion remonte ; l’invité reçoit une interruption ; l’application reprend.
NVMe passthrough :
- L’application invitée lance une E/S (syscall).
- Le pilote NVMe du noyau invité soumet directement à la file matérielle (via PCIe MMIO/DMA).
- Le matériel complète ; interruption vers l’invité (MSI-X), complétion gérée dans l’invité.
Le passthrough supprime la pile bloc de l’hôte du chemin chaud. Cela peut être énorme pour la latence. Il supprime aussi des points de contrôle côté hôte dont vous pourriez avoir besoin, comme la mise en cache hôte, les snapshots, la migration à chaud flexible et la multiplexation facile du stockage.
Voici la règle que j’utilise en production : si votre charge est sensible à la latence et prévisible, le passthrough est attractif. Si votre charge est mixte, multi-tenant et opérationnellement désordonnée, VirtIO est généralement le compromis correct—à moins que vous l’exécutiez en configuration par défaut puis que vous vous demandiez pourquoi ce n’est pas magique.
Faits historiques et contexte intéressant (ce qui explique aujourd’hui)
La virtualisation du stockage n’est pas devenue « compliquée » pour le plaisir. Elle est devenue compliquée parce que les systèmes réels le sont. Quelques points concrets de contexte qui comptent lorsque vous choisissez entre passthrough et VirtIO :
- VirtIO est né de la douleur : les premières émulations complètes de périphériques (comme IDE) étaient lentes car chaque E/S ressemblait à une fête d’interruptions matérielles en logiciel.
- NVMe a standardisé le multi-queue dès le départ, conçu pour le parallélisme. Cela s’adapte aux CPU modernes ; cela signifie aussi que le mappage des files et le routage des interruptions importent beaucoup plus que pour l’ancien SATA.
- MSI-X a rendu les IOPS élevées possibles en supportant plusieurs interruptions par périphérique. C’est pourquoi « un disque » peut se répartir sur plusieurs cœurs, et pourquoi une mauvaise affinité d’interruption peut vous pourrir la journée.
- Linux blk-mq a changé la donne : la couche bloc multi-file d’attente a réduit la contention par verrou et amélioré l’évolutivité, mais elle a aussi ajouté de nouveaux réglages et de nouvelles façons de malconfigurer.
- vhost a été créé pour sortir QEMU du chemin de données : déplacer le travail du datapath dans le noyau a réduit les switches de contexte et amélioré le débit pour le réseau et le stockage virtio.
- Les ordonneurs d’E/S sont passés de « choisir un » à « ne rien choisir » pour NVMe : pour les périphériques rapides, les ordonneurs peuvent ajouter de la latence sans réel bénéfice, donc « none » gagne souvent.
- On benchmarkait autrefois en 4k random read et on proclamait victoire. Les services modernes sont généralement un mix de lectures, écritures, fsyncs, opérations de métadonnées et de rafales—donc la culture des anciens benchmarks induit encore en erreur.
- Les hyperviseurs cloud ont normalisé VirtIO : les fonctionnalités opérationnelles (migration, snapshots, contrôles de tenancy) contaient autant que la vitesse brute, donc VirtIO a gagné en étant praticable.
- Le passthrough est devenu pratique à grande échelle quand l’IOMMU est devenu banal : VFIO et l’isolation IOMMU l’ont rendu moins effrayant, mais « moins effrayant » n’est pas synonyme de « sans compromis ».
Le gagnant de performance que personne ne mentionne : moins de couches, moins de mensonges
Lorsque les équipes discutent « passthrough vs VirtIO », elles sautent souvent la vraie question : quelles couches supplémentaires ajoutez-vous accidentellement, et mesurez-vous la bonne chose ?
Exemple : une VM utilise VirtIO, supportée par une image QCOW2, posée sur un système de fichiers ext4 sur LVM au-dessus d’un contrôleur RAID avec un cache en écriture que vous n’avez pas vérifié. Puis quelqu’un compare cela au passthrough NVMe et appelle ça de la science. Ce n’est pas de la science ; c’est un millefeuille avec une garniture latence.
Le gagnant « que personne ne mentionne » est généralement l’un de ces éléments :
- VirtIO bien configuré : virtio-scsi avec files multiples, iothreads, mode de cache correct et LUNs directs au lieu de fichiers image, peut atteindre la portée du passthrough pour beaucoup de charges de travail.
- Topologie CPU et interruptions correcte : épingler les vCPU, aligner les files sur les cœurs, et sortir les interruptions de vos voisins bruyants peut réduire dramatiquement la latence d’extrémité sans changer le périphérique de stockage du tout.
- Supprimer la mauvaise couche de cache : le cache de pages de l’hôte, le cache de pages de l’invité et le cache d’écriture du périphérique peuvent interagir de façons qui semblent rapides jusqu’à ce que vous rencontriez un crash ou un orage de flush.
Une vérité sèche : vous pouvez acheter un disque NVMe qui fait des millions d’IOPS et obtenir quand même des pics à 50 ms parce que le thread de complétion E/S de votre VM est affamé. Le stockage n’est pas seulement un problème de périphérique ; c’est un problème d’ordonnancement portant un masque de disque.
Blague #1 : Les benchmarks de stockage sont comme des CV : techniquement vrais, stratégiquement incomplets, et souvent sans la partie où ça s’effondre sous pression.
Quand le passthrough NVMe l’emporte (et pourquoi)
Le passthrough gagne quand votre goulot d’étranglement est le surcoût logiciel dans la couche de virtualisation, et que la charge y est sensible. Signes typiques :
- Vous vous préoccupez de la latence d’extrémité (p99, p999) plus que de la latence moyenne.
- Vous faites fréquemment des fsync ou de petites écritures synchrones (bases de données, queues de messages, systèmes de fichiers journaling sous charge).
- Vous avez des IOPS élevés avec de petites tailles de bloc, et VirtIO ajoute un coût CPU mesurable par E/S.
- Vous pouvez dédier un périphérique à une VM sans pleurer sur l’utilisation.
Pourquoi c’est plus rapide
NVMe est déjà un protocole basé sur des files d’attente et à faible latence. Le passthrough laisse l’invité soumettre directement aux files du contrôleur. Pas de thread QEMU à ordonnancer. Pas de métadonnées de système de fichiers hôte. Moins de switches de contexte. Moins de verrous. L’invité voit un vrai périphérique NVMe, donc le noyau peut appliquer des optimisations et fonctionnalités spécifiques à NVMe.
Où ça vous mord
Le passthrough n’est pas une fonctionnalité « configurer et oublier ». Il change la physique opérationnelle :
- La migration à chaud devient difficile ou impossible (au sens habituel). Un périphérique lié à une VM ne veut pas téléporter.
- Les réinitialisations et erreurs de périphérique deviennent visibles dans l’invité. Si le contrôleur NVMe fait des siennes, votre VM a une place au premier rang.
- Partager un périphérique est non trivial sauf si vous avez SR-IOV ou des namespaces NVMe conçus pour cela (et même là, la complexité de gestion augmente).
- La sécurité/l’isolation dépend de l’IOMMU. Si votre configuration IOMMU est incorrecte, vous ne faites pas du passthrough ; vous faites de l’ingénierie de confiance aveugle.
Quand VirtIO l’emporte (et pourquoi)
VirtIO l’emporte quand le système est plus grand qu’une seule VM et un seul disque. C’est la plupart des environnements de production.
Fonctionnalités opérationnelles qui vous manqueront
- La migration à chaud est beaucoup plus facile avec des disques virtuels.
- Snapshots, sauvegardes, réplication sont plus simples quand le stockage est un artefact géré (LVM LV, Ceph RBD, ZVOL, etc.).
- Surcharge et pool deviennent possibles. Pas toujours judicieux, mais souvent économiquement nécessaire.
- Application de politiques : vous pouvez appliquer throttling, priorité I/O et frontières de locataires sur l’hôte ou le backend de stockage.
La performance n’est pas automatiquement mauvaise
La performance VirtIO est souvent excellente quand vous évitez les blessures auto-infligées :
- Utilisez des périphériques bloc bruts (LV LVM, namespace NVMe exposé comme /dev/… sur l’hôte) plutôt que QCOW2 sur un système de fichiers pour les charges haute performance.
- Utilisez virtio-scsi avec files multiples si vous avez besoin d’échelle et de concurrence. virtio-blk peut être suffisant, mais virtio-scsi tend à offrir plus de flexibilité pour le queueing et le comportement du modèle de périphérique.
- Ajoutez des iothreads afin que la complétion I/O ne soit pas bloquée derrière le thread principal de QEMU qui fait autre chose.
- Choisissez le bon mode de cache en fonction de vos exigences de durabilité, pas en fonction d’impressions.
Blague #2 : Les paramètres par défaut de VirtIO sont comme des mots de passe par défaut : ils servent à démarrer, pas à rester sûr en production.
Pourquoi les microbenchmarks mentent et la production vous punit
Les benchmarks sont nécessaires. Ils relèvent aussi souvent de la mauvaise pratique.
Les microbenchmarks :
- Tourent sur un hôte inactif avec des caches chauds.
- Utilisent un seul job, une seule profondeur de file et une localité parfaite.
- Mesurent la latence moyenne, pas la latence d’extrémité.
- Ignorent le temps CPU consommé par E/S, où VirtIO peut « payer » pour sa flexibilité.
Les charges de production :
- Sont mixtes en lectures/écritures et opérations de métadonnées.
- Ont des rafales qui causent des files d’attente.
- Concurrent pour le CPU avec d’autres invités et démons hôtes.
- Subissent des flushs périodiques, commits de journal, trim/discard et maintenance en arrière-plan.
Le gagnant de performance que vous cherchez est souvent une amélioration des percentiles de latence, pas un chiffre IOPS en gros titre. Le passthrough peut améliorer le p99 en réduisant le jitter logiciel. VirtIO peut maintenir le p99 stable en rendant le comportement du système gérable—si on le règle et si le backend ne fait pas quelque chose de sournois.
Une citation pertinente, car les pannes de stockage sont rarement « une seule chose » :
« Tout échoue, tout le temps. » — Werner Vogels
Tâches pratiques : commandes, sorties et décisions (12+)
Voici les tâches que j’exécute réellement quand quelqu’un dit « le stockage est lent ». Chacune inclut : la commande, ce que la sortie signifie, et la décision à prendre.
Tâche 1 : Confirmer ce que l’invité pense du disque
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 /
Signification : vda avec le modèle « Virtio Block Dev » est VirtIO. Si vous voyez nvme0n1 et un vrai modèle fournisseur, c’est probablement du passthrough.
Décision : Si les attentes de performance supposent « NVMe », alignez les parties prenantes : VirtIO n’est pas NVMe, et « type SSD » n’est pas une spécification.
Tâche 2 : Vérifier si le passthrough NVMe est réellement NVMe à l’intérieur de l’invité
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
Signification : L’invité voit un vrai périphérique NVMe et un modèle ; le passthrough est plausible.
Décision : Si vous avez besoin de fonctions d’administration (SMART log, firmware, namespaces), le passthrough vous donne de la visibilité ; VirtIO les masque souvent.
Tâche 3 : Sur l’hôte, vérifier le binding VFIO pour le 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
Signification : « Kernel driver in use: vfio-pci » indique que l’hôte a remis le périphérique à VFIO, pas au pilote nvme de l’hôte.
Décision : Si l’hôte utilise encore nvme, vous ne faites pas de passthrough ; corrigez le binding avant de discuter de performance.
Tâche 4 : Identifier le modèle de périphérique VirtIO et les files depuis l’invité
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
Signification : Une seule file matérielle signifie un parallélisme limité ; les options d’ordonnanceur montrent ce qui est disponible, avec none actuellement sélectionné.
Décision : Si vous poussez la concurrence et voyez nr_hw_queues:1, envisagez virtio-scsi multi-queue ou la configuration multi-queue de virtio-blk côté hôte.
Tâche 5 : Vérifier l’ordonnanceur bloc côté hôte et la profondeur de file
cr0x@server:~$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq
Signification : Pour NVMe, none est souvent correct. Si vous voyez BFQ sur un backend NVMe à haut IOPS, vous payez peut-être une latence supplémentaire pour une équité que vous n’avez pas demandée.
Décision : Pour un NVMe dédié soutenant une VM, préférez none ou mq-deadline selon la charge ; testez avec la latence p99.
Tâche 6 : Mesurer la distribution de latence avec fio (invité)
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]
Signification : La moyenne semble correcte (avg=680us), mais p99/p99.9 est affreux. C’est le type de « tout va bien » qui brûle les bases de données.
Décision : Si la latence d’extrémité est élevée, investiguez l’ordonnancement CPU, les iothreads, l’affinité IRQ, la contention hôte, les modes de cache et le comportement d’écriture du backend. Ne poursuivez pas seulement les IOPS.
Tâche 7 : Déterminer si vous payez le cache de pages de l’hôte (hôte)
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 ...
Signification : cache=writeback implique que le cache de pages de l’hôte peut être impliqué. Ça peut être rapide, jusqu’à ce que la pression mémoire déclenche des orages de writeback.
Décision : Pour les charges critiques en latence, préférez cache=none avec O_DIRECT pour réduire l’interférence du cache hôte (et assurez-vous que votre modèle de durabilité est compris).
Tâche 8 : Vérifier la pression mémoire hôte et le risque de writeback (hôte)
cr0x@server:~$ grep -E 'Dirty:|Writeback:|MemAvailable:' /proc/meminfo
MemAvailable: 2148320 kB
Dirty: 842112 kB
Writeback: 90112 kB
Signification : Dirty/Writeback élevés par rapport à la mémoire disponible suggèrent que le noyau flushe beaucoup. Cela peut corréler avec des pics de latence.
Décision : Si Dirty est persistamment élevé, ajustez le comportement de writeback des VM, réduisez l’usage du cache hôte, ou fournissez plus de RAM/marge. Vérifiez aussi si votre backend de stockage est lent à absorber les écritures.
Tâche 9 : Identifier le CPU steal et les problèmes d’ordonnancement (invité)
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
Signification : %steal indique que l’hyperviseur prend du temps CPU. Un steal élevé signifie que le chemin de complétion I/O peut être retardé même si le disque est correct.
Décision : Si le steal est non négligeable lors des pics de latence, corrigez la contention CPU (épingler, réservations, réduire l’overcommit). Changer de pilote de stockage ne guérira pas la famine d’ordonnancement.
Tâche 10 : Inspecter les interruptions et confirmer la distribution (hôte ou invité)
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
Signification : Plusieurs files NVMe (q0..q3) existent. Si toutes les interruptions s’incrémentent uniquement sur le CPU0, vous avez un problème d’affinité.
Décision : Si les interruptions sont concentrées, définissez l’affinité IRQ ou activez irqbalance avec une politique qui ne sabote pas les CPU sensibles à la latence.
Tâche 11 : Distinguer IO wait et saturation réelle du périphérique (invité)
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
Signification : %util proche de 100% suggère que la file du périphérique est occupée. r_await/w_await montrent le temps passé en attente dans la file du périphérique, pas seulement le CPU.
Décision : Si %util est élevé et que les awaits montent, vous êtes proche des limites du périphérique ou du backend ; envisagez plus de files, un backend plus rapide ou shardez sur plusieurs périphériques. Si %util est faible mais la latence élevée, cherchez des problèmes d’ordonnancement/caching/verrous.
Tâche 12 : Confirmer le comportement discard/TRIM (invité)
cr0x@server:~$ lsblk -D
NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
vda 0 512B 2G 0
Signification : La granularité de discard et le maximum indiquent si TRIM/discard est supporté et à quelles tailles.
Décision : Si discard est activé involontairement sur un backend qui le gère mal (certains systèmes thin-provisioned ou réseau), cela peut causer des pics de latence. Planifiez fstrim hors pic ou désactivez l’option discard au montage.
Tâche 13 : Vérifier le modèle de threads QEMU et la présence d’iothreads (hôte)
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
Signification : La présence de IO iothread0 suggère que vous avez un thread I/O dédié ; s’il est absent, l’I/O peut partager la boucle d’événements principale de QEMU.
Décision : Pour des IOPS élevés ou VirtIO sensible à la latence, ajoutez des iothreads et isolez leur placement CPU pour qu’ils ne se battent pas avec des tâches bruyantes.
Tâche 14 : Vérifier que le backend est un bloc brut, pas QCOW2-sur-système de fichiers (hôte)
cr0x@server:~$ virsh domblklist vm01
Target Source
------------------------------------------------
vda /var/lib/libvirt/images/vm01.qcow2
Signification : QCOW2 ajoute un overhead de métadonnées et un risque de fragmentation ; ça peut convenir pour beaucoup d’usages, mais ce n’est pas gratuit aux hauts débits d’écriture.
Décision : Si vous recherchez une latence faible et stable, déplacez les disques chauds vers des LVs bruts ou des périphériques bloc dédiés, et gardez QCOW2 pour les disques de boot et la commodité là où elle appartient.
Méthode de diagnostic rapide
Ceci est la séquence « vous avez 20 minutes avant que l’appel d’incident ne chauffe ». Le but est de localiser rapidement la couche goulot, pas de créer un benchmark parfait.
Première étape : décider s’il s’agit de saturation du périphérique ou de jitter d’ordonnancement
- Vérifiez iostat invité : si
%utilest élevé et queawaitaugmente avec la charge, suspectez une saturation du backend. - Vérifiez le CPU steal invité : si
%stealpique pendant les pics de latence, suspectez une contention CPU hôte. - Vérifiez les percentiles fio : si la moyenne semble correcte mais que p99/p99.9 est horrible, suspectez du queueing et de la contention, pas la vitesse brute du périphérique.
Deuxième étape : identifier le chemin I/O et retirer la couche la plus suspecte
- Si vous êtes sur VirtIO avec QCOW2 : c’est votre premier suspect. Passez le disque chaud en bloc brut/LV comme test.
- Si vous êtes sur VirtIO sans iothreads : ajoutez des iothreads, puis retestez p99.
- Si vous êtes en passthrough et que vous êtes toujours lent : arrêtez de blâmer VirtIO ; regardez l’affinité IRQ, les réglages du noyau invité, et les fonctionnalités de power/latency NVMe.
Troisième étape : confirmer la santé du backend hôte
dmesgde l’hôte pour erreurs NVMe, resets, timeouts.- Pression mémoire hôte (Dirty/Writeback), qui peut créer des orages de flush.
- Utilisation CPU des threads QEMU ; si le thread I/O est saturé ou affamé, vous avez trouvé le méchant.
Si vous ne faites qu’une chose : mesurez la latence p99 et le CPU steal pendant que vous reproduisez le problème. Cette paire vous dit si vous vous battez contre le stockage ou l’ordonnancement.
Erreurs courantes : symptômes → cause racine → correction
1) Symptom : « VirtIO est lent, donc nous avons besoin de passthrough »
Cause racine : Disque VirtIO soutenu par QCOW2 sur un système de fichiers avec cache hôte et pics de writeback ; plus absence d’iothreads.
Correction : Placez les disques chauds sur du bloc brut (LV, RBD, ou périphérique direct), utilisez cache=none quand approprié, activez les iothreads, et validez la configuration des files. Puis comparez à nouveau.
2) Symptom : Latence moyenne excellente, p99.9 terrifiant lors des pics
Cause racine : Contention CPU : thread I/O QEMU ou vCPU désordonnancé ; les IRQ s’accumulent sur un seul cœur ; recollement/writeback de l’hôte.
Correction : Épinglez iothreads et vCPU, corrigez l’affinité IRQ, réduisez l’overcommit, et assurez la marge mémoire de l’hôte. Vérifiez avec mpstat et /proc/interrupts.
3) Symptom : La VM en passthrough est rapide jusqu’à ce qu’elle ne le soit plus ; puis ça s’effondre
Cause racine : Reset du contrôleur NVMe, quirk firmware, ou erreur PCIe propagée directement à l’invité ; la récupération est visible et brutale dans l’invité.
Correction : Validez le firmware, vérifiez les logs AER PCIe, surveillez les compteurs d’erreurs NVMe, et concevez pour la défaillance (réplication, clustering). Le passthrough réduit les couches ; il réduit aussi l’amorti.
4) Symptom : Des pics de latence toutes les quelques minutes comme une horloge
Cause racine : Flush/journal commit périodique, jobs fstrim/discard, ou seuils de writeback de l’hôte provoquant des rafales.
Correction : Planifiez les trims, ajustez les paramètres de writeback, évitez la double mise en cache, et assurez-vous que les réglages de durabilité correspondent aux attentes de votre base de données (ne « optimisez » pas les flushs si vous tenez à vos données).
5) Symptom : « Nous avons augmenté la profondeur de file et c’est devenu pire »
Cause racine : Trop de queueing augmente la latence ; vous construisez juste une plus grande salle d’attente. Possibilité aussi de contention par verrou ou saturation du backend amplifiée par la concurrence.
Correction : Ajustez l’iodepth pour correspondre au périphérique et à la charge ; mesurez la latence d’extrémité. Pour les bases de données, baisser l’iodepth améliore souvent le p99 même si les IOPS de pointe chutent.
6) Symptom : La migration à chaud fonctionne, mais les performances sont incohérentes selon les hôtes
Cause racine : Différence de modèles CPU, topologie NUMA, politiques d’équilibrage IRQ, ou modèles/firmware NVMe différents dans le cluster.
Correction : Standardisez les profils d’hôte, épinglez interruptions/threads de façon cohérente, et traitez « même type d’instance » comme un contrat opérationnel, pas du marketing.
Trois mini-histoires d’entreprise des tranchées du stockage
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
Une entreprise de taille moyenne exploitait une flotte de VMs hébergeant des agents de build et des caches d’artefacts. Les builds étaient « aléatoirement lents », ce qui est le type de symptôme qui pousse tout le monde à accuser le réseau en premier et le stockage en second. Quelqu’un remarqua que les hôtes avaient de beaux NVMe et conclut que les VMs de build étaient « sur NVMe ». Cette phrase survécut assez longtemps pour devenir un système de croyance.
Pendant une semaine de release, les temps de build doublèrent. L’équipe on-call ne voyait pas de saturation évidente du disque. Les IOPS allaient bien. La latence moyenne allait bien. Mais la longue traîne était méchante. L’outil de build passait beaucoup de temps à attendre de petites opérations de métadonnées de fichiers et des fsyncs synchrones, le genre de charge qui punit le jitter.
La mauvaise hypothèse était simple : les VMs utilisaient des disques VirtIO soutenus par des images QCOW2 sur un ext4. Le cache de pages hôte faisait paraître le système rapide en faible charge, puis la pression mémoire déclenchait du writeback aux pires moments. Le « NVMe » était réel, mais enterré sous une pile de overhead et de contention.
La correction n’a pas été héroïque. Ils ont déplacé les disques chauds vers des LVs bruts, défini le mode de cache intentionnellement, et ajouté des iothreads. La latence d’extrémité s’est améliorée suffisamment pour faire disparaître le ralentissement des builds. La correction culturelle importait aussi : ils ont arrêté d’utiliser « sur NVMe » comme promesse de performance et ont commencé à spécifier le chemin réel.
Mini-histoire 2 : L’optimisation qui a mal tourné
Une équipe de services financiers avait une VM de base de données qui combattait constamment la latence p99. Ils ont choisi le passthrough NVMe parce que les benchmarks étaient magnifiques. Et pendant des semaines, ça l’était. Latence plus basse, overhead CPU plus faible, moins de mystères.
Puis un hôte a eu un hic PCIe. Rien de dramatique : une erreur transitoire, un retrain de lien, le genre d’événement qui arrive dans des datacenters réels quand vous entassez du hardware haute densité et faites comme si la physique était optionnelle. Le contrôleur NVMe s’est réinitialisé. Sur du bare metal, l’OS a récupéré après une pause. Dans la VM en passthrough, la pause ressemblait à la disparition du stockage en plein milieu d’une opération.
La base de données a réagi exactement comme prévu : panique face aux erreurs I/O, marquage des périphériques comme suspects, et déclenchement d’un basculement. Le basculement a fonctionné, mais il a été bruyant : orages de reconnexion, réchauffement du cache, et une cascade de tickets « pourquoi tout est lent ». L’optimisation avait déplacé la frontière de défaillance de « l’hôte l’absorbe » à « l’invité l’expérimente ».
Ils ont gardé le passthrough, mais seulement après avoir ajouté des résiliences ennuyeuses : réplication de base de données réglée pour un basculement rapide, alertes sur les compteurs de reset NVMe, et un runbook qui supposait que le périphérique pouvait disparaître. La leçon n’était pas « le passthrough est mauvais ». C’était « le passthrough est honnête ». Les systèmes honnêtes vous montrent les arêtes vives que vous ignoriez auparavant.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une entreprise SaaS exécutait des charges mixtes sur un cluster de virtualisation. Ils standardisèrent sur VirtIO pour la plupart des VMs car ils avaient besoin de migration et de flexibilité opérationnelle. Rien d’exotique. Mais ils firent quelque chose qui sonne ennuyeux et est donc rare : ils maintenaient un profil de performance stockage par hôte.
Chaque hôte avait un run fio de base lors de sa mise en service, capturant non seulement les IOPS mais les percentiles de latence à plusieurs valeurs d’iodepth. Ils le répétaient après mises à jour firmware, upgrades du noyau et échanges hardware. Les résultats étaient stockés avec les métadonnées de l’hôte, donc « cet hôte est bizarre » pouvait être prouvé en quelques minutes.
Un jour, après un cycle de maintenance, un sous-ensemble d’hôtes commença à montrer des pics p99 intermittents sur des VMs autrement normales. L’équipe compara les baselines et vit immédiatement un changement : le comportement de writeback hôte et la consommation CPU des threads QEMU semblaient différents. Pas « cassé », juste assez différent pour provoquer des problèmes de latence d’extrémité pour certains locataires.
Ils ont roll-backé une combinaison spécifique de réglages noyau hôte et ajusté leur politique d’épinglage d’iothread. L’incident n’est pas devenu une guerre de salle de crise d’une semaine car l’équipe avait une baseline ennuyeuse et traitait la performance comme une propriété monitorée, pas une anecdote. Le geste salvateur n’était pas un tuning brillant. C’était rendre le « normal » mesurable.
Listes de contrôle / plan étape par étape
Checklist décisionnelle : cette VM doit-elle obtenir un NVMe passthrough ?
- La charge est-elle critique en latence au p99/p99.9 ? Si non, n’en faites pas un objectif.
- Pouvez-vous dédier un périphérique (ou namespace) à cette VM ? Si non, le passthrough deviendra une bataille d’allocation de ressources.
- Pouvez-vous vous passer de la migration à chaud ? Si non, VirtIO gagne par défaut.
- Avez-vous une maturité opérationnelle IOMMU et VFIO ? Si non, vous apprendrez en incident. Ce n’est pas le meilleur moment pour apprendre.
- Avez-vous un design de gestion des pannes ? Le passthrough rend les resets et erreurs de périphérique visibles dans l’invité. Prévoyez-le.
Checklist VirtIO « bien faire » (valeurs par défaut pratiques)
- Supportez les disques chauds par du bloc brut quand c’est possible (LV, ZVOL, RBD) au lieu de QCOW2-sur-système de fichiers.
- Utilisez virtio-scsi quand vous avez besoin de multi-queue et de flexibilité ; validez le nombre de files dans l’invité.
- Ajoutez des iothreads et épinglez-les sur des CPU stables. Ne laissez pas le chemin I/O se battre avec la maintenance de l’émulateur.
- Choisissez le mode de cache intentionnellement :
- cache=none pour un I/O direct et réduire le jitter du cache hôte.
- cache=writeback seulement quand vous comprenez les compromis de durabilité et de pression mémoire.
- Validez la latence d’extrémité avec les percentiles fio, pas seulement la moyenne.
Checklist Passthrough « le faire en sécurité »
- Confirmez que le périphérique est lié à
vfio-pcisur l’hôte. - Vérifiez que l’IOMMU est activé et que vous avez des groupes IOMMU sensés (pas de partage de périphérique surprise).
- Épinglez les vCPUs et gérez l’affinité IRQ pour que les interruptions NVMe ne soient pas concentrées sur un seul cœur.
- Surveillez les logs d’erreurs NVMe et les resets ; traitez-les comme des signaux prédictifs.
- Concevez l’application/le cluster pour les fautes au niveau périphérique (réplication, tests de basculement).
FAQ
1) Le passthrough NVMe est-il toujours plus rapide que VirtIO ?
Non. Il est souvent plus rapide pour les petites E/S sensibles à la latence car il supprime l’overhead côté virtualisation. Mais VirtIO peut égaler ou dépasser le débit dans certaines configurations, et il gagne généralement sur le plan opérationnel.
2) Quel est le changement VirtIO le plus simple qui apporte de réelles améliorations de performance ?
Arrêtez de mettre des disques à fort écritures et à haut IOPS sur des images QCOW2 posées sur un système de fichiers généraliste. Déplacez les disques chauds vers du bloc brut et ajoutez des iothreads.
3) VirtIO-blk ou virtio-scsi ?
virtio-scsi est souvent le meilleur choix quand vous voulez un scaling multi-queue et un chemin mature pour des comportements de stockage complexes. virtio-blk peut être plus simple et rapide, mais vérifiez les limites de queueing et les options de configuration de votre hyperviseur.
4) Une iodepth « aussi haute que possible » rend-elle NVMe plus rapide ?
Elle rend les files plus profondes. Ce n’est pas la même chose. Une iodepth élevée peut augmenter le débit mais détruire la latence d’extrémité. Réglez l’iodepth sur la base du p99, pas de l’ego.
5) Pourquoi la latence p99 est-elle mauvaise même quand %util est faible ?
Parce que votre goulot est probablement l’ordonnancement ou la contention : CPU steal, affinité IRQ, un thread QEMU principal occupé, reclaim mémoire hôte, ou contention par verrou dans le chemin I/O. Une faible utilisation du périphérique ne signifie pas une faible latence de bout en bout.
6) Puis-je migrer à chaud une VM avec passthrough NVMe ?
Pas dans le sens normal de « déplacer la VM en cours d’exécution vers un autre hôte ». Il existe des approches spécialisées, mais si la migration à chaud est une exigence stricte, VirtIO est la réponse pratique.
7) Le passthrough est-il sûr en environnement multi-tenant ?
Ça peut l’être, si l’isolation IOMMU est correcte et que vous contrôlez opérationnellement l’attribution des périphériques. Mais cela réduit la capacité de l’hôte à appliquer des politiques, et augmente le rayon d’impact lorsqu’un périphérique se comporte mal.
8) Qu’en est-il d’utiliser NVMe-oF ou des périphériques bloc réseau à la place ?
Le stockage réseau peut être excellent, mais il introduit du jitter réseau et des modes de défaillance différents. Il change aussi la propriété du problème de queueing. Mesurez la latence p99 de bout en bout et décidez selon la sensibilité de votre charge.
9) Si je ne me soucie que du débit (MB/s), que dois-je choisir ?
VirtIO avec un backend bien configuré fournit souvent suffisamment de débit, parfois limité plus par le CPU que par le disque. Si vous faites des I/O séquentielles massives, la différence entre passthrough et VirtIO peut être plus petite que prévu.
10) Quel est encore le « vrai gagnant » ?
Le vrai gagnant est de supprimer la complexité accidentelle : couches d’images inutiles, mauvais modes de cache, absence d’iothreads, topologie CPU/IRQ mal alignée. Le passthrough est une façon de supprimer des couches. Le tuning correct de VirtIO en est une autre.
Étapes pratiques suivantes
- Choisissez un profil fio représentatif pour votre charge (mélange lectures/écritures, inclure fsync si pertinent) et enregistrez p50/p95/p99/p99.9 de latence.
- Cartographiez votre chemin I/O réel : type de périphérique invité, mode de cache hôte, type de backing store (raw vs QCOW2), et ordonnanceur du périphérique backend.
- Corrigez les victoires faciles en premier : bloc brut pour les disques chauds, iothreads, mode de cache correct, et placement CPU/IRQ. Mesurez à nouveau.
- Ne considérez le passthrough NVMe qu’ensuite pour la poignée de VMs qui en ont vraiment besoin—et seulement si vous pouvez accepter les compromis opérationnels.
- Institutionnalisez des baselines : stockez les profils de performance stockage des hôtes et VMs pour détecter les régressions avant que vos clients ne le fassent.
Si vous voulez une directive opinionnée unique : considérez la virtualisation du stockage comme un problème de budget de latence, pas comme un problème de choix de pilote. Le budget est dépensé sur les couches. Dépensez-le intentionnellement.