Vous ne choisissez pas « containers » ou « VMs » en l’abstrait. Vous les choisissez alors que quelqu’un vous réveille parce que la latence p99 a doublé,
que le job batch est 3× plus lent, ou qu’une mise à jour « mineure » d’un nœud a déclenché une semaine d’archéologie des reproches.
Le CPU est l’endroit où cette décision devient politique. Tout le monde pense comprendre le CPU. Puis on rencontre le throttling, le steal time, le NUMA,
et la vérité gênante : l’ordonnanceur ne se soucie pas de vos engagements de sprint.
Profils CPU qui comptent vraiment
« CPU-bound » n’est pas une description suffisante. Le choix entre containers et VMs dépend de la forme de la demande CPU et
des pénalités que vous êtes prêt à accepter : jitter, latence en queue, coût des changements de contexte, churn de cache, comportement de préemption,
et à quel point vous faites confiance aux charges voisines.
Profil 1 : Sensible à la latence tail (p99/p999 est le produit)
Pensez : trading, enchères publicitaires, OLTP, services requête/réponse avec SLO stricts, ou tout ce pour quoi un seul cœur lent ruine votre journée.
Ces charges détestent :
- Le throttling CPU (quotas cgroup) et la planification en rafales.
- La préemption imprévisible par d’autres charges (voisins bruyants).
- L’accès mémoire cross-NUMA et les défauts de cache.
- Les tempêtes d’interruptions qui tombent sur « vos » cœurs.
Elles veulent généralement un temps CPU garanti, une isolation de cœurs et un placement stable. Les containers peuvent fournir cela, mais seulement si vous
le configurez réellement. Les VMs peuvent aussi le faire, mais seulement si vous cessez de prétendre que l’overcommit est un repas gratuit.
Profil 2 : Batch orienté débit (terminer pour le matin, le jitter n’importe peu)
Pensez : ETL, encodage vidéo, indexation, génération de features ML. Ces charges se soucient des CPU-seconds totaux et de l’échelle.
Elles tolèrent le jitter. Elles ne tolèrent pas que vous les facturiez pour des cœurs inactifs.
Les containers gagnent souvent ici parce que vous pouvez les empaqueter serré, les scaler rapidement, et laisser l’ordonnanceur faire son boulot. Les VMs peuvent compétitionner
quand vous avez des frontières de locataire strictes ou besoin de noyaux différents, mais vous paierez en densité et en friction opérationnelle.
Profil 3 : Interactif en rafales (CPU en pics, humain dans la boucle)
Pensez : runners CI, environnements preview pour développeurs, tableaux de bord internes qui piquent lors des déploiements. Ils veulent un démarrage rapide et de l’élasticité.
Opérationnellement, les containers tendent à gagner. Côté CPU, ils gagnent tant que vous n’« optimisez » pas en posant des limites CPU agressives qui transforment
les pics en misère throttlée.
Profil 4 : Mode mixte « ça va jusqu’à ce que ça n’aille plus » (hôtes partagés, beaucoup de services)
C’est le défaut en entreprise : beaucoup de petits services, quelques moyens, et une charge mystère que personne ne possède.
La contention CPU ici est un problème de gouvernance déguisé en technique.
Les containers vous donnent un contrôle plus fin et une meilleure densité, mais aussi plus de façons de créer accidentellement de la contention. Les VMs vous donnent un
mur plus épais, ce qui est agréable jusqu’à ce que vous réalisiez que vous partagez quand même le CPU physique et que le mur n’est pas insonorisé.
Profil 5 : Comportement CPU spécial (temps réel, DPDK, quasi-HPC)
Si vous avez besoin d’ordonnancement temps réel, d’un routage d’interruptions prévisible, ou de réseaux en espace utilisateur qui veulent des cœurs dédiés, vous êtes dans
la catégorie « arrêtez de deviner ». Les deux, containers et VMs, peuvent fonctionner, mais vous finirez probablement par faire du CPU pinning, isolcpus, hugepages, et
un placement NUMA explicite. À ce stade, le problème n’est pas « containers vs VMs ». C’est de savoir si votre équipe plateforme peut supporter des réglages déterministes sans tout casser.
Un modèle mental CPU : ce que font réellement le noyau et l’hyperviseur
Les containers ne sont pas de petites VMs. Ce sont des processus avec des opinions : des namespaces pour mentir sur ce qu’ils voient, et des cgroups pour appliquer
ce qu’ils peuvent utiliser. L’ordonnanceur CPU reste celui du noyau hôte.
Les VMs exécutent un noyau invité sur des vCPU. Ces vCPU sont planifiés sur des CPU physiques par l’hyperviseur/noyau hôte.
Vous avez donc deux ordonnanceurs : l’ordonnanceur invité qui assigne des threads aux vCPU, puis l’hôte qui planifie les vCPU sur les pCPU.
Cette double couche est à la fois une fonctionnalité (isolation) et une source d’étrangetés (steal time, interférence de planification).
Temps CPU : les trois grandes métriques qui décident de votre sort
- Usage : combien de CPU votre charge consomme réellement.
- Wait : temps passé exécutable mais pas en cours d’exécution (queueing, contention).
- Jitter : variance de latence due à la planification, au throttling, aux interruptions et aux effets de cache.
Les containers tendent à minimiser l’overhead et maximiser la densité, mais augmentent la probabilité de « wait » et de « jitter » à moins que vous ne soyez discipliné sur la gouvernance des ressources.
Les VMs ajoutent une couche d’overhead et de complexité de planification, mais peuvent réduire le couplage inter-tenant quand elles sont configurées correctement. La phrase clé
est « quand elles sont configurées correctement. » La plupart ne le sont pas.
Quotas vs shares : comment les containers obtiennent « moins de CPU » sans que vous le remarquiez
Dans cgroup v2, le contrôle CPU est généralement un mélange de :
- weights (priorité relative en cas de contention), et
- max (plafond dur qui peut causer du throttling même quand des CPU libres existent ailleurs, selon la configuration et le comportement en burst).
Le mode d’échec est classique : vous mettez des limites CPU « pour l’équité », puis votre service sensible à la latence atteint la limite lors d’un pic,
se fait throttler, et le p99 chute pendant que la moyenne CPU semble « raisonnable ».
Steal time : comment les VMs vous disent « quelqu’un d’autre a pris mon déjeuner »
Dans les environnements virtualisés, le steal time est le temps CPU que l’invité souhaitait mais n’a pas obtenu parce que l’hôte a lancé autre chose.
Un steal élevé est l’équivalent VM de faire la queue au café pendant que votre réunion commence sans vous.
NUMA : la taxe que vous payez quand la mémoire est « là-bas »
Sur les systèmes multi-sockets, CPU et mémoire sont organisés en nœuds NUMA. Accéder à la mémoire locale est plus rapide que la mémoire distante.
Quand vous planifiez des threads sur un socket et que leur mémoire vit sur un autre, vous obtenez une performance qui ressemble à une perte de paquets : sporadique,
difficile à reproduire, et toujours d’abord imputée au « réseau ».
Comportement de cache et changements de contexte : la facture cachée
Si vous empilez beaucoup de containers sur un hôte, vous augmentez les changements de contexte et la pression sur le cache. Ce n’est pas automatiquement mauvais.
C’est mauvais quand vous attendez une latence tail à un chiffre de millisecondes et que vous traitez le CPU comme une marchandise infiniment divisible.
Idée paraphrasée de Werner Vogels (CTO d’Amazon) : Vous le construisez, vous l’exécutez ; la propriété améliore la fiabilité.
Cela s’applique aussi ici : l’équipe qui définit la politique CPU devrait sentir le pager.
Qui gagne quand : containers vs VMs selon le profil CPU
Services sensibles à la latence : généralement VMs pour une isolation forte, containers pour des plateformes disciplinées
Si vous exploitez une plateforme multi-tenant où les équipes ne coordonnent pas, les VMs sont le choix par défaut plus sûr pour les services sensibles à la latence.
Pas parce que les VMs sont magiques, mais parce que les frontières sont plus difficiles à franchir accidentellement. Un container sans limites CPU et sans isolation peut ruiner un nœud entier ; une VM peut aussi,
mais il faut un autre type de mauvaise configuration.
Si vous exploitez une plateforme Kubernetes disciplinée avec :
- Guaranteed QoS pods (requests == limits for CPU),
- static CPU manager policy pour des cœurs épinglés quand nécessaire,
- ordonnancement aware NUMA,
- réglage d’affinité IRQ sur les chemins critiques,
…alors les containers peuvent fournir d’excellentes latences tail avec une densité supérieure aux VMs. Mais ce n’est pas le « Kubernetes par défaut ». C’est
« Kubernetes après que vous ayez lu les notes de bas de page et payé la taxe ».
Calcul stateless à haut débit : les containers gagnent en densité et vélocité opérationnelle
Pour du compute stateless, scalable horizontalement, où le jitter est acceptable, les containers sont l’outil préférable. L’overhead est plus faible,
l’ordonnancement est plus simple, et vous obtenez un meilleur bin packing. De plus : les mises à jour progressives, l’autoscaling et les rollbacks rapides
sont plus faciles à exécuter sans traiter la flotte d’hyperviseurs comme un artefact sacré.
Applications legacy et noyaux différents : les VMs gagnent, et ce n’est même pas contestable
Si vous avez besoin d’un noyau différent, de modules noyau, ou que vous exécutez un logiciel qui suppose qu’il possède la machine (bonjour, anciens systèmes de licence),
utilisez une VM. Les containers partagent le noyau hôte ; si le noyau est la couche de compatibilité que vous devez contrôler, les containers sont la mauvaise abstraction.
Risque de voisin bruyant : les VMs réduisent le périmètre ; les containers nécessitent politique et enforcement
Les deux, containers et VMs, partagent le même CPU physique. La différence est combien de couches vous devez mal configurer avant qu’une charge nuise à une autre.
- Dans les containers, l’ordonnanceur du noyau est partagé directement. Les erreurs de cgroup apparaissent immédiatement.
- Dans les VMs, des tailles de vCPU mal calibrées, l’overcommit et la contention hôte apparaissent comme du steal time et du jitter.
Quand l’approche « quasi bare metal » compte : les containers ont moins d’overhead, mais les VMs peuvent être ajustées
Les containers ont généralement moins d’overhead CPU parce qu’il n’y a pas de second noyau ni de périphériques émulés (à supposer que vous ne fassiez rien de créatif côté réseau).
Les VMs peuvent être très proches du natif avec la virtualisation matérielle et des pilotes paravirtualisés, mais vous paierez toujours un coût en exits, interruptions, et indirection de planification.
Conseil pratique : si vous cherchez les derniers 5–10% sur des charges CPU-intensives, mesurez. Ne discutez pas.
Faits et historique qui expliquent les bizarreries actuelles
- Années 1970 : la virtualisation n’est pas nouvelle. Les mainframes IBM exécutaient des machines virtuelles des décennies avant que le cloud ne les rende tendance ; l’idée était l’isolation et l’utilisation.
- 2006 : AWS popularise « louer une VM ». EC2 a fait de l’exploitation centrée sur les VMs le modèle mental par défaut pour une génération d’ingénieurs.
- 2007–2008 : les cgroups Linux apparaissent. Les control groups ont donné au noyau un moyen de comptabiliser et limiter CPU/mémoire par groupe de processus — les containers ont suivi cette vague.
- 2008 : LXC a rendu les « containers » tangibles. Avant Docker, LXC faisait déjà namespaces+cgroups ; il n’avait simplement pas la même UX ni l’histoire de distribution.
- 2013 : Docker a rendu le packaging contagieux. La fonctionnalité killer n’était pas l’isolation ; c’était le fait d’expédier une appli avec ses dépendances de façon répétable.
- 2014 : Kubernetes a transformé l’ordonnancement en produit. Il a normalisé l’idée que la plateforme alloue le CPU, pas l’équipe applicative qui mendie des VMs.
- 2015+ : l’ère Spectre/Meltdown a complexifié la conversation sur l’overhead. Certaines mitigations ont impacté les chemins intensifs en syscall et la virtualisation ; les discussions de performance sont devenues plus nuancées.
- cgroup v2 a unifié les sémantiques de contrôle. Il a réduit certaines bizarreries legacy mais introduit de nouveaux réglages que les gens lisent mal, surtout autour de CPU.max et du comportement en burst.
- Les CPU modernes ne sont pas « juste des cœurs ». Turbo, scaling de fréquence, SMT/Hyper-Threading et caches partagés font de l’isolation CPU un exercice probabiliste à moins de pinner et isoler.
Playbook de diagnostic rapide
Quand la performance CPU part en vrille, votre premier boulot est d’identifier si vous avez affaire à pas assez de CPU,
pas de planification, ou trop de travail par requête. Tout le reste est décoration.
Première étape : confirmer le symptôme et l’étendue
- La latence est-elle en hausse, le débit en baisse, ou les deux ?
- Est-ce un pod/VM, un nœud, une zone de disponibilité, ou partout ?
- Est-ce que ça a commencé après un déploiement, un événement d’auto-scaling, un recyclage de nœud, une mise à jour du noyau, ou un changement de type d’hôte ?
Deuxième étape : décider s’il s’agit de throttling/steal ou d’une saturation réelle
- Containers : vérifiez les compteurs de throttling cgroup et la configuration CPU.max/CPU quota.
- VMs : vérifiez le steal time et la contention au niveau hôte.
- Les deux : vérifiez la longueur de la run queue et le taux de context switches.
Troisième étape : contrôler les pathologies de placement (NUMA, pinning, interruptions)
- Déséquilibre NUMA ou accès mémoire cross-node.
- Charges CPU épinglées partageant des hyperthreads frères.
- IRQs qui atterrissent sur les mêmes cœurs que vos threads sensibles à la latence.
Quatrième étape : valider la fréquence et la gestion d’énergie
- Fréquence CPU anormalement basse à cause du governor d’énergie ou du throttling thermique.
- Comportement Turbo modifié après des mises à jour BIOS/firmware.
Cinquième étape : seulement ensuite regardez « l’inefficacité applicative »
Si vous commencez par des flamegraphs alors que le noyau throttle littéralement votre process, vous faites de l’archéologie les lumières éteintes.
Tâches pratiques : commandes, sorties et décisions (12+)
Voici les vérifications que vous pouvez exécuter aujourd’hui. Chacune inclut : la commande, ce que signifie une sortie plausible, et quelle décision prendre ensuite.
Exécutez-les sur le nœud/hôte, puis à l’intérieur du container ou de la VM selon le cas. Ne vous fiez pas uniquement aux tableaux de bord ; les dashboards sont l’endroit où la nuance meurt.
Task 1: See if the host is CPU saturated (run queue and load vs CPU count)
cr0x@server:~$ nproc
32
cr0x@server:~$ uptime
15:42:10 up 12 days, 3:17, 2 users, load average: 48.12, 44.90, 39.77
Signification : Une load average ~48 sur un hôte 32 cœurs suggère une forte mise en file d’exécution (ou beaucoup de sleep non interruptible ; vérifiez les tâches suivantes).
Si le p99 est mauvais, c’est un signal d’alerte.
Décision : Si la charge > cœurs pendant des périodes soutenues, arrêtez de discuter d’optimisations micro. Réduisez la contention : scaler horizontalement, réduire la colocation, ou augmenter l’allocation CPU.
Task 2: Check CPU breakdown and steal time (VM clue)
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (node-7) 01/12/2026 _x86_64_ (32 CPU)
15:42:21 CPU %usr %nice %sys %iowait %irq %soft %steal %idle
15:42:22 all 62.10 0.00 10.43 0.12 0.00 0.55 18.34 8.46
15:42:23 all 63.02 0.00 10.21 0.09 0.00 0.61 17.80 8.27
15:42:24 all 61.77 0.00 10.66 0.14 0.00 0.59 18.02 8.82
Signification : 18% de steal est énorme. Dans une VM, cela signifie que l’hyperviseur ne planifie pas vos vCPUs.
Décision : Ne tunez pas l’application encore. Déplacez la VM, réduisez l’overcommit de l’hôte, ou négociez une capacité réservée. Si vous ne pouvez pas, acceptez une pire latence tail — c’est de la physique facturée.
Task 3: Check per-process CPU and context switching pressure
cr0x@server:~$ pidstat -w -u 1 3
Linux 6.5.0 (node-7) 01/12/2026 _x86_64_ (32 CPU)
15:43:10 UID PID %usr %system %CPU CPU Command
15:43:11 1001 24811 310.0 42.0 352.0 7 java
15:43:11 1001 24811 12000.0 800.0 - cswch/s nvcswch/s
15:43:11 1001 24811 12000.0 650.0 - java
Signification : Des taux extrêmement élevés de context switches indiquent généralement une forte contention de threads, du churn de locks, ou de l’oversubscription.
Décision : Si c’est un container sur un nœud partagé, envisagez le pinning et la réduction de la cotenancy. Si c’est une VM, vérifiez le nombre de vCPU vs threads et contrôlez la planification hôte.
Task 4: For containers, verify cgroup v2 CPU limits (CPU.max)
cr0x@server:~$ cat /sys/fs/cgroup/cpu.max
200000 100000
Signification : Cela signifie un quota de 200ms de temps CPU par période de 100ms (effectivement l’équivalent de 2 cœurs). Si la charge dépasse cela en burst, elle sera throttlée.
Décision : Pour les services sensibles à la latence, évitez les plafonds CPU durs sauf si vous faites exprès d’appliquer un comportement prévisible. Préférez requests/Guaranteed QoS et des cœurs dédiés.
Task 5: For containers, check throttling counters
cr0x@server:~$ cat /sys/fs/cgroup/cpu.stat
usage_usec 12888904512
user_usec 12110000000
system_usec 778904512
nr_periods 934112
nr_throttled 212334
throttled_usec 9901123456
Signification : Si nr_throttled est élevé et throttled_usec non négligeable, votre container est mis en pause par le noyau en raison des quotas.
Décision : Si vous vous souciez du p99, soit augmentez/supprimez la limite, soit repensez le pattern de burst (par ex. contrôle de la concurrence). Le throttling est une machine à latence.
Task 6: In Kubernetes, confirm QoS class (Guaranteed vs Burstable)
cr0x@server:~$ kubectl -n prod get pod api-7d9c6b8c9f-4kq2m -o jsonpath='{.status.qosClass}{"\n"}'
Burstable
Signification : Les pods Burstable peuvent être dépriorisés en cas de contention et sont plus susceptibles de subir de la variabilité du temps CPU.
Décision : Pour les services critiques en latence, visez Guaranteed (set requests == limits for CPU) ou utilisez des nœuds dédiés sans limites CPU mais avec un strict admission control.
Task 7: Check Kubernetes CPU requests/limits and spot “limit set, request tiny”
cr0x@server:~$ kubectl -n prod get pod api-7d9c6b8c9f-4kq2m -o jsonpath='{range .spec.containers[*]}{.name}{" req="}{.resources.requests.cpu}{" lim="}{.resources.limits.cpu}{"\n"}{end}'
api req=100m lim=2000m
Signification : Le scheduler pense que vous avez besoin de 0.1 cœur, mais vous pouvez burster jusqu’à 2 cœurs. En cas de contention, vous perdrez en priorité de planification et subirez du jitter.
Décision : Pour une latence stable, fixez des requests réalistes. Pour l’efficacité coût, ne mentez pas au scheduler puis ne vous plaignez pas des résultats.
Task 8: Check CPU manager policy on a Kubernetes node (pinning capability)
cr0x@server:~$ ps -ef | grep -E 'kubelet.*cpu-manager-policy' | head -n 1
root 1123 1 1 Jan10 ? 00:12:44 /usr/bin/kubelet --cpu-manager-policy=static --kube-reserved=cpu=500m --system-reserved=cpu=500m
Signification : La politique static permet une allocation exclusive de CPU pour les pods Guaranteed (avec des requests CPU entiers).
Décision : Si vous avez besoin d’une latence tail cohérente, envisagez des nœuds avec cpu-manager statique plus topology manager. Sans cela, vous jouez à la roulette.
Task 9: Identify NUMA topology and whether your workload spans nodes
cr0x@server:~$ lscpu | egrep 'NUMA node|Socket|CPU\(s\)|Thread|Core'
CPU(s): 32
Thread(s) per core: 2
Core(s) per socket: 8
Socket(s): 2
NUMA node(s): 2
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
Signification : Deux nœuds NUMA. Si votre process saute entre les CPUs 0–31 et alloue la mémoire librement, vous pouvez payer des pénalités de mémoire distante.
Décision : Pour les services sensibles à la latence CPU, gardez CPU et mémoire locaux : pinnez les CPU, utilisez un ordonnancement aware NUMA, ou exécutez des instances plus petites qui tiennent sur un nœud.
Task 10: Check which CPUs a process is allowed to run on (cpuset / affinity)
cr0x@server:~$ taskset -pc 24811
pid 24811's current affinity list: 0-31
Signification : Le process peut s’exécuter n’importe où. Ça maximise la flexibilité mais peut augmenter le churn de cache et les effets NUMA.
Décision : Si vous observez du jitter et des problèmes cross-NUMA, envisagez de réduire l’affinité (ou utilisez les fonctionnalités de l’orchestrateur pour le faire en sécurité). Si le débit est l’objectif, laissez-le flexible.
Task 11: Check interrupt distribution (IRQ affinity hot spots)
cr0x@server:~$ cat /proc/interrupts | head -n 8
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
24: 98122321 0 0 0 1022334 0 0 0 PCI-MSI 524288-edge eth0-TxRx-0
25: 0 93455321 0 0 0 993442 0 0 PCI-MSI 524289-edge eth0-TxRx-1
26: 0 0 90211233 0 0 0 889120 0 PCI-MSI 524290-edge eth0-TxRx-2
27: 0 0 0 88777210 0 0 0 901223 PCI-MSI 524291-edge eth0-TxRx-3
NMI: 1223311 1219988 1213444 1209987 1198877 1190021 1189911 1180221 Non-maskable interrupts
Signification : Les interruptions réseau sont concentrées sur certains CPUs. Si votre workload sensible à la latence partage ces CPUs, vous verrez du jitter.
Décision : Ajustez l’affinité IRQ ou isolez des CPUs pour la charge. Ne « résolvez » pas la latence tail en ajoutant des retries ; c’est comme ça que naissent les incidents caractériels.
Task 12: Check frequency governor and current MHz (silent performance killer)
cr0x@server:~$ cpupower frequency-info | egrep 'governor|current CPU frequency' | head -n 6
The governor "powersave" may decide which speed to use
current CPU frequency: 1200 MHz (asserted by call to hardware)
Signification : Le governor powersave à 1.2GHz sur un serveur qui devrait chauffer… ce n’est pas idéal.
Décision : Passez en governor performance pour les nœuds sensibles à la latence, ou au moins validez les réglages firmware/power. Mesurez les économies d’énergie avant de vendre l’idée.
Task 13: In a VM, correlate steal with host contention (guest view)
cr0x@server:~$ sar -u 1 3
Linux 6.5.0 (vm-12) 01/12/2026 _x86_64_ (8 CPU)
15:45:01 CPU %user %nice %system %iowait %steal %idle
15:45:02 all 52.11 0.00 9.88 0.10 22.34 15.57
15:45:03 all 50.90 0.00 10.12 0.08 21.77 17.13
15:45:04 all 51.33 0.00 9.94 0.09 22.01 16.63
Signification : Steal >20% signifie que votre VM n’est pas régulièrement planifiée. L’invité voit aussi de « idle », mais ce n’est pas un idle réel.
Décision : N’ajoutez pas de vCPUs par réflexe ; cela aggrave souvent la planification. Réduisez l’overcommit, déplacez la VM, ou utilisez des instances réservées/pinning CPU sur l’hyperviseur.
Task 14: Spot hyperthread sibling contention (SMT/HT awareness)
cr0x@server:~$ for c in 0 1 2 3; do echo -n "cpu$c siblings: "; cat /sys/devices/system/cpu/cpu$c/topology/thread_siblings_list; done
cpu0 siblings: 0,16
cpu1 siblings: 1,17
cpu2 siblings: 2,18
cpu3 siblings: 3,19
Signification : CPU0 partage un cœur physique avec CPU16, etc. Si vous épinglez deux charges bruyantes sur des threads frères, attendez-vous à des surprises de performance.
Décision : Pour les travaux sensibles aux SLOs, préférez un thread par cœur (évitez le partage sibling) ou désactivez le SMT sur des nœuds dédiés si vous pouvez vous permettre la perte de capacité.
Blague #1 : Les limites CPU sont comme les budgets de bureau — tout le monde se sent « discipliné » jusqu’au premier incident, puis soudain les limites deviennent « juste une suggestion ».
Trois mini-histoires d’entreprise depuis les tranchées CPU
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse (containers « sans overhead »)
Une entreprise SaaS de taille moyenne a déplacé une API sensible à la latence de VMs vers Kubernetes. Le pitch était simple : « les containers sont plus légers, donc on aura
de meilleures performances et une plus grande densité. » La migration a été rapide, ce qui aurait dû être le premier indice.
L’API a été déployée avec des limites CPU pour « prévenir les voisins bruyants ». Les requêtes étaient petites — 100m — parce que le service n’utilisait pas beaucoup de CPU
en moyenne. Les limites étaient de 2 cœurs, ce qui semblait généreux. En trafic normal, tout avait l’air correct. Puis une campagne marketing a fait exploser le trafic.
Le service a scalé, mais chaque pod a atteint son quota CPU à répétition pendant les rafales de requêtes.
Les graphiques montraient une utilisation CPU sous la capacité du nœud, si bien que la réponse initiale a été d’optimiser le code applicatif et d’accuser la base de données. Pendant ce temps,
le noyau throttlait silencieusement les pods les plus chauds. Le p99 a doublé, puis triplé. Les timeouts ont suivi. L’autoscaler a paniqué et a ajouté plus de pods, ce qui a augmenté la contention
et créé une boucle de feedback misérable.
La correction réelle a été ennuyeuse : supprimer les limites CPU pour ce service sur des nœuds dédiés, définir des requests CPU réalistes, et utiliser le cpu-manager statique pour des cœurs épinglés.
Ils ont aussi ajouté des garde-fous dans l’admission control pour que « 100m requests » ne se retrouvent pas en production pour des services critiques.
L’hypothèse erronée n’était pas « les containers sont plus rapides ». Les containers peuvent être rapides. L’hypothèse fausse était que le CPU moyen est la métrique pertinente pour un service en rafales
avec une latence tail stricte.
Mini-histoire 2 : L’optimisation qui s’est retournée contre eux (overprovisionnement vCPU)
Une équipe d’entreprise exploitait une plateforme basée sur VM pour des services internes. On leur a mis la pression pour réduire le nombre de VMs. Quelqu’un a proposé
de « right-sizing » en augmentant le nombre de vCPUs par VM pour que chaque VM héberge plus de threads worker, réduisant ainsi le nombre d’instances.
Sur le papier, cela semblait efficace : moins de VMs, moins d’overhead de gestion, meilleure utilisation. En pratique, le cluster d’hyperviseurs était déjà
modérément overcommit. Augmenter les vCPUs rendait chaque VM plus difficile à planifier sur des hôtes occupés. Le steal time est monté, mais seulement aux heures de pointe.
Naturellement, l’incident a commencé à 10h00 quand tout le monde regardait.
Le pire : l’équipe a ajouté des vCPUs à nouveau, pensant que l’application manquait de CPU. Cela a augmenté l’ensemble des tâches prêtes et rendu la planification encore plus chaotique.
La latence tail a dégénéré sur plusieurs services, pas seulement celui « optimisé ».
Ils ont récupéré en annulant les changements de vCPU, en répartissant la charge sur plus de VMs, et en appliquant une politique d’overcommit plus stricte pour ce cluster.
La leçon était peu glamour : de plus grandes VMs ne sont pas toujours de meilleures VMs. La planification est une contrainte réelle, pas un détail d’implémentation.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise (isolement CPU + discipline des changements)
Un système lié aux paiements avait un petit ensemble de services avec des SLOs de latence serrés et une cryptographie lourde. L’équipe plateforme les gardait sur un pool
dédié de nœuds, avec cpu-manager statique, cœurs épinglés, et réglage explicite des IRQ. Ce n’était pas populaire. Les pools dédiés ressemblent à une « capacité gaspillée »
pour la finance et à un « amusement gâché » pour les ingénieurs qui veulent une plateforme unique.
Ils avaient aussi une politique de changement : les mises à jour du noyau et du BIOS étaient staged, avec un nœud canari exécutant des tests de charge synthétiques qui suivaient
le p99 et le jitter, pas seulement le débit. Chaque fois que quelqu’un proposait de sauter l’étape canari, la réponse était poliment constante : non.
Un jour, une mise à jour firmware de la flotte est arrivée dans le pool général. Elle a modifié le comportement d’alimentation CPU. Plusieurs services ont vu des décalages de latence,
et quelques équipes sont passées en mode incident. Le pool paiements n’a pas bronché. Leurs nœuds étaient épinglés à un profil de governor validé et n’étaient mis à jour qu’après que les résultats canaris
soient stables.
La pratique n’était pas astucieuse. Elle était juste cohérente. En production, « ennuyeux » est une fonctionnalité.
Blague #2 : La seule chose plus virtuelle qu’une VM est la certitude dans un postmortem rédigé avant que les métriques n’arrivent.
Erreurs courantes : symptôme → cause racine → correction
1) Symptom: p99 latency spikes every few minutes; average CPU looks fine
Cause racine : Throttling des CPU de container (cgroup CPU.max / CFS quota) pendant les rafales.
Correction : Supprimez ou augmentez les limites CPU pour le service ; définissez des requests CPU réalistes ; utilisez Guaranteed QoS et envisagez le CPU pinning pour des SLO stricts.
2) Symptom: VM CPU usage is high but throughput is low; graphs show “idle” too
Cause racine : Steal time élevé dû à l’overcommit hôte ou à la contention CPU.
Correction : Réduisez l’overcommit ; déplacez la VM vers un hôte moins chargé ; utilisez des réservations ou des politiques d’hôte dédié ; n’ajoutez pas de vCPUs en premier réflexe.
3) Symptom: performance got worse after scaling up to a bigger instance type
Cause racine : Effets NUMA ou planification cross-socket ; la mémoire est distante pour une partie de la charge.
Correction : Assurez un placement aware NUMA ; pinnez CPU et mémoire ; préférez des instances qui tiennent sur un nœud NUMA pour les services sensibles à la latence.
4) Symptom: “random” latency spikes during network-heavy traffic
Cause racine : Interruptions (IRQs) tombant sur des cœurs applicatifs ; surcharge de softirq.
Correction : Ajustez l’affinité IRQ ; séparez les interruptions réseau des CPUs applicatifs ; validez avec /proc/interrupts et les stats softirq.
5) Symptom: containerized Java/Go service becomes unstable under load, lots of context switches
Cause racine : Oversubscription de threads par rapport à l’allocation CPU ; thrash de l’ordonnanceur amplifié par des limites CPU serrées.
Correction : Réduisez le nombre de threads ; augmentez les requests CPU ; évitez les faibles requests avec de hauts limits ; envisagez le CPU pinning pour un comportement stable.
6) Symptom: everything slows down after “power saving” rollout
Cause racine : Governor de fréquence CPU réglé sur powersave ; capping thermique/énergie.
Correction : Utilisez le governor performance pour les nœuds critiques ; validez les réglages BIOS ; surveillez la fréquence et le throttling sous charge soutenue.
7) Symptom: Kubernetes node looks underutilized but pods are slow
Cause racine : Limites CPU throttlant par pod ; le nœud peut être idle pendant que des pods individuels sont caps.
Correction : Reconsidérez les limites ; utilisez les requests pour le placement, pas les limits comme instrument brutal ; séparez les charges bruyantes sur des nœuds différents.
8) Symptom: VM-based service regresses after enabling “more security”
Cause racine : Changements de microcode/mitigations noyau affectant les chemins intensifs en syscall/virtualisation (dépendant de la charge).
Correction : Benchmarkez avant/après ; isolez le changement ; si nécessaire, ajustez la taille d’instance ou déplacez la charge vers un profil qui tolère l’overhead.
Checklists / step-by-step plan
Step-by-step: choosing containers vs VMs for a CPU profile
- Écrivez la métrique de succès CPU. p99 latency ? requêtes/sec ? temps de complétion de job ? Si vous ne pouvez pas la nommer, vous allez optimiser les vibes.
- Classifiez la charge. Sensible à la latence, batch débit, interactif en rafales, mode mixte, comportement CPU spécial.
- Décidez des besoins d’isolation. Multi-tenant avec faible gouvernance ? Préférez VMs ou nœuds dédiés. Plateforme disciplinée ? Les containers peuvent bien fonctionner.
-
Décidez comment prévenir les voisins bruyants.
- Containers : requests, QoS, CPU pinning pour les pods critiques, pools de nœuds, admission control.
- VMs : limiter l’overcommit, réservations, pinning CPU quand nécessaire, monitoring hôte de la contention.
- Définissez des défauts qui correspondent à la réalité. Pas de « 100m request » pour des services qui ont des pics. Pas de « 8 vCPUs » pour un service qui passe la moitié du temps à attendre.
- Validez sur le hardware représentatif. Topologie NUMA, SMT, comportement de fréquence. « Même nombre de vCPU » ne signifie pas « même performance ».
- Testez la charge pour le jitter, pas seulement les moyennes. Suivez p95/p99/p999 et la variance sous contention et lors de la colocation.
- Opérationnalisez le diagnostic. Mettez les métriques de throttling/steal/run-queue dans des alertes. Si ce n’est pas mesuré, cela deviendra un débat.
Checklist: CPU governance for container platforms
- Définir les plages autorisées pour requests/limits CPU par tier de service.
- Appliquer via des politiques d’admission : pas de demandes minuscules pour les workloads critiques.
- Utiliser Guaranteed QoS pour les services à SLO stricts ; envisager le cpu-manager statique.
- Séparer les pools de nœuds : « throughput pack » vs « latency clean room ».
- Surveiller les compteurs de throttling cgroup et alerter en cas de throttling soutenu.
- Documenter quand les limites CPU sont requises (généralement pour l’équité sur des pools batch partagés).
Checklist: CPU governance for VM platforms
- Définir et publier une politique d’overcommit (et s’y tenir).
- Surveiller le steal time et la run queue hôte ; alerter quand la contention persiste.
- Éviter le scaling réflexe des vCPU ; valider l’impact sur la planification.
- Pour les workloads critiques : envisager le CPU pinning et la capacité réservée.
- Suivre le placement NUMA et l’alignement de la topologie hôte pour les VMs plus grandes.
FAQ
1) Les containers sont-ils toujours plus rapides que les VMs pour le CPU ?
Souvent, les containers ont un overhead plus faible parce qu’il n’y a pas de noyau invité et moins d’exits de virtualisation. Mais « plus rapide » s’effondre si vous
throttlez les containers avec des limites CPU ou les empaquetez dans la contention. Les VMs peuvent être très proches du natif quand elles sont réglées et non overcommit.
2) Quel est l’équivalent CPU de « noisy neighbor » dans Kubernetes ?
Un pod avec une forte demande CPU plus soit pas de limites (accaparant en cas de contention) soit des limites mal configurées (causant des cascades de throttling chez les autres via la pression de planification).
La correction est des pools de nœuds par tier, des requests réalistes, et enforcement.
3) Pourquoi les limites CPU nuisent-elles à la latence même quand les nœuds sont idle ?
Parce que les limites peuvent agir comme des plafonds durs par cgroup. Votre pod peut être throttlé même s’il y a du CPU disponible ailleurs sur le nœud, selon l’alignement de la demande avec la période de quota.
Le symptôme est la montée des compteurs de throttling alors que le CPU hôte n’est pas saturé.
4) Dois-je définir des limites CPU sur chaque container ?
Non. Posez des limites quand vous contraignez délibérément le comportement de burst pour l’équité (pools batch) ou pour empêcher des processus runaway.
Pour les services sensibles à la latence, les limites sont souvent une blessure auto-infligée sauf si vous avez validé qu’elles n’induisent pas de throttling.
5) Dans les VMs, un plus grand nombre de vCPUs est-il toujours mieux ?
Pas sous contention. Plus de vCPUs peut rendre la VM plus difficile à planifier et augmenter le steal time. Adaptez les vCPUs au parallélisme que vous pouvez réellement utiliser, et validez sous conditions de pic.
6) Comment savoir si le NUMA me pénalise ?
Vous verrez des performances inconsistantes, une latence tail pire sur des instances plus grandes, et parfois un trafic cross-socket élevé. Confirmez avec la topologie NUMA, l’affinité des processus CPU, et (si disponible) les statistiques de localité mémoire NUMA.
La correction est un placement aware NUMA et le pinning.
7) Le SMT/Hyper-Threading aide ou nuit ?
Il aide le débit pour beaucoup de charges, spécialement celles avec des stalls. Il peut nuire à la prévisibilité pour les travaux sensibles à la latence quand les threads siblings se disputent les ressources d’exécution.
Pour des SLO stricts, évitez de partager les siblings ou désactivez le SMT sur des nœuds dédiés si la capacité le permet.
8) Si je suis sur Kubernetes, dois-je utiliser des nœuds dédiés pour les services critiques ?
Oui, quand le p99 du service compte et que le reste du cluster est un mélange de workloads et d’équipes. Les nœuds dédiés simplifient l’histoire de performance et réduisent l’entropie des incidents.
Ce n’est pas du « gaspillage », c’est payer pour moins de réveils à 3h du matin.
9) Quelles métriques doivent me réveiller pour des problèmes CPU ?
Containers : temps throttlé (cpu.stat), run queue, context switches, et saturation CPU nœud. VMs : steal time plus run queue invité et contention hôte. Pour les deux : p99 corrélé avec des indicateurs de planification.
10) Puis-je obtenir une isolation proche des VMs avec des containers ?
Vous pouvez vous en approcher pour le CPU en utilisant des pools de nœuds dédiés, Guaranteed QoS, cpu-manager statique pour le pinning, et un enforcement strict des politiques.
Vous partagez toujours un noyau, donc l’histoire d’isolation n’est pas identique. Si cela importe dépend de votre modèle de menace et de votre maturité opérationnelle.
Conclusion : prochaines étapes actionnables
Choisissez en fonction du profil CPU, pas de la mode. Si vous avez besoin d’une latence tail déterministe et que vous n’avez pas une gouvernance plateforme forte, commencez
avec des VMs ou des nœuds containers dédiés. Si vous voulez du débit et de la densité, les containers sont généralement le bon pari — n’y sabotons juste pas avec des limites CPU naïves.
Prochaines étapes :
- Choisissez un service critique et exécutez les contrôles de diagnostic rapide pendant la charge de pointe : throttling (containers) ou steal (VMs), run queue, hot spots IRQ, fréquence.
- Corrigez un problème de politique, pas dix : soit supprimez les limites CPU nuisibles pour les services latence, soit réduisez l’overcommit VM là où le steal est élevé.
- Créez deux classes de nœuds/hôtes : « latency clean room » (pinned/isolated) et « throughput pack » (haute densité, partage équitable).
- Faites des requests/limits (ou le dimensionnement vCPU) une partie de la revue de code avec une courte grille d’évaluation. Le pager vous remerciera plus tard.