CPU Docker à 100 % : localiser le conteneur bruyant et le limiter correctement

Cet article vous a aidé ?

La machine hôte est saturée. La charge moyenne grimpe comme si elle avait un rendez-vous important. SSH devient lent. Vos tableaux de bord affichent “CPU 100%”, mais ils ne disent pas à qui la faute. Docker est impliqué, ce qui signifie que le problème est soit bien contenu… soit joliment réparti.

Ceci est le playbook que j’utilise quand une machine Linux brûle des cycles et que des conteneurs sont les principaux suspects. C’est pratique, un peu subjectif, et cela vous aidera à identifier le conteneur bruyant, prouver qu’il est le vrai goulot d’étranglement, et le limiter d’une manière qui n’engendre pas de latence, throttling ou comportement étrange du planificateur.

Playbook de diagnostic rapide

Quand le CPU est bloqué à fond, vous n’avez pas besoin d’un stage de méditation. Vous avez besoin d’une boucle de triage rapide qui distingue :

  • Un conteneur qui brûle le CPU vs. plusieurs conteneurs chacun “un peu” chauds
  • Saturation CPU vs. throttling CPU vs. contention de la file d’exécution
  • Travail réel vs. boucles d’attente actives vs. coût noyau

Première étape : confirmer que c’est bien le CPU et non un I/O qui ressemble au CPU

  • Exécutez uptime et top pour voir la charge moyenne vs le pourcentage d’inactivité CPU.
  • Si la charge est élevée mais que l’inactivité CPU l’est aussi, vous êtes probablement bloqué sur de l’I/O ou des verrous.

Deuxième étape : identifier le(s) conteneur(s) responsables

  • Utilisez docker stats --no-stream pour un classement rapide.
  • Puis mappez les PID hôtes vers les conteneurs (parce que “docker stats” peut mentir par omission quand ça devient bizarre).

Troisième étape : décider s’il faut limiter, scaler ou corriger le code

  • Limiter quand une charge malmène ses voisins et que vous pouvez tolérer plus de latence pour cette charge.
  • Scalper (scaler) quand la charge est légitime et que le débit compte.
  • Corriger quand vous observez des boucles actives, des retries, des verrous chauds ou un GC pathologique.

Quatrième étape : appliquer une limite qui correspond à la réalité de votre ordonnanceur

  • Sur Linux, le contrôle CPU de Docker est basé sur les cgroups. Vos limites ne valent que par la version des cgroups et le comportement du noyau.
  • Choisissez le quota CPU pour “ce conteneur peut utiliser jusqu’à X temps CPU”. Choisissez cpuset pour “ce conteneur ne peut tourner que sur ces cœurs”.

Si vous êtes en astreinte et avez besoin d’une commande unique : identifiez le PID hôte le plus consommateur, mappez-le sur un conteneur, puis vérifiez s’il est déjà throttlé. Limiter un conteneur déjà throttlé, c’est comme dire à quelqu’un “calme-toi” alors que vous lui maintenez la tête sous l’eau.

Ce que signifie vraiment “CPU 100%” sur Docker

“CPU 100%” est un de ces métriques qui semble précis et qui se comporte comme des rumeurs.

  • Sur une machine à 4 cœurs, un seul cœur entièrement occupé représente 25 % de la capacité totale si vous mesurez la capacité totale.
  • Dans les outils Docker, le pourcentage CPU d’un conteneur peut être rapporté par rapport à un seul cœur ou tous les cœurs selon le calcul et la version.
  • Dans les cgroups, l’utilisation CPU est mesurée en temps (nanosecondes). Les limites sont appliquées comme des quotas par période, pas comme des “pourcentages” au sens humain.

L’idée opérationnelle clé : vous pouvez voir une machine affichant “100 % CPU” tandis que le service critique est lent parce qu’il est throttlé, privé par la file d’exécution, ou perd du temps en steal sur un hyperviseur.

Voici le modèle mental qui ne vous trahira pas :

  • Utilisation CPU vous dit combien de temps a été passé à exécuter.
  • File d’exécution vous dit combien de threads veulent tourner mais ne peuvent pas.
  • Throttling vous dit que le noyau a activement empêché un cgroup de tourner parce qu’il a atteint son quota.
  • Steal time vous dit que la VM voulait du CPU mais que l’hyperviseur a dit « pas maintenant ».

Faits et contexte : pourquoi c’est plus compliqué qu’il n’y paraît

Quelques points de contexte qui comptent en production parce qu’ils expliquent les bizarreries que vous verrez dans les sorties :

  1. Les limites CPU Docker sont des limites cgroup. Docker n’a pas inventé l’isolation CPU ; c’est une enveloppe autour des cgroups et namespaces Linux.
  2. v1 vs v2 des cgroups change le câblage. Beaucoup de sessions de debug “pourquoi ce fichier n’existe pas ?” se résument à “vous êtes en v2 maintenant”.
  3. Le contrôle de bande passante CFS (quota/period) a été intégré au noyau Linux bien avant la popularité des conteneurs ; les conteneurs l’ont démocratisé, pas inventé.
  4. Les CPU shares ne sont pas une limite dure. Les shares sont un poids utilisé seulement en cas de contention ; ils n’empêcheront pas un conteneur d’utiliser du CPU inactif.
  5. “cpuset” est ancien et direct. Le pinning aux cœurs est déterministe mais peut gaspiller du CPU si vous pinnez mal ou négligez la topologie NUMA.
  6. Le throttling peut ressembler à une “faible utilisation CPU”. Un conteneur peut être lent tout en affichant un CPU modeste parce qu’il passe du temps bloqué par l’application du quota.
  7. La charge moyenne inclut plus que le CPU. Sur Linux, la charge moyenne compte les tâches en sommeil ininterruptible aussi, donc des problèmes de stockage peuvent se faire passer pour des problèmes CPU.
  8. La virtualisation ajoute du steal time. Sur des hôtes surabonnés, le “CPU 100%” de la VM peut être majoritairement du “j’aurais tourné si j’avais pu”.
  9. La surveillance a une longue traîne de mensonges. Les métriques CPU sont faciles à collecter et faciles à mal interpréter ; différences de fenêtres d’échantillonnage et de normalisation créent des pics fantômes.

Une citation à garder sur votre bureau :

Werner Vogels (idée paraphrasée) : « Tout échoue ; concevez pour que l’échec soit attendu et géré, pas traité comme une exception. »

Tâches pratiques : commandes, sorties, décisions

Voici les tâches réelles que j’attends d’un ingénieur en astreinte. Chacune inclut : la commande, ce que la sortie signifie, et la décision que vous prenez à partir de là. Ne les exécutez pas toutes aveuglément ; faites-les en investigation avec embranchement.

Tâche 1 : Vérifier si l’hôte est réellement saturé CPU

cr0x@server:~$ uptime
 14:22:19 up 37 days,  6:11,  2 users,  load average: 18.42, 17.96, 16.10

Ce que cela signifie : Une charge moyenne ~18 sur une machine 8 cœurs est problématique ; sur une machine 32 cœurs ça peut être acceptable. La charge seule n’est pas une preuve de saturation CPU.

Décision : Ensuite, vérifiez l’inactivité CPU et la file d’exécution avec top ou mpstat. Si l’inactivité CPU est élevée, orientez-vous vers un problème I/O ou des verrous.

Tâche 2 : Vérifier l’inactivité CPU, le steal time, et les pires consommateurs

cr0x@server:~$ top -b -n1 | head -25
top - 14:22:27 up 37 days,  6:11,  2 users,  load average: 18.42, 17.96, 16.10
Tasks: 512 total,   9 running, 503 sleeping,   0 stopped,   0 zombie
%Cpu(s): 94.7 us,  2.1 sy,  0.0 ni,  0.6 id,  0.0 wa,  0.0 hi,  0.3 si,  2.3 st
MiB Mem :  32114.2 total,   1221.4 free,  14880.3 used,  16012.5 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.  14880.9 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
21483 root      20   0 1614920  82364  19624 R 380.0   0.3  76:12.39 python
12914 root      20   0 2344812 109004  40220 R 160.0   0.3  21:44.07 node
 9881 root      20   0  986604  61172  17640 R  95.0   0.2  12:11.88 java

Ce que cela signifie : Inactivité CPU ~0.6 % : vous êtes saturé CPU. Le steal time est 2,3 % : pas énorme, mais cela indique que l’hyperviseur prend quelques cycles.

Décision : Identifiez ces PIDs : sont-ils dans des conteneurs ? Si oui, mappez-les vers des IDs de conteneur. Sinon, vous avez un processus hôte problématique (ou un conteneur tournant avec l’espace de noms PID de l’hôte — oui, ça arrive).

Tâche 3 : Classement rapide des conteneurs via docker stats

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME                    CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
a12b3c4d5e6f   api-prod-1              265.42%   612.4MiB / 2GiB        29.90%    1.2GB / 980MB    11.2MB / 0B      78
b98c7d6e5f4a   worker-prod-queue       410.11%   1.1GiB / 1GiB          110.02%   120MB / 98MB     2.4GB / 1.9GB    213
c11d22e33f44   redis-prod              18.33%    3.2GiB / 4GiB          80.00%    420MB / 390MB    0B / 0B          6

Ce que cela signifie : worker-prod-queue est à la fois chaud en CPU et dépasse sa limite mémoire (110 % implique un risque d’échange/oom selon la configuration). Les chiffres CPU supérieurs à 100 % indiquent une utilisation multi-cœur.

Décision : Ne limitez pas encore le conteneur. Confirmez d’abord si ce worker effectue un travail légitime, thrash à cause de la pression mémoire, ou tourne en boucle. La pression mémoire peut provoquer une surconsommation CPU via GC, churn d’allocation ou tempêtes de retries.

Tâche 4 : Mapper un PID hôte vers son conteneur

cr0x@server:~$ ps -o pid,comm,cgroup -p 21483 | sed -n '1,3p'
  PID COMMAND         CGROUP
21483 python          0::/docker/b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a

Ce que cela signifie : Ce PID hôte appartient à un cgroup Docker (chemin unifié cgroups v2 affiché comme 0::). Le long hash correspond au préfixe d’un ID de conteneur.

Décision : Confirmez avec docker ps et docker inspect, puis enquêtez à l’intérieur de ce conteneur.

Tâche 5 : Confirmer l’identité du conteneur et l’entrypoint

cr0x@server:~$ docker ps --no-trunc | grep b98c7d6e5f4a
b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a   acme/worker:7f3c1d   "python /app/worker.py"   3 days ago   Up 3 days   worker-prod-queue

Ce que cela signifie : C’est un worker Python, probablement consommateur de file ou jobs batch.

Décision : Si c’est un worker batch, les caps CPU sont souvent sûrs. Si c’est sensible en latence (API), les caps peuvent devenir des pannes auto-infligées.

Tâche 6 : Vérifier si le conteneur est déjà limité

cr0x@server:~$ docker inspect -f 'NanoCpus={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} CpusetCpus={{.HostConfig.CpusetCpus}} CpuShares={{.HostConfig.CpuShares}}' worker-prod-queue
NanoCpus=0 CpuQuota=0 CpuPeriod=0 CpusetCpus= CpuShares=0

Ce que cela signifie : Aucune limite ou poids CPU explicite n’est configuré. Il peut consommer tout le CPU disponible.

Décision : Vous avez la possibilité de limiter. Mais d’abord, vérifiez ce qu’il fait, et contrôlez les métriques de throttling après l’application d’une limite.

Tâche 7 : Vérifier les stats CPU du cgroup v2 pour le throttling

cr0x@server:~$ CID=b98c7d6e5f4a9c5b6a3e1c2d9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a
cr0x@server:~$ CGP=$(docker inspect -f '{{.HostConfig.CgroupParent}}' $CID); echo "${CGP:-/sys/fs/cgroup}"
/sys/fs/cgroup
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.stat
usage_usec 129884420
user_usec 127110003
system_usec 2774417
nr_periods 0
nr_throttled 0
throttled_usec 0

Ce que cela signifie : Sur cet hôte, le cgroup du conteneur existe sous /sys/fs/cgroup/docker/<id>. Aucun throttling pour l’instant parce qu’aucun quota n’est configuré.

Décision : Si vous définissez ensuite un quota et que nr_throttled augmente rapidement avec un throttled_usec élevé, vous avez créé un plafond de débit/latence. Cela peut être correct, mais ce doit être intentionnel.

Tâche 8 : Valider si le “problème CPU” est du temps noyau ou utilisateur

cr0x@server:~$ pidstat -p 21483 1 3
Linux 6.5.0-18-generic (server) 	01/02/2026 	_x86_64_	(16 CPU)

14:23:31      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
14:23:32        0     21483   92.00    6.00    0.00    0.00   98.00     7  python
14:23:33        0     21483   93.00    5.00    0.00    0.00   98.00     7  python
14:23:34        0     21483   90.00    7.00    0.00    0.00   97.00     7  python

Ce que cela signifie : Majoritairement du temps utilisateur. Cela pointe vers du calcul applicatif ou une boucle serrée, pas un coût noyau.

Décision : Profilez à l’intérieur du conteneur (ou sur l’hôte pour ce PID) et cherchez des fonctions chaudes, des boucles actives, ou un backlog de file qui génère du vrai travail.

Tâche 9 : Entrer dans le conteneur et vérifier si c’est une fête de threads

cr0x@server:~$ docker exec -it worker-prod-queue bash -lc 'ps -eLo pid,tid,pcpu,comm --sort=-pcpu | head'
  PID   TID %CPU COMMAND
    1     1 96.4 python
    1    42 92.1 python
    1    43 91.8 python
    1    44 90.9 python
    1    45 90.2 python
   88    88  1.1 bash

Ce que cela signifie : Plusieurs threads sont chauds. Pour Python cela peut signifier plusieurs processus/threads, des extensions C qui travaillent, ou quelque chose comme gevent/eventlet qui brûle encore du CPU.

Décision : Si c’est une piscine de workers, limitez-la ou réduisez la concurrence. Si elle n’est pas censée être multithreadée, vérifiez une parallélisation accidentelle (par ex. une librairie qui spawn des threads, ou un changement de configuration).

Tâche 10 : Utiliser perf pour trouver les points chauds (côté hôte, pas besoin d’outils dans le conteneur)

cr0x@server:~$ sudo perf top -p 21483 -n 5
Samples: 1K of event 'cycles', 4000 Hz, Event count (approx.): 250000000
Overhead  Shared Object          Symbol
  22.11%  python                 [.] _PyEval_EvalFrameDefault
  15.37%  python                 [.] PyObject_RichCompare
  10.02%  libc.so.6              [.] __memcmp_avx2_movbe
   7.44%  python                 [.] list_contains
   6.98%  python                 [.] PyUnicode_CompareWithASCIIString

Ce que cela signifie : Le CPU va dans l’évaluation de l’interpréteur et les comparaisons. C’est du calcul réel, pas un bug noyau. Cela suggère aussi que la charge peut être lourde en comparaisons de données (filtres, dédup, scans).

Décision : Pour la contention immédiate : limitez le CPU ou régulez la concurrence du travail. À long terme : profilez au niveau applicatif ; peut-être faites-vous des comparaisons O(n²) sur un lot.

Blague #1 : Si votre worker fait du O(n²) dans une boucle, félicitations — il a réinventé le radiateur d’appoint.

Tâche 11 : Vérifier la pression de la file d’exécution par CPU

cr0x@server:~$ mpstat -P ALL 1 2
Linux 6.5.0-18-generic (server) 	01/02/2026 	_x86_64_	(16 CPU)

14:24:31     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %idle
14:24:32     all    92.11    0.00    5.02    0.00    0.00    0.44    2.12    0.31
14:24:32       7    99.00    0.00    0.90    0.00    0.00    0.10    0.00    0.00
14:24:32       8    97.00    0.00    2.70    0.00    0.00    0.30    0.00    0.00

Ce que cela signifie : Plusieurs CPU sont quasiment saturés. Si seuls quelques cœurs étaient chauds, vous envisageriez le cpuset pinning ou identifieriez un goulet mono-thread.

Décision : Si l’hôte est globalement saturé, limiter un conteneur est un geste d’équité. Si seulement un cœur est chaud, limiter par quota ne résoudra pas une limite mono-thread ; corrigez la concurrence ou le pinning.

Tâche 12 : Inspecter les contraintes de ressources du conteneur en cgroups v2 (quota et CPUs effectifs)

cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.max
max 100000
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpuset.cpus.effective
0-15

Ce que cela signifie : cpu.max est “quota/period”. max signifie illimité. La période est 100000 microsecondes (100 ms). Les CPUs effectifs montrent que le conteneur peut tourner sur les 16 CPUs.

Décision : Si vous voulez “l’équivalent de 2 CPUs”, vous mettrez le quota à 200000 pour la période 100000, ou utiliserez le flag pratique Docker --cpus=2.

Tâche 13 : Appliquer une limite CPU en direct (prudemment) et vérifier le throttling

cr0x@server:~$ docker update --cpus 4 worker-prod-queue
worker-prod-queue
cr0x@server:~$ docker inspect -f 'CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} NanoCpus={{.HostConfig.NanoCpus}}' worker-prod-queue
CpuQuota=400000 CpuPeriod=100000 NanoCpus=4000000000
cr0x@server:~$ cat /sys/fs/cgroup/docker/$CID/cpu.max
400000 100000

Ce que cela signifie : Le conteneur peut maintenant consommer jusqu’à 4 CPUs de temps par période de 100 ms. Docker a traduit --cpus en quota/period.

Décision : Surveillez le CPU hôte et la latence des services. Si le conteneur bruyant est non critique, c’est souvent la bonne solution immédiate. Si c’est critique, il faudra peut-être allouer plus de CPU ou scaler horizontalement.

Tâche 14 : Confirmer si la limite provoque du throttling (et si c’est acceptable)

cr0x@server:~$ sleep 2; cat /sys/fs/cgroup/docker/$CID/cpu.stat
usage_usec 131992884
user_usec 129050221
system_usec 2942663
nr_periods 2201
nr_throttled 814
throttled_usec 9811123

Ce que cela signifie : Du throttling a lieu (nr_throttled a augmenté). Le conteneur atteint son quota CPU. C’est attendu quand vous limitez une charge chaude.

Décision : Décidez si le throttling est l’objectif (protéger les autres services) ou si vous avez trop serré la limite (effondrement du débit, croissance des backlogs). Vérifiez la profondeur des files et la latence. Si le backlog augmente, augmentez la limite ou scalez les workers.

Tâche 15 : Identifier les threads les plus actifs du conteneur depuis l’hôte (sans exec)

cr0x@server:~$ ps -T -p 21483 -o pid,tid,pcpu,comm --sort=-pcpu | head
  PID   TID %CPU COMMAND
21483 21483 96.2 python
21483 21510 92.0 python
21483 21511 91.7 python
21483 21512 90.5 python
21483 21513 90.1 python

Ce que cela signifie : Les threads chauds sont visibles depuis l’hôte. Utile quand les images conteneurales sont minimales et sans outils de debug.

Décision : Si un thread domine, vous êtes en situation mono-thread. Si plusieurs threads sont chauds, les quotas se comporteront de façon plus prévisible.

Tâche 16 : Vérifier si vous luttez contre du CPU steal (surabonnement de l’hyperviseur)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
13  0      0 125132 210204 8023412    0    0     0     5 2841 7102 91  5  2  0  2
18  0      0 124980 210204 8023520    0    0     0     0 2911 7299 90  5  2  0  3
16  0      0 124900 210204 8023604    0    0     0     0 2780 7010 89  6  2  0  3
14  0      0 124820 210204 8023710    0    0     0    10 2894 7255 90  5  2  0  3
15  0      0 124700 210204 8023794    0    0     0     0 2810 7098 91  5  1  0  3

Ce que cela signifie : st (steal) est de 2–3 %. Pas catastrophique. Si vous voyez 10–30 %, vous n’êtes pas “limité CPU”, vous êtes “limité par les voisins matériels”.

Décision : Si le steal est élevé, les limites ne résoudront pas l’expérience. Déplacez les charges, redimensionnez les instances ou changez le placement des hôtes. Sinon, vous optimisez la mauvaise couche.

Comment limiter le CPU correctement (sans s’auto-saboter)

“Juste limiter” est la manière la plus sûre de créer l’incident suivant. Les limites CPU sont un contrat : vous dites au noyau « Cette charge peut être ralentie pour protéger le reste ». Rendez ce contrat explicite et testable.

Choisir le bon contrôle : quota vs shares vs cpuset

1) Quota/period CPU (la limitation raisonnable par défaut)

À utiliser quand : vous voulez qu’un conteneur obtienne au maximum N CPUs de temps, tout en permettant au planificateur de le placer sur plusieurs cœurs.

  • Flag Docker : --cpus 2 (pratique) ou --cpu-quota et --cpu-period.
  • Mécanisme noyau : contrôle de bande passante CFS.

Ce qui peut mal tourner : des limites agressives provoquent un throttling important, ce qui crée des latences en rafales. Votre appli devient un métronome : elle tourne, se fait throttler, tourne à nouveau.

2) CPU shares (un poids d’équité, pas une limite dure)

À utiliser quand : vous voulez une priorité relative entre conteneurs en cas de contention, mais vous acceptez que n’importe quel conteneur utilise le CPU libre quand l’hôte est inactif.

  • Flag Docker : --cpu-shares.

Ce qui peut mal tourner : des gens définissent des shares en attendant une limite, puis se demandent pourquoi un conteneur en folie occupe encore l’hôte la nuit.

3) Cpuset (pinning sur des cœurs)

À utiliser quand : vous avez des contraintes de licence, vous isolez des voisins bruyants, ou vous gérez la localité NUMA/caches intentionnellement. C’est pour les adultes qui aiment les graphiques.

  • Flag Docker : --cpuset-cpus 0-3.

Ce qui peut mal tourner : pinner sur les « mauvais » cœurs peut entrer en collision avec l’affinité IRQ, d’autres charges pinnées, ou laisser la moitié de la machine idle pendant qu’un cœur fond.

Mon approche préférée en production

  1. Commencez par un quota avec --cpus, fixé assez haut pour éviter un throttling constant.
  2. Mesurez le throttling via cpu.stat après le changement. Le throttling n’est pas automatiquement mauvais ; le throttling inattendu l’est.
  3. Si vous avez besoin d’isolation plus forte (ex. multi-tenant), ajoutez du cpuset, mais seulement après audit de la topologie CPU et de la distribution des interruptions.
  4. Utilisez les shares pour favoriser les services critiques par rapport aux best-effort, mais ne les prenez pas pour une ceinture de sécurité.

Quelle quantité de CPU donner ?

Ne devinez pas. Utilisez la demande observée par la charge et la tolérance business.

  • Pour les services API, gardez assez de CPU pour protéger le p99 de latence. Si le CPU chauffe, scaler horizontalement est souvent plus sûr que limiter.
  • Pour les workers/batch, limitez pour l’équité et alignez la concurrence sur la limite. Sinon vous allez juste throttler une ruée.
  • Pour les bases de données, soyez prudent : les limites CPU peuvent amplifier la latence en queue et créer des contentions de verrous. Préférez des nœuds dédiés ou des cpusets si vous devez isoler.

Validation consciente du throttling : quoi surveiller après une limite

Après avoir appliqué une limite, vérifiez ces signaux :

  • Charge hôte et inactivité CPU : les autres services se sont-ils rétablis ?
  • Throttling du conteneur : nr_throttled augmente-t-il constamment ?
  • Profondeur des files/backlog : Si le backlog croît, vous avez réduit le débit en dessous du taux d’arrivée.
  • Latence/taux d’erreur : Pour les services synchrones, les limites se traduisent souvent par des timeouts, pas seulement des réponses “plus lentes”.

Blague #2 : Les quotas CPU sont comme les budgets d’entreprise — tout le monde les déteste, mais l’alternative, c’est qu’une équipe achète six machines à expresso et appelle ça “infrastructure”.

Compose, Swarm et le piège « pourquoi ma limite n’a pas marché ? »

Il existe deux classes de tickets « ma limite CPU ne fonctionne pas » :

  1. Elle n’a jamais été appliquée. L’orchestrateur l’a ignorée ou vous l’avez mise dans la mauvaise section.
  2. Elle a été appliquée, mais vous avez mal mesuré. Vous attendiez « 50 % » et vous obtenez « toujours chaud » parce que l’hôte a beaucoup de cœurs, ou parce que la charge explose en rafales et est throttlée plus tard.

Docker Compose : pièges de version

Compose a historiquement deux emplacements pour les limites : l’ancien style cpu_shares/cpus et le style Swarm deploy.resources. Le piège : Compose non-Swarm ignore les limites sous deploy dans beaucoup de configurations. Les gens copient des configs de blogs et supposent que le noyau obéit au YAML.

Si vous voulez une limite fiable en local/Compose, validez-la avec docker inspect après docker compose up. Ne faites pas confiance au fichier sans vérification.

cr0x@server:~$ docker compose ps
NAME                 IMAGE             COMMAND                  SERVICE   CREATED         STATUS         PORTS
stack_worker_1        acme/worker:7f3c1d "python /app/worker.py" worker    2 minutes ago   Up 2 minutes

cr0x@server:~$ docker inspect -f 'CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} NanoCpus={{.HostConfig.NanoCpus}}' stack_worker_1
CpuQuota=200000 CpuPeriod=100000 NanoCpus=2000000000

Ce que cela signifie : Les limites sont réellement appliquées. Si elles affichent des zéros, vos paramètres Compose n’ont pas pris effet.

Décision : Corrigez la configuration pour que les limites soient appliquées là où votre runtime les respecte, ou faites un docker update puis formalisez-le correctement.

Différences Swarm et Kubernetes (opérationnel, pas philosophique)

  • Dans Swarm, deploy.resources.limits.cpus est réel et appliqué parce que Swarm programme les tâches avec ces contraintes.
  • Dans Kubernetes, les limites et requests CPU interagissent avec les classes QoS. Les limites peuvent throttler ; les requests influencent l’ordonnancement. “J’ai mis une limite” n’est pas la même chose que “J’ai garanti du CPU”.

Si vous déboguez sur des hôtes Docker mais que votre modèle mental vient de Kubernetes, prudence : vous risquez de manquer la nuance “request vs limit” qui affecte le comportement des voisins bruyants.

Trois mini-histoires d’entreprise depuis les tranchées CPU

Mini-histoire 1 : L’incident causé par une mauvaise hypothèse

L’entreprise avait un hôte Docker exécutant “juste quelques services”. Cette phrase est un mensonge que l’on se raconte pour se sentir maître de la situation. Un des services était une API, un autre un worker de queue, et un troisième un sidecar métriques que personne ne voulait toucher parce que “ça marchait”.

Pendant une période chargée, l’hôte a commencé à atteindre 100 % CPU. L’ingénieur d’astreinte a ouvert le dashboard, vu la courbe CPU de l’API monter, et a fait l’hypothèse raisonnable mais erronée : « L’API est le problème. » Il a limité le conteneur API à 1 CPU avec une mise à jour en direct. La CPU hôte est tombée. Tout le monde a soufflé pendant environ huit minutes.

Puis le taux d’erreur a augmenté. Des timeouts sont apparus. L’API n’était pas “réparée” ; elle avait été étranglée. Le vrai coupable était le conteneur worker qui inondait Redis de retries parce qu’il avait atteint une limite mémoire et commençait à swapper dans l’environnement cgroup du conteneur. Le worker avait déclenché une tempête de retries. L’API subissait simplement en essayant de suivre.

La correction n’a pas été dramatique. Ils ont retiré la limite sur l’API, ajouté une limite quota au worker, et — ce qui est toujours gênant — réduit la concurrence du worker pour qu’elle corresponde au nouveau contrat CPU. La tempête de retries s’est arrêtée. Redis s’est calmé. La CPU s’est normalisée. La leçon est restée : limitez le harceleur, pas la victime.

Mini-histoire 2 : L’optimisation qui a mal tourné

Une autre équipe avait une pipeline d’ingestion batch en conteneurs. Ils voulaient plus de débit et ont constaté que le CPU était sous-utilisé en heures creuses. Quelqu’un a proposé “plus de parallélisme” en augmentant les threads worker de 4 à 32. Ça semblait moderne. Ça semblait bien dans un test rapide. En production, cela s’est transformé en un effondrement au ralenti.

La pipeline décompressait des données et faisait de la validation de schéma. Avec 32 threads par conteneur et plusieurs conteneurs par hôte, ils ont créé un troupeau tonitruant sur le CPU et la mémoire. Le changement de contexte a grimpé. La localité du cache s’est dégradée. Le planificateur hôte a donné sa meilleure imitation d’un jongleur dans une tempête.

Ils ont essayé de limiter le CPU pour “stabiliser”. Ça a stabilisé, oui — de la même manière qu’une voiture “stabilise” en percutant un mur. Le throttling a explosé et le débit s’est effondré. Les files de travail se sont accumulées, et l’équipe a commencé à scaler les conteneurs. Cela a empiré la contention, car le goulet était la bande passante CPU et mémoire de l’hôte, pas le nombre de conteneurs.

Ce qui a finalement résolu le problème était ennuyeux : ils ont réduit le nombre de threads worker, puis augmenté légèrement le nombre de conteneurs mais en pinçant le workload batch sur une plage cpuset éloignée des services sensibles en latence. Ils ont aussi appris à mesurer nr_throttled et pas seulement le pourcentage CPU. L’“optimisation” avait échoué parce qu’elle supposait que le CPU est linéaire. Dans les systèmes réels, le parallélisme se fait concurrence à lui-même.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Il existe un type d’organisation qui n’aime pas les exploits parce qu’elle n’en a pas besoin. Une équipe plateforme avait une règle stricte : chaque conteneur en production doit déclarer des contraintes CPU et mémoire, et elles doivent être validées automatiquement après déploiement.

Les ingénieurs se sont plaints. Ils disaient que ça ralentissait le déploiement. Ils disaient que c’était de la “pensée Kubernetes” alors que c’était du Docker basique. L’équipe plateforme a poliment ignoré les plaintes et a continué d’appliquer la règle. Ils exigeaient aussi que chaque service définisse un mode de dégradation : quand le CPU est contraint, doit-on abandonner du travail, le mettre en file, ou échouer rapidement ?

Un après-midi, une mise à jour d’une librairie tierce a introduit une régression : une boucle active qui se déclenchait sous un patron d’entrée rare. Un sous-ensemble de requêtes provoquait des pics CPU. Normalement, cela aurait pris l’hôte et créé un incident touchant plusieurs services.

À la place, le conteneur affecté a atteint son quota CPU et a été throttlé. Il est devenu plus lent, oui, mais il n’a pas affamé le reste du nœud. Les autres services ont continué de servir. La surveillance a montré un signal clair : le throttling pour ce service seul a augmenté. L’astreinte a rapidement rollbacké. Pas de panne en cascade, pas de “tous les services dégradés”, et pas de salle de crise à minuit.

La pratique ennuyeuse — toujours définir des limites, toujours les valider, toujours définir le comportement en contrainte — n’a pas seulement empêché la contention des ressources. Elle a rendu le mode de défaillance lisible.

Erreurs courantes : symptômes → cause racine → correction

Cette section vous évite de rejouer les plus grands succès du passé.

1) Symptom : L’hôte affiche CPU à 100 %, mais docker stats ne montre rien d’extrême

Cause racine : Processus hôtes chauds (journald, node exporter, threads noyau), ou des conteneurs tournent avec l’espace de noms PID de l’hôte, ou l’échantillonnage de docker stats rate des pics courts.

Correction : Utilisez d’abord les outils PID de l’hôte : top, puis mappez les PIDs chauds vers des conteneurs via ps -o cgroup. Si aucun cgroup Docker n’apparaît, ce n’est pas un problème de conteneur.

2) Symptom : Après avoir mis --cpus, le service devient plus lent et des timeouts augmentent

Cause racine : Vous avez limité un service sensible à la latence en dessous de ses besoins p99, provoquant des rafales de throttling et de la mise en file de requêtes.

Correction : Supprimez ou augmentez la limite ; scalez horizontalement ; ajoutez du backpressure et des limites de concurrence. Mesurez cpu.stat et la latence des requêtes ensemble.

3) Symptom : Vous avez défini des CPU shares mais le conteneur occupe toujours l’hôte

Cause racine : Les shares sont des poids, pas une limite. Si du CPU inactif est disponible, le conteneur peut le prendre.

Correction : Utilisez --cpus ou --cpu-quota pour une limite dure. Gardez les shares pour la priorisation sous contention.

4) Symptom : La charge moyenne est énorme, mais l’inactivité CPU est aussi élevée

Cause racine : Tâches bloquées (iowait, sommeil ininterruptible), contention de verrous, ou stalls système de fichiers. La charge moyenne compte plus que les tâches exécutables.

Correction : Vérifiez top pour wa, utilisez iostat si disponible, inspectez les tâches bloquées et cherchez un goulet de stockage/réseau. Ne “limitez pas le CPU” pour un problème I/O.

5) Symptom : Le CPU chauffe sur un seul cœur, et les performances sont terribles

Cause racine : Goulet mono-thread, verrou global, ou un shard/partition chaud.

Correction : N’ajoutez pas de limites CPU ; corrigez la concurrence ou le partitionnement. Si vous devez isoler, le pinning cpuset peut empêcher un thread chaud d’interférer avec tout le reste, mais ne le rendra pas plus rapide.

6) Symptom : L’utilisation CPU semble correcte, mais le service est lent

Cause racine : Throttling : le conteneur est limité et passe du temps sans pouvoir s’exécuter. Le pourcentage CPU peut sembler modéré parce que le temps throttlé n’est pas “utilisation CPU”.

Correction : Lisez cpu.stat (nr_throttled, throttled_usec). Augmentez la limite, réduisez la concurrence, ou scalez.

7) Symptom : Les pics CPU arrivent après que vous ayez “optimisé” le logging ou les métriques

Cause racine : Métriques haute-cardinalité, formatage de logs coûteux, logging synchrone, ou contention dans les pipelines de télémétrie.

Correction : Réduisez la cardinalité, échantillonnez, batch, ou déplacez le formatage lourd hors des chemins chauds. Limitez aussi les sidecars de télémétrie ; ils ne sont pas innocents.

8) Symptom : Après avoir pinning cpuset, le débit baisse et certains CPUs sont inactifs

Cause racine : Mauvaise sélection de cœurs, interférence avec les IRQ, mismatch NUMA, ou pinning de trop peu de cœurs pour des patterns en rafales.

Correction : Préférez d’abord le quota. Si vous utilisez cpuset, auditez la topologie CPU, considérez les nœuds NUMA, et gardez de la marge pour le travail noyau/interruptions.

Listes de contrôle / plan pas à pas

Étape par étape : localiser le conteneur bruyant

  1. Mesurez l’hôte : uptime, top. Confirmez que l’inactivité CPU est faible et vérifiez la répartition utilisateur/système.
  2. Classez rapidement les conteneurs : docker stats --no-stream.
  3. Classez les PIDs hôte : dans top triez par CPU, copiez les PIDs en tête.
  4. Mappez PID → cgroup : ps -o cgroup -p <pid>. Si c’est sous /docker/<id>, vous avez votre conteneur.
  5. Confirmez le nom/image du conteneur : docker ps --no-trunc | grep <id> et docker inspect.
  6. Validez ce qu’il fait : pidstat, perf top, ou ps à l’intérieur du conteneur.

Étape par étape : le limiter en toute sécurité

  1. Décidez l’objectif : protéger les autres workloads vs préserver le débit de celui-ci.
  2. Choisissez le contrôle : quota (--cpus) pour la plupart des cas ; shares pour pondération relative ; cpuset pour isolation forte.
  3. Appliquez la limite en direct (si nécessaire) : docker update --cpus N <container>.
  4. Vérifiez l’application : docker inspect et cat cpu.max (v2) ou les fichiers équivalents v1.
  5. Mesurez le throttling : vérifiez cpu.stat après quelques secondes.
  6. Surveillez les SLOs : latence, erreurs, profondeur des files, retries. Si ça se dégrade, augmentez la limite ou scalez.
  7. Rendez-la permanente : mettez à jour Compose/Swarm ou votre pipeline de déploiement ; ne laissez pas un docker update vivre comme une magie tribale.

Étape par étape : prévenir la récurrence

  1. Définissez des valeurs par défaut : chaque service déclare des contraintes CPU et mémoire.
  2. Validez automatiquement : des contrôles post-deploiement comparent les limites désirées et docker inspect.
  3. Instrumentez le throttling : alertez sur des augmentations soutenues de nr_throttled pour les services critiques.
  4. Alignez la concurrence : les tailles des pools de workers doivent suivre les limitations CPU ; évitez “32 threads parce que des cœurs existent quelque part”.
  5. Interrupteurs d’arrêt : feature flags ou limits de taux pour réduire la création de travail lors des pics.

FAQ

1) Pourquoi docker stats affiche 400 % CPU pour un conteneur ?

Parce qu’il utilise environ 4 cœurs de CPU pendant la fenêtre d’échantillonnage. Le %CPU est souvent normalisé sur un seul cœur, donc l’utilisation multi-cœur dépasse 100 %.

2) Est-ce que --cpus équivaut à --cpuset-cpus ?

Non. --cpus utilise le quota/period CFS (limite basée sur le temps). --cpuset-cpus restreint les CPUs sur lesquels le conteneur peut tourner (isolation basée sur le placement). Ils résolvent des problèmes différents.

3) Les CPU shares empêcheront-ils un conteneur en folie de saturer l’hôte ?

Seulement en cas de contention. Si l’hôte a du CPU inactif, les shares ne stopperont pas un conteneur qui le prend. Utilisez un quota pour une limite dure.

4) Comment savoir si le throttling me nuit ?

Lisez cpu.stat. Si nr_throttled et throttled_usec augmentent régulièrement pendant que la latence/backlog s’aggrave, vous avez limité trop sévèrement la charge actuelle.

5) Pourquoi la charge moyenne est élevée mais l’inactivité CPU n’est pas zéro ?

La charge moyenne inclut des tâches en attente d’I/O (sommeil ininterruptible), pas seulement des tâches prêtes à exécuter. Une forte charge avec de l’inactivité peut signifier des stalls de stockage, des verrous, ou d’autres blocages.

6) Puis-je limiter le CPU d’un conteneur en cours d’exécution sans le redémarrer ?

Oui : docker update --cpus N <container> (et flags associés) s’applique en live. Traitez cela comme un levier d’urgence ; formalisez ensuite les changements dans votre config de déploiement.

7) J’ai mis des limites dans docker-compose.yml sous deploy: mais rien n’a changé. Pourquoi ?

Parce que les limites sous deploy sont principalement pour Swarm mode. Beaucoup d’installations Compose non-Swarm les ignorent. Confirmez toujours avec docker inspect que les limites ont été appliquées.

8) Dois-je limiter le conteneur base de données quand le CPU est chaud ?

Généralement non, pas en premier geste. Les bases de données sous pression CPU ont souvent besoin de plus de CPU, d’optimisation de requêtes, ou d’isolation. Limiter peut transformer une contention brève en latence longue et des acumuls de verrous.

9) “CPU 100%” à l’intérieur d’un conteneur — est-ce la même chose que sur l’hôte ?

Ça dépend de l’outil et de la normalisation. À l’intérieur du conteneur vous pouvez voir une vue qui ignore la capacité hôte. Faites confiance à la comptabilité au niveau hôte et aux stats cgroup pour les limites et le throttling.

10) Le pinning cpuset vaut-il le coup pour résoudre les voisins bruyants ?

Parfois. C’est puissant et déterministe, mais facile à mal utiliser. Commencez par des quotas et des shares ; passez au cpuset quand vous avez besoin d’isolation forte et que vous comprenez la topologie CPU.

Conclusion : prochaines étapes pratiques

Quand des hôtes Docker atteignent 100 % CPU, la bonne action n’est pas de “redémarrer des trucs jusqu’à ce que le graphe ait meilleure mine”. La bonne action est : identifier le conteneur (et le PID) exact, valider s’il fait du travail réel ou du non-sens, puis limiter avec intention et confirmer le comportement de throttling.

La prochaine fois que l’hôte chauffe, faites ceci :

  • Utilisez top pour obtenir les PIDs les plus chauds, mappez-les vers des conteneurs via les cgroups.
  • Considérez docker stats comme un indice, pas un verdict.
  • Avant de limiter, décidez si la charge est sensible à la latence ou orientée débit.
  • Appliquez docker update --cpus pour un confinement rapide, puis confirmez via cpu.stat si le throttling est acceptable.
  • Après l’incident, rendez les limites permanentes, validez-les automatiquement, et alignez la concurrence des workers sur le contrat CPU.

Les urgences CPU ne sont que rarement mystérieuses. Elles sont généralement sous-instrumentées, trop supposées, et sous-limitée. Corrigez ces trois aspects, et vos rotations d’astreinte seront plus courtes — et un peu moins poétiques.

← Précédent
ZFS : chemins de périphériques manquants — utiliser by-id et WWN intelligemment
Suivant →
Dnodes et métadonnées ZFS : pourquoi les métadonnées peuvent être votre vrai goulot d’étranglement

Laisser un commentaire