Dépannage des conteneurs Docker lents : un guide de performance étape par étape
Découvrez pourquoi les conteneurs Docker sont lents en vérifiant le CPU, la mémoire, les E/S disque, le réseau, les limites, les montages et la journalisation.
Dépannage des conteneurs Docker lents : un guide de performance étape par étape
Lorsqu'un conteneur Docker semble lent, ne commencez pas par reconstruire l'image ou modifier des indicateurs d'exécution aléatoires. Déterminez d'abord ce que signifie « lent ». Le temps de réponse de l'API est-il élevé ? Un worker prend-il du retard ? Le démarrage est-il lent ? Les builds sont-ils lents ? L'hôte est-il surchargé ? Chaque cas pointe vers une solution différente.
Un conteneur n'est pas une isolation magique des lois de la physique. Il utilise toujours le CPU de l'hôte, la mémoire de l'hôte, le stockage de l'hôte, le réseau de l'hôte et le code applicatif que vous avez livré. Docker ajoute des contrôles et des espaces de noms autour de ces ressources, mais cela ne rend pas une requête lente rapide ni un disque saturé inactif.
Commencez par une vue en direct rapide :
docker stats
Observez le conteneur pendant que vous reproduisez le ralentissement. Un seul instantané est moins utile que de voir ce qui change sous charge. Si le CPU monte et reste élevé, vous avez un problème de CPU. Si la mémoire augmente jusqu'à ce que le conteneur meure, suivez la piste de la mémoire. Si BLOCK I/O bouge fortement alors que les requêtes stagnent, le stockage mérite attention. Si le conteneur semble calme mais que les utilisateurs constatent toujours une latence, examinez l'application, les appels réseau, la base de données ou les services en amont.
D'abord, comparez la santé du conteneur et de l'hôte
Un conteneur lent peut simplement vivre sur un hôte lent. Vérifiez les deux niveaux.
docker stats <conteneur>
top
free -h
df -h
Sous Linux, iostat -xz 1 est utile si disponible. Une utilisation élevée du disque ou des temps d'attente longs peuvent expliquer des bases de données lentes, des installations de paquets et des services à forte journalisation. Sous Docker Desktop, vérifiez également le CPU et la mémoire attribués à la VM Docker. Un Mac avec beaucoup de mémoire peut encore affamer les conteneurs si Docker Desktop est limité trop bas.
Si tous les conteneurs sont lents, l'hôte est suspect. Si un conteneur est lent alors que les voisins vont bien, concentrez-vous sur cette charge de travail, ses limites, ses montages et ses dépendances.
Goulots d'étranglement CPU
Dans docker stats, le CPU peut dépasser 100 % car Docker rapporte l'utilisation sur tous les cœurs. Un conteneur utilisant 200 % utilise environ deux cœurs. La question importante est de savoir si cela est attendu pour la charge de travail.
Vérifiez les limites d'exécution :
docker inspect <conteneur> --format 'NanoCPUs={{.HostConfig.NanoCpus}} CpuQuota={{.HostConfig.CpuQuota}} CpuPeriod={{.HostConfig.CpuPeriod}} Cpuset={{.HostConfig.CpusetCpus}}'
Si un service a été démarré avec --cpus=0.5, il peut être limité sous un trafic normal. Dans Kubernetes ou Compose, le même problème peut se cacher dans les limites CPU. Un worker qui traitait rapidement des travaux sur un ordinateur portable peut ramper dans CI parce qu'il n'obtient qu'un demi-CPU.
Pour le CPU au niveau applicatif, profilez le processus au lieu de deviner. Pour Node, utilisez le profilage CPU intégré ou des outils de type clinic. Pour Python, échantillonnez avec py-spy là où c'est autorisé. Pour Java, utilisez JFR ou async-profiler. Si vous ne pouvez pas installer d'outils dans une image de production, exécutez la même image dans un environnement de staging ou utilisez un modèle de conteneur de débogage.
Les causes CPU courantes incluent les boucles d'interrogation serrées, la sérialisation JSON coûteuse, le backtracking regex, le traitement d'images, la compression et trop de threads de travail se battant pour trop peu de cœurs. Augmenter le CPU n'aide que si l'application peut l'utiliser et que l'hôte a la capacité.
Pression mémoire et kills OOM
Les problèmes de mémoire se manifestent par une utilisation croissante de la mémoire, un garbage collection fréquent, une activité de swap sur l'hôte ou des sorties soudaines. Confirmez le statut OOM :
docker inspect <conteneur> --format 'exit={{.State.ExitCode}} oom={{.State.OOMKilled}} memory={{.HostConfig.Memory}}'
Si OOMKilled=true, le conteneur a dépassé sa situation mémoire. Cela peut être une limite --memory explicite, une limite de VM Docker Desktop ou une pression à l'échelle de l'hôte.
Utilisez docker stats tout en envoyant du trafic réaliste. Si la mémoire augmente sans s'aplatir, suspectez une fuite, un cache non borné, une accumulation de file d'attente ou une charge de travail qui charge trop de données à la fois. Si la mémoire augmente pendant le démarrage puis se stabilise, la limite est peut-être simplement trop basse pour l'exécution.
Les valeurs par défaut du langage comptent. Java, Node et certains serveurs d'applications peuvent réserver ou utiliser la mémoire différemment dans les conteneurs selon la version et la configuration. Définissez des options de tas ou de mémoire explicites lorsque vous avez besoin d'un comportement prévisible. Par exemple, un service Java peut avoir besoin de pourcentages de tas adaptés aux conteneurs ; un service Node peut avoir besoin de --max-old-space-size ; une base de données a besoin de paramètres de cache qui laissent de la place pour le processus et le système de fichiers.
Ne définissez pas les limites mémoire si serrées que l'application passe tout son temps à collecter les déchets. Un conteneur qui ne plante jamais mais qui se met constamment en pause est toujours cassé.
E/S disque et montages bind lents
Les problèmes de stockage sont faciles à manquer car les graphiques CPU et mémoire semblent normaux. Dans Docker, la lenteur du disque provient souvent de l'un des quatre endroits : E/S applicatives lourdes, journaux excessifs, pilote de stockage ou montages bind sur Docker Desktop.
Vérifiez la vue de Docker :
docker stats <conteneur>
docker logs --tail 20 <conteneur>
Si les journaux sont extrêmement bavards, le pilote de journalisation a du travail. Les journaux JSON-file peuvent grossir rapidement si la rotation n'est pas configurée. Sur un service occupé, journaliser chaque corps de requête ou ligne de débogage peut devenir un véritable problème de performance.
Inspectez les paramètres de journalisation :
docker inspect <conteneur> --format '{{json .HostConfig.LogConfig}}'
Pour les configurations locales et de petits serveurs, envisagez la rotation des journaux dans la configuration du démon ou le fichier Compose. Pour les plateformes de production, envoyez les journaux au système de journalisation de la plateforme et gardez le volume des journaux applicatifs intentionnel.
Les montages bind méritent une attention particulière sur macOS et Windows. Un arbre source monté de l'hôte dans un conteneur Linux traverse une couche de virtualisation. C'est pratique pour le développement, mais cela peut être beaucoup plus lent qu'un volume nommé pour les dossiers de dépendances, les bases de données ou les répertoires à forte écriture.
Par exemple, un conteneur de développement Node peut être lent si node_modules se trouve sur un montage bind. Un meilleur modèle est de monter le code source mais de garder les dépendances dans un volume nommé :
services:
app:
volumes:
- .:/app
- node_modules:/app/node_modules
volumes:
node_modules:
Pour les bases de données, préférez les volumes nommés aux montages bind, sauf si vous avez un workflow de sauvegarde ou d'inspection spécifique qui nécessite des chemins hôte.
Latence réseau et lenteur des dépendances
Un conteneur peut être « lent » parce qu'il attend un autre service. Le processus local peut être sain tandis que le DNS, une base de données, Redis, une API ou un proxy est lent.
Testez depuis l'intérieur du conteneur :
docker exec -it <conteneur> sh
curl -w '
lookup:%{time_namelookup} connect:%{time_connect} start:%{time_starttransfer} total:%{time_total}
' -o /dev/null -s http://service:8080/health
Cette sortie curl -w sépare la recherche DNS, la connexion TCP, le premier octet et le temps total. Si la recherche DNS est lente, inspectez /etc/resolv.conf et les paramètres DNS du démon Docker. Si la connexion est lente ou échoue, vérifiez les réseaux, les pare-feux et la liaison de service. Si le temps jusqu'au premier octet est lent, le service en amont a accepté la connexion mais a mis du temps à répondre.
Pour le trafic conteneur-à-conteneur, utilisez un réseau pont défini par l'utilisateur afin que les conteneurs puissent se résoudre par nom :
docker network create appnet
docker run -d --name api --network appnet my-api
docker run --rm --network appnet curlimages/curl http://api:8080/health
Ne faites pas de benchmark via les ports hôte publiés lorsque le trafic réel est conteneur-à-conteneur. Testez le chemin utilisé en production.
La performance au démarrage est un problème distinct
Un démarrage lent provient souvent du temps de téléchargement de l'image, de l'installation des dépendances au démarrage du conteneur, des migrations de base de données ou du préchauffage de l'application.
Un conteneur ne devrait pas installer de paquets à chaque démarrage. Si votre point d'entrée exécute npm install, pip install, apt-get ou télécharge des binaires à chaque démarrage, déplacez ce travail dans la construction de l'image, sauf s'il y a une forte raison de ne pas le faire.
Vérifiez les journaux de démarrage avec des horodatages si votre application les fournit. Sinon, ajoutez des horodatages simples autour des étapes du point d'entrée pendant le débogage :
date; echo 'démarrage des migrations'
# commande de migration
date; echo 'démarrage du serveur'
# commande du serveur
Pour les images téléchargées sur un réseau, la taille de l'image compte. Les builds multi-étapes, .dockerignore et les bases d'exécution plus petites améliorent la vitesse de démarrage à froid et de déploiement. Mais une fois que l'image est déjà présente et que le conteneur est en cours d'exécution, la taille de l'image importe généralement moins que le CPU, la mémoire, les E/S et le comportement de l'application.
La performance de build n'est pas la performance d'exécution
Les builds Docker lents sont frustrants, mais ils constituent une classe de problème différente. Si les modifications de code forcent l'installation des dépendances à chaque build, corrigez l'ordre des couches :
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
Ne copiez pas tout le dépôt avant d'installer les dépendances, sauf si vous voulez que chaque modification de source invalide la couche de dépendances.
Gardez également le contexte de build petit :
.git
node_modules
coverage
dist
*.log
Les caches de montage BuildKit peuvent aider pour les téléchargements répétés de dépendances, mais assurez-vous d'abord que le Dockerfile est correctement ordonné. Un cache de montage ne peut pas sauver complètement un Dockerfile qui invalide le cache trop tôt.
Les limites de ressources peuvent protéger l'hôte et nuire à l'application
Les limites CPU et mémoire sont utiles car un conteneur ne devrait pas faire tomber un hôte. Elles peuvent également créer une lenteur artificielle si elles sont copiées d'un exemple sans mesurer la charge de travail.
Inspectez les limites :
docker inspect <conteneur> --format '{{json .HostConfig}}' | jq '{Memory, NanoCpus, CpuQuota, CpuPeriod, BlkioWeight}'
Si jq n'est pas disponible, inspectez le conteneur normalement et cherchez HostConfig.
Pour Compose, vérifiez la configuration réelle rendue :
docker compose config
Cela capture les limites héritées des fichiers de remplacement ou des variables d'environnement. Une surprise courante est un fichier de remplacement de développement qui définit des limites basses et est accidentellement utilisé dans un environnement de test.
Un flux de diagnostic pratique
Utilisez ce flux lorsque la plainte est simplement « le conteneur est lent » :
- Reproduisez le comportement lent et exécutez
docker statspendant la reproduction. - Vérifiez le CPU, la mémoire, le disque de l'hôte et les limites de la VM Docker Desktop.
- Inspectez les limites CPU et mémoire du conteneur.
- Lisez les journaux pour les tentatives, les timeouts de connexion, les migrations, la journalisation de débogage ou les indices OOM.
- Testez les dépendances depuis l'intérieur du conteneur avec
curl,digou une image de débogage dédiée. - Vérifiez les montages : déplacez les chemins à forte écriture vers des volumes nommés lorsque c'est approprié.
- Profilez l'application si les graphiques de ressources pointent vers le code.
Les meilleures solutions ont tendance à être spécifiques : augmenter une limite mémoire trop basse, arrêter de journaliser des charges utiles énormes, déplacer les données de base de données d'un montage bind, corriger un chemin DNS lent, réordonner les couches du Dockerfile ou ajuster l'exécution de l'application. Un conseil générique « optimisez Docker » est moins utile que de prouver quelle ressource est réellement lente.