Optimiser les performances des conteneurs Docker avec les limites CPU et mémoire
Apprenez à optimiser les performances des conteneurs Docker en définissant des limites CPU et mémoire. Ce guide couvre les options de configuration essentielles comme les parts CPU, les quotas, les limites mémoire et le swap. Découvrez comment surveiller l'utilisation des ressources des conteneurs avec `docker stats` et mettre en œuvre les meilleures pratiques pour éviter la famine des ressources, améliorer la stabilité des applications et renforcer l'efficacité globale du système.
Optimiser les performances des conteneurs Docker avec les limites CPU et mémoire
Les conteneurs Docker ne se comportent pas automatiquement comme de petites machines virtuelles avec des ressources fixes. Sauf indication contraire, un conteneur peut rivaliser pour le CPU et la mémoire de l'hôte comme n'importe quel autre processus. C'est pratique sur un ordinateur portable mais risqué sur un serveur partagé.
Les limites CPU et mémoire ne sont pas des boosters de performance magiques. Une limite trop basse rend une application plus lente ou instable. Une limite trop haute ne protège pas l'hôte. L'objectif est de donner à chaque conteneur suffisamment d'espace pour un travail normal et de pointe tout en empêchant un processus défaillant de faire tomber tout le reste sur la machine.
Pourquoi les limites sont importantes
Le mode de défaillance courant est familier : un conteneur lance un processus incontrôlé, la mémoire monte, l'hôte commence à swapper, et les services non liés ralentissent. Ou une tâche par lots gourmande en CPU utilise tous les cœurs et le conteneur API à côté commence à manquer les objectifs de latence.
Les limites aident à résoudre trois problèmes pratiques :
- Elles protègent l'hôte d'un seul conteneur utilisant toute la mémoire.
- Elles rendent les tests de performance plus honnêtes car le conteneur fonctionne avec des contraintes similaires à la production.
- Elles vous obligent à remarquer quand une application nécessite une mise à l'échelle ou un réglage au lieu d'emprunter silencieusement des ressources aux voisins.
Ne commencez pas avec des nombres aléatoires. Exécutez le service sous charge, surveillez l'utilisation réelle et définissez des limites avec une marge de manœuvre. Un petit worker en arrière-plan et un service JVM nécessitent un traitement très différent.
Contrôles CPU
Docker expose quelques contrôles CPU. Le plus utile au quotidien est --cpus.
Limitez un conteneur à environ un CPU et demi :
docker run -d --name api --cpus="1.5" nginx
C'est plus facile à lire que de définir manuellement le quota et la période CFS. Sous le capot, Docker utilise les mécanismes de planification CPU de Linux.
--cpu-shares est différent. C'est un poids relatif en cas de contention CPU, pas une limite stricte :
docker run -d --name important-api --cpu-shares 2048 nginx
docker run -d --name batch-worker --cpu-shares 512 worker-image
Quand l'hôte a du CPU inactif, les deux conteneurs peuvent en utiliser plus. Quand ils sont en compétition, le premier conteneur a plus de poids de planification. C'est utile pour la priorisation, mais cela n'empêche pas un conteneur d'utiliser du CPU de réserve.
Si vous avez besoin de paramètres CFS exacts, --cpu-period et --cpu-quota sont toujours disponibles :
docker run -d --name limited-api \
--cpu-period 100000 \
--cpu-quota 50000 \
nginx
Cela donne au conteneur 50 000 microsecondes de temps CPU dans chaque période de 100 000 microsecondes, soit environ la moitié d'un CPU. Pour la plupart des équipes, --cpus="0.5" communique la même intention plus clairement.
Contrôles mémoire
Les limites mémoire sont plus dangereuses que les limites CPU car les dépasser peut tuer le processus. Définissez-les délibérément et testez le comportement de pointe.
L'option de base est --memory :
docker run -d --name web --memory 512m nginx
Si le conteneur dépasse la limite, le noyau peut tuer un processus dans le conteneur. Dans la sortie Docker, vous verrez souvent cela comme un kill OOM :
docker inspect web --format '{{.State.OOMKilled}}'
Le comportement du swap est facile à mal comprendre. Quand --memory est défini, --memory-swap est le total de mémoire plus l'allocation de swap, pas l'allocation de swap seule.
Cela permet 256 Mo de RAM et jusqu'à 256 Mo de swap, pour un total de 512 Mo :
docker run -d --name worker --memory 256m --memory-swap 512m alpine
Définir --memory-swap égal à --memory désactive le swap supplémentaire pour ce conteneur sur les systèmes où la comptabilité swap est disponible :
docker run -d --name no-extra-swap --memory 256m --memory-swap 256m alpine
Définir --memory-swap -1 permet un swap illimité jusqu'au swap disponible de l'hôte. Cela peut maintenir un processus en vie, mais peut rendre la latence terrible.
Surveiller avant et après
Utilisez docker stats pendant que le service est sous charge réaliste :
docker stats
docker stats web worker
Surveillez CPU %, MEM USAGE / LIMIT, BLOCK I/O et PIDS. Un service à 100 pour cent de sa limite mémoire n'est pas "efficace" ; il est à un pic de trafic d'être tué. Un service lié au CPU qui est constamment limité peut montrer une utilisation CPU moyenne acceptable tandis que les utilisateurs voient des requêtes lentes.
Pour une vue unique rapide :
docker stats --no-stream
Pour les indices OOM et de redémarrage :
docker ps -a
docker inspect <conteneur> --format 'OOM={{.State.OOMKilled}} Exit={{.State.ExitCode}}'
docker logs --tail 100 <conteneur>
Pour une surveillance continue, exportez les métriques des conteneurs vers Prometheus, Grafana, un service de surveillance cloud ou la plateforme que vous utilisez déjà. docker stats est un triage, pas un système d'alerte à long terme.
Un workflow de réglage sensé
Commencez sans limites strictes dans un environnement de test et capturez l'utilisation au repos, normale et de pointe. Ensuite, définissez la mémoire au-dessus du pic observé avec une marge suffisante pour le garbage collection, la croissance du cache, les poignées de main TLS et les courtes rafales. Pour le CPU, décidez si vous avez besoin d'une limite stricte ou juste d'une priorité relative.
Exemple pour une petite API :
docker run -d --name api \
--cpus="2" \
--memory 1g \
--memory-swap 1g \
-p 8080:80 \
my-api:latest
Cela signifie que l'API obtient jusqu'à deux CPUs, 1 Go de RAM et aucune allocation de swap supplémentaire. Ce n'est pas universellement correct. C'est un point de départ clair pour un service qui a été testé près de cette plage.
Pour Docker Compose, le moteur Docker local prend en charge des options de ressources telles que :
services:
api:
image: my-api:latest
mem_limit: 1g
cpus: 2.0
Les paramètres de ressources de Compose et Swarm/Kubernetes ne sont pas identiques, alors vérifiez la cible de déploiement avant de supposer qu'un champ a le même comportement partout.
Erreurs courantes
Définir la mémoire trop basse est l'erreur la plus courante. L'application démarre, passe un test de fumée, puis meurt sous un modèle de requête qui alloue plus de mémoire que le test n'a couvert.
Utiliser les limites CPU pour cacher un code inefficace en est une autre. Si un service est lent parce qu'il effectue un travail coûteux par requête, une limite CPU plus basse rend les symptômes plus évidents. Cela ne corrige pas le chemin de code.
Ignorer les environnements d'exécution des langages nuit aussi. Les applications JVM, Node.js, Go, Python et Ruby réagissent différemment à la pression mémoire. Certains environnements d'exécution ont besoin de paramètres de tas explicites pour que leurs hypothèses mémoire internes correspondent à la limite du conteneur.
Enfin, ne réglez pas seulement un conteneur. L'hôte a toujours besoin de CPU et de mémoire pour le noyau, Docker, la journalisation, les agents de surveillance et tous les processus sidecar. Laissez de l'espace.
De bonnes limites rendent les défaillances plus petites et les performances plus prévisibles. Elles doivent provenir de mesures, pas de suppositions.