Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Lire les journaux et attribuer une saturation

Par mis à jour le

Améliorer cette page

Logo Docker

Une requête traverse trois services, elle met une demi-seconde à répondre, et aucun journal ne dit où le temps est passé. Cette leçon s'adresse à quelqu'un qui sait écrire et lancer une stack Compose : vous y apprenez à lire les journaux de toute la stack, à suivre une requête de bout en bout, puis à désigner le service saturé avec les compteurs du noyau.

Vous verrez aussi deux pièges : le service qui écrit le plus de journaux n'est pas celui qui est saturé, et un journal mal réglé recopie un secret dans un fichier qui reste lisible bien après la requête. La leçon se termine par le risque que les journaux font courir au disque : un journal json-file grossit sans borne.

  • Lire les journaux d'une stack, les ordonner avec --timestamps et les filtrer avec --since, --tail et --no-log-prefix.
  • Reconstituer une requête à travers plusieurs services avec un identifiant commun.
  • Distinguer ce que chaque journal dit, ou ne dit pas, du temps passé.
  • Reconnaître un journal qui copie un secret, et le corriger.
  • Attribuer une saturation à un service avec cpu.max, cpu.stat et docker stats.
  • Mesurer la croissance d'un journal json-file sans borne et comprendre le risque pour le disque.

Il vous faut un hôte Linux avec Docker Engine 29.9.2 et son plugin Compose 5.6.1, installés depuis le dépôt officiel comme le montre la leçon d'installation de Docker. Le noyau Linux compte et limite les ressources d'un conteneur dans un cgroup (un groupe de contrôle). Les fichiers lus plus bas (/sys/fs/cgroup/...) sont ceux de cgroup v2, la version que Debian 13 utilise. Lire les fichiers de journaux sur le disque demande sudo. Les commandes curl et awk servent aux mesures.

Vous savez écrire une stack avec Compose, et vous avez vu les variantes de fichiers dans Développement et déploiement : variantes explicites. La leçon Encadrer ressources et redémarrage d'une stack a présenté cpus, cpu.max et le compteur nr_throttled ; cette leçon-ci les réutilise pour trouver un coupable.

Pour comprendre comment le noyau applique un quota de CPU, Brider un conteneur l'explique. Les journaux, les métriques et les traces en général sont présentés dans Observabilité : logs, métriques et traces ; ici, on reste sur ce qu'un hôte Docker offre sans outil en plus.

Une stack de trois services, dans un dossier ~/guichet. Le champ name: guichet du fichier fixe le nom du projet, quel que soit le nom du dossier et sans option -p : docker compose ls liste le projet guichet, et les conteneurs s'appellent guichet-.... Le service entree reçoit les requêtes sur le port 8090 de votre machine. Il les passe à annuaire, qui les passe à calcul. Le service calcul simule un travail de CPU : à chaque requête, il calcule l'empreinte de 20 Mo de zéros.

Créez le dossier, puis écrivez dedans le fichier compose.yaml.

Fenêtre de terminal
mkdir -p ~/guichet
cd ~/guichet
name: guichet
services:
entree:
image: nginx:1.30.6-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
ports:
- "127.0.0.1:8090:80"
volumes:
- ./entree.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- annuaire
annuaire:
image: nginx:1.30.6-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
volumes:
- ./annuaire.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- calcul
calcul:
image: alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
command: ["nc", "-lk", "-p", "8080", "-e", "/srv/service.sh"]
cpus: 0.5
volumes:
- ./service.sh:/srv/service.sh:ro

calcul est limité à un demi-CPU avec cpus: 0.5. Les deux autres services n'ont aucune limite : cette différence compte plus bas. Le service calcul écoute avec nc -lk -e, qui lance le script à chaque connexion.

Les deux services nginx écrivent leurs lignes d'accès en JSON sur la sortie standard, avec un identifiant de requête : une chaîne unique créée à l'arrivée de la requête, que chaque service recopie pour qu'on puisse la retrouver partout. Voici entree.conf, qui crée cet identifiant avec la variable $request_id, le renvoie au client dans un en-tête de réponse et le passe à annuaire.

log_format jsonlog escape=json '{"service":"entree","t":"$time_iso8601","id":"$request_id","path":"$uri","status":$status,"duree":$request_time}';
server {
listen 80;
access_log /dev/stdout jsonlog;
add_header X-Request-ID $request_id always;
location / {
proxy_set_header X-Request-ID $request_id;
proxy_pass http://annuaire:80;
}
}

Et annuaire.conf, qui journalise l'identifiant reçu ($http_x_request_id), répond lui-même à /ping, et transmet /calcul au calculateur.

log_format jsonlog escape=json '{"service":"annuaire","t":"$time_iso8601","id":"$http_x_request_id","path":"$uri","status":$status,"duree":$request_time}';
server {
listen 80;
access_log /dev/stdout jsonlog;
location = /ping {
return 200 "ok\n";
}
location /calcul {
proxy_set_header X-Request-ID $http_x_request_id;
proxy_pass http://calcul:8080/;
}
}

Enfin service.sh, le calculateur. Il lit les en-têtes de la requête, retient X-Request-ID, écrit une ligne au début et une à la fin du calcul sur sa sortie d'erreur, puis répond. Rendez-le exécutable avec chmod 755 service.sh, sinon nc ne peut pas le lancer.

#!/bin/sh
id=inconnu
while IFS= read -r l; do
l=${l%?}
[ -z "$l" ] && break
case "$l" in [Xx]-[Rr]equest-[Ii][Dd]:*) id=${l#*: };; esac
done
echo "calcul id=$id debut" >&2
r=$(head -c 20000000 /dev/zero | sha256sum)
echo "calcul id=$id fin" >&2
printf 'HTTP/1.0 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n%s\n' "$r"

Lancez la stack et envoyez deux requêtes : un calcul, puis un simple ping.

Fenêtre de terminal
cd ~/guichet
docker compose up -d
curl -s -i http://127.0.0.1:8090/calcul
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/ping

Regardez l'en-tête X-Request-ID de la première réponse : c'est l'identifiant que vous suivrez dans les journaux. Un calcul seul met entre 0,2 et 0,3 s à répondre, le ping répond 200 aussitôt. La machine de référence de cette leçon a 2 CPU : vos durées dépendent de la vôtre.

Avec docker compose logs, qui rassemble les journaux de tous les services du projet. Chaque ligne commence par le nom du service et son numéro, suivis d'une barre verticale. Sans service nommé, vous obtenez d'abord les messages de démarrage de nginx, puis les lignes d'accès.

Fenêtre de terminal
docker compose logs
entree-1 | /docker-entrypoint.sh: Configuration complete; ready for start up
entree-1 | 2026/11/02 21:06:22 [notice] 1#1: nginx/1.30.6
entree-1 | {"service":"entree","t":"2026-11-02T21:06:25+00:00","id":"ba1b658820b112427826a40b76362fbd","path":"/calcul","status":200,"duree":0.227}
entree-1 | {"service":"entree","t":"2026-11-02T21:06:25+00:00","id":"5b686531951b2d9905c8eae607c2d2fd","path":"/ping","status":200,"duree":0.000}

Cet extrait ne garde que quatre lignes de entree. Regardez le préfixe entree-1 | : il vous dit quel conteneur a écrit la ligne. Regardez aussi les deux lignes JSON, la requête /calcul (0,227 s) et le /ping (0,000 s) : le champ duree est la durée totale vue par ce service. C'est une mesure applicative, écrite par nginx, par opposition aux compteurs du noyau lus plus bas.

Pourquoi l'ordre des lignes change-t-il d'un appel à l'autre ?

Section intitulée « Pourquoi l'ordre des lignes change-t-il d'un appel à l'autre ? »

Parce que, sans l'option -f, docker compose logs affiche les lignes regroupées par service, et l'ordre des groupes n'est pas stable. D'un appel de docker compose logs --tail 4 au suivant, les services défilent dans un ordre qui change, par exemple entree, annuaire, calcul, puis calcul, entree, annuaire. Le résultat n'est donc pas une chronologie : deux lignes de services différents, l'une sous l'autre, ne sont pas dans l'ordre où elles sont arrivées.

Pour rétablir le temps, ajoutez --timestamps. Docker place alors devant chaque ligne l'heure à laquelle il a reçu le message, en UTC et à la nanoseconde.

Fenêtre de terminal
docker compose logs --timestamps --tail 2 calcul

Les lignes de calcul ne portent aucune date dans leur message : c'est cette option qui permet de les situer. Avec les dates devant, vous pouvez remettre vous-même dans l'ordre les lignes de plusieurs services. En mode suivi (-f), les lignes arrivent au contraire dans l'ordre où elles sont écrites, entrelacées entre les services.

Avec quatre options, qui se combinent. Le tableau les résume, puis un exemple les montre à l'œuvre.

OptionCe qu'elle faitÀ savoir
--since 5sNe montre que les lignes des 5 dernières secondesAccepte aussi une date comme 2026-11-02T20:39:22Z ou un horodatage Unix
--tail 2Ne montre que les 2 dernières lignes de chaque service--tail s'applique service par service, pas au total
-fSuit les journaux en continuRejoue d'abord tout l'historique, sauf avec --tail 0
--no-log-prefixRetire `entree-1` devant chaque ligne

--since refuse une date écrite avec un espace entre le jour et l'heure : gardez le T. Vous pouvez aussi nommer les services à lire : docker compose logs --tail 2 calcul entree ne montre que ces deux-là. Pour regarder ce qui arrive pendant quatre secondes, sans l'historique, bornez le suivi avec timeout.

Fenêtre de terminal
timeout 4 docker compose logs -f --tail 0

Pendant ces quatre secondes, envoyez un ping depuis un autre terminal : vous voyez la ligne d'annuaire puis celle d'entree, et rien d'autre. La commande se termine avec le code 124, celui de timeout : c'est normal, c'est lui qui l'a arrêtée.

--no-log-prefix est pratique pour passer les lignes à un outil comme grep, à condition que chaque ligne dise elle-même qui l'a écrite. Ici, les lignes JSON portent le champ service ; celles de calcul ne le portent pas.

Comment reconstituer une requête à travers les services ?

Section intitulée « Comment reconstituer une requête à travers les services ? »

En lui donnant un identifiant que chaque service recopie dans son journal, puis en cherchant cet identifiant partout. C'est le rôle de X-Request-ID dans le décor : entree le crée, le renvoie au client, et le passe à annuaire, qui le passe à calcul.

Le schéma suit une requête de /calcul et marque les moments où chaque service écrit dans son journal. Regardez que entree et annuaire n'écrivent qu'au retour de la réponse, alors que calcul écrit au début et à la fin de son travail.

Schéma en séquence. Le client envoie une requête sur /calcul à entree, qui crée l'identifiant X-Request-ID. Entree transmet la requête et l'identifiant à annuaire, qui les transmet à calcul. Calcul écrit une ligne debut, puis une ligne fin, et renvoie sa réponse à annuaire. Annuaire écrit sa ligne avec la durée totale et renvoie la réponse à entree, qui écrit à son tour sa ligne avec la durée totale, puis renvoie au client la réponse et l'identifiant.calculannuaireentreeClientcrée l'identifiantX-Request-IDécrit debutécrit finécrit sa ligne(durée totale)écrit sa ligne(durée totale)requête sur /calculrequête + identifiantrequête + identifiantréponseréponseréponse +X-Request-ID
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Envoyez une requête, récupérez l'identifiant dans l'en-tête de réponse, puis cherchez-le dans tous les journaux, avec leurs dates.

Fenêtre de terminal
ID=$(curl -s -D - -o /dev/null http://127.0.0.1:8090/calcul | tr -d '\r' | awk -F': ' 'tolower($1)=="x-request-id"{print $2}')
docker compose logs --timestamps | grep "$ID"
entree-1 | 2026-11-02T21:06:25.921365686Z {"service":"entree","t":"2026-11-02T21:06:25+00:00","id":"a7dd1f757575d4d7ed1f5523b234ec4e","path":"/calcul","status":200,"duree":0.279}
calcul-1 | 2026-11-02T21:06:25.643240040Z calcul id=a7dd1f757575d4d7ed1f5523b234ec4e debut
calcul-1 | 2026-11-02T21:06:25.921101617Z calcul id=a7dd1f757575d4d7ed1f5523b234ec4e fin
annuaire-1 | 2026-11-02T21:06:25.921185845Z {"service":"annuaire","t":"2026-11-02T21:06:25+00:00","id":"a7dd1f757575d4d7ed1f5523b234ec4e","path":"/calcul","status":200,"duree":0.279}

Regardez d'abord que le même identifiant apparaît dans les trois services : la requête est reconstituée. Regardez ensuite les dates. entree et annuaire écrivent leur ligne à la fin de la requête, à moins d'une demi-milliseconde l'une de l'autre, et avec la même duree, 0,279 s. Avec un format qui ne note que la durée totale ($request_time), cette durée dit que la requête a été lente, mais pas où : des durées égales situent seulement le temps dans annuaire ou plus loin. nginx sait séparer le temps passé en amont avec la variable $upstream_response_time, que ce format n'écrit pas.

Seules les deux lignes de calcul situent le temps : le calcul a commencé à la seconde 25,64 de la minute 21:06 et fini à la seconde 25,92, donc les 0,28 s sont passées dans calcul. Ces lignes de début et de fin sont, comme duree, des mesures applicatives écrites par les services eux-mêmes, à la différence des compteurs du noyau.

Le tableau résume ce que chaque journal apporte.

JournalÉcrit quandCe qu'il dit du temps
entreeÀ la fin de la requêteLa durée totale vue du client
annuaireÀ la fin de la requêteLa durée vue depuis annuaire, c'est-à-dire le travail de calcul et le trajet jusqu'à lui ; la durée d'entree y ajoute le trajet d'entree vers annuaire
calculAu début et à la fin du travailLa durée du calcul lui-même

Un journal d'accès qui ne note que la durée totale d'un service intermédiaire ne localise donc pas une lenteur : il la répète. Avec ce format, pour savoir où le temps passe, il faut qu'au moins un service journalise le début et la fin de son travail avec l'identifiant. Un identifiant que personne n'a écrit ne rend rien : grep -c répond 0.

Comment éviter qu'un secret finisse dans les journaux ?

Section intitulée « Comment éviter qu'un secret finisse dans les journaux ? »

En ne journalisant jamais les en-têtes ou les corps qui en contiennent, et en le vérifiant. Un journal est une copie : tout ce que vous y écrivez est conservé sur le disque de l'hôte, lu par docker compose logs et repris par tous les outils qui collectent les journaux.

Vérifiez avec une valeur témoin, un faux secret facile à retrouver. Envoyez une requête avec un en-tête Authorization, puis comptez les occurrences dans la sortie de Compose et dans le fichier de journal de chaque conteneur. Le témoin passe en argument de la commande parce que sa valeur est fausse : un vrai secret ne se tape pas dans une commande, il se lit dans un fichier, comme le pose la leçon Injecter les secrets, et connaître leurs limites.

Fenêtre de terminal
T=TEMOIN-bearer-s3cr3t-42
curl -s -o /dev/null -H "Authorization: Bearer $T" http://127.0.0.1:8090/ping
docker compose logs | grep -c "$T"
for c in $(docker compose ps -q); do sudo grep -c "$T" /var/lib/docker/containers/$c/$c-json.log; done
0
0
0
0

Avec le format de journal de la démonstration, qui ne contient ni l'en-tête ni son contenu, la valeur n'apparaît nulle part : un zéro pour la sortie de Compose, puis un zéro pour chacun des trois fichiers. L'en-tête a pourtant bien traversé la stack, car proxy_pass le transmet à annuaire ; personne ne l'a simplement écrit.

Que se passe-t-il si un journal copie l'en-tête ?

Section intitulée « Que se passe-t-il si un journal copie l'en-tête ? »

Le secret est conservé. Pour le voir, lancez une variante qui ajoute "auth":"$http_authorization" au format de entree. Créez entree-fuite.conf, copie de entree.conf dont seule la ligne log_format change.

log_format jsonlog escape=json '{"service":"entree","t":"$time_iso8601","id":"$request_id","path":"$uri","status":$status,"auth":"$http_authorization","duree":$request_time}';

Créez ensuite compose.fuite.yaml, qui monte ce fichier à la place du premier.

services:
entree:
volumes:
- ./entree-fuite.conf:/etc/nginx/conf.d/default.conf:ro
Fenêtre de terminal
docker compose -f compose.yaml -f compose.fuite.yaml up -d
curl -s -o /dev/null -H "Authorization: Bearer $T" http://127.0.0.1:8090/ping
docker compose logs entree | grep "$T"
c=$(docker compose ps -q entree)
sudo grep "$T" /var/lib/docker/containers/$c/$c-json.log
entree-1 | {"service":"entree","t":"2026-11-02T21:06:28+00:00","id":"c276b07a1b26330b2c15f1825b080c46","path":"/ping","status":200,"auth":"Bearer TEMOIN-bearer-s3cr3t-42","duree":0.000}
{"log":"{\"service\":\"entree\",\"t\":\"2026-11-02T21:06:28+00:00\",\"id\":\"c276b07a1b26330b2c15f1825b080c46\",\"path\":\"/ping\",\"status\":200,\"auth\":\"Bearer TEMOIN-bearer-s3cr3t-42\",\"duree\":0.000}\n","stream":"stdout","time":"2026-11-02T21:06:28.92724702Z"}

Regardez le champ "auth" : le secret est en clair dans la ligne de docker compose logs, et la même ligne est copiée telle quelle dans le fichier ...-json.log, avec en plus l'enveloppe JSON de Docker.

Le fichier appartient à root et n'est lisible que par lui (-rw-r-----), mais la sortie de docker compose logs est lisible par tout compte du groupe docker, c'est-à-dire par tout compte autorisé à parler au démon. Un compte sans cet accès n'obtient rien : docker compose logs échoue avec permission denied while trying to connect to the docker API, et la lecture directe du fichier est refusée. Tout collecteur qui lit ces journaux voit aussi le secret, et, pour le fichier, la fuite dure autant que le conteneur : il part avec lui. Les copies prises par les collecteurs, elles, lui survivent.

La réparation retire le champ du format, puis recrée le conteneur. Dans la démonstration, docker compose up -d suffit parce que l'override compose.fuite.yaml disparaît de la liste des fichiers : la configuration du conteneur change, et Compose le recrée. Si vous corrigez seulement le fichier .conf monté, la configuration du conteneur ne change pas et docker compose up -d ne le recrée pas ; il faut alors docker compose up -d --force-recreate entree, puis recompter le témoin dans les journaux pour prouver la réparation. Considérez ensuite le secret comme compromis et renouvelez-le : l'effacer du fichier ne dit pas qui l'a déjà lu.

Docker enregistre les journaux d'un conteneur avec un pilote de journalisation, qui décide où et sous quelle forme ils sont écrits. Pour retrouver ce pilote et le fichier d'un conteneur, lisez-les dans docker inspect.

Fenêtre de terminal
docker inspect -f '{{.LogPath}} {{.HostConfig.LogConfig.Type}}' $(docker compose ps -q entree)
/var/lib/docker/containers/<identifiant>/<identifiant>-json.log json-file

La sortie donne le chemin du fichier suivi du pilote, json-file, le pilote par défaut : un fichier JSON par conteneur, dans /var/lib/docker/containers/. Tant qu'aucun fichier /etc/docker/daemon.json ne change ce réglage, tous vos conteneurs l'utilisent.

En comparant ce que chaque service consomme et ce qu'on le laisse consommer. Un service est saturé en CPU quand il atteint son plafond de CPU et que les requêtes attendent derrière lui. Le journal ne le dit pas : il faut lire les compteurs du noyau.

Deux fichiers du cgroup du conteneur suffisent : cpu.max contient le quota, cpu.stat contient les compteurs.

Avec la période par défaut, 100 ms, un service qui a un quota ne peut utiliser qu'une part de chaque période ; s'il l'épuise, le noyau suspend ses processus jusqu'à la période suivante. On dit alors que la période est bridée. Les champs utiles de cpu.stat sont les suivants.

  • usage_usec : le temps de CPU consommé, en microsecondes ;
  • nr_periods : le nombre de périodes actives sous quota, c'est-à-dire où le service avait du travail ;
  • nr_throttled : le nombre de périodes où le service a été bridé ;
  • throttled_usec : le temps total passé à attendre.

Ces compteurs ne font que croître : lisez-les avant et après une charge, puis comparez les différences.

Comment relever les compteurs avant et après une charge ?

Section intitulée « Comment relever les compteurs avant et après une charge ? »

Avec une petite boucle qui lit les deux fichiers dans chaque service. L'option -T de exec évite d'allouer un pseudo-terminal : c'est la forme à prendre dans un script, où la sortie part dans une variable et non à l'écran.

Fenêtre de terminal
for s in entree annuaire calcul; do
echo "[$s] cpu.max: $(docker compose exec -T "$s" cat /sys/fs/cgroup/cpu.max)"
docker compose exec -T "$s" cat /sys/fs/cgroup/cpu.stat | grep -E '^(usage_usec|nr_periods|nr_throttled|throttled_usec) ' | tr '\n' ' '; echo
done
[entree] cpu.max: max 100000
usage_usec 70104 nr_periods 0 nr_throttled 0 throttled_usec 0
[annuaire] cpu.max: max 100000
usage_usec 75324 nr_periods 0 nr_throttled 0 throttled_usec 0
[calcul] cpu.max: 50000 100000
usage_usec 473451 nr_periods 15 nr_throttled 8 throttled_usec 647419

Regardez cpu.max. La valeur 50000 100000 de calcul veut dire « 50 ms de CPU par période de 100 ms », soit le demi-CPU demandé par cpus: 0.5. La valeur max 100000 des deux autres veut dire « pas de limite ».

Regardez aussi nr_periods : il reste à 0 pour entree et annuaire, parce que le noyau ne compte les périodes que s'il y a un quota. Sans quota, nr_throttled vaut 0 par construction, et ce 0 ne prouve pas que le service n'est pas chargé : il prouve seulement qu'il n'a pas de plafond.

Notez ces valeurs, puis lancez une charge bornée : 12 calculs, trois à la fois, en même temps que 300 ping. Le client enregistre le code et la durée de chaque calcul.

Fenêtre de terminal
( seq 12 | xargs -P3 -I{} curl -s -o /dev/null -w '%{http_code} %{time_total}\n' http://127.0.0.1:8090/calcul > /tmp/calcul.out ) &
( seq 300 | xargs -P4 -I{} curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/ping > /tmp/ping.out ) &
wait
sort /tmp/calcul.out | awk '{n++; s+=$2; if($2>m)m=$2} END{print n" req, moyenne "s/n" s, max "m" s"}'
12 req, moyenne 0.793539 s, max 1.017968 s

Les 300 ping ont tous répondu 200. Regardez la durée : un calcul seul prenait environ 0,25 s, il en prend maintenant 0,8 s en moyenne. Quelque chose attend : reste à savoir qui. Relevez de nouveau les compteurs avec la même boucle, puis soustrayez les valeurs d'avant.

ServiceHausse de usage_usecHausse de nr_periodsHausse de nr_throttledHausse de throttled_usec
entree99 177000
annuaire65 090000
calcul1 718 10137333 508 743

Regardez la ligne de calcul. Il a consommé 1,72 s de CPU là où les deux autres en ont consommé moins de 0,1 s. Surtout, 33 de ses 37 périodes ont été bridées (environ 89 %). Le noyau lui a compté 3,5 s de bridage (throttled_usec) pour une charge qui a duré 3,3 s : ce total peut dépasser la durée réelle, parce que le noyau additionne les durées de bridage de chaque file d'exécution du groupe, une par processeur, et que la machine de mesure en a deux. Les trois services ont eu leurs compteurs relevés dans les mêmes conditions : seul calcul a un plafond, et il l'atteint. C'est lui, le composant saturé.

Parce qu'il donne un instantané, sans dire si le pourcentage affiché est un plafond. Lancé en pleine charge (ici environ une seconde après son début), il montre ceci.

Fenêtre de terminal
docker stats --no-stream $(docker compose ps -q)
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
070354b9f32e guichet-annuaire-1 0.11% 3.43MiB / 2.84GiB 0.12% 176kB / 145kB 0B / 4.1kB 3
fce8a928de17 guichet-calcul-1 50.58% 1.762MiB / 2.84GiB 0.06% 10.3kB / 7.42kB 0B / 0B 9
a486c3347bbf guichet-entree-1 1.46% 3.531MiB / 2.84GiB 0.12% 289kB / 315kB 0B / 4.1kB 3

Regardez la colonne CPU % : 100 % représente un cœur entier, donc calcul à 50,58 % est à son demi-CPU, alors que les deux autres restent sous 2 %. Mais docker stats ne vous apprend pas que 50 % est un plafond : sans cpu.max et nr_throttled, ce pourcentage pourrait être celui d'un service tranquille.

Deux autres colonnes se lisent avec prudence. La limite de mémoire 2.84GiB est la mémoire de la machine, parce qu'aucune limite de mémoire n'a été posée sur le service. Les colonnes NET I/O et BLOCK I/O sont cumulées depuis le démarrage. Un relevé fait une fois la charge terminée retombe vers 0 % : l'instantané ne garde rien.

docker compose stats --no-stream existe aussi avec Compose 5.6.1 : sans argument, il rend les mêmes colonnes pour les seuls conteneurs du projet, dans un ordre qui peut différer.

Que disent docker compose ps et docker compose top ?

Section intitulée « Que disent docker compose ps et docker compose top ? »

docker compose ps ne dit rien de la charge. Pendant la charge, il affiche pour chaque service son image, sa commande, son âge et son état, et les trois sont Up. Il confirme que tout est vivant, pas que quelque chose est lent.

docker compose top va plus loin : il liste les processus de chaque service. Lancé pendant la charge, il montre dans calcul trois copies actives de service.sh (une par calcul en cours), chacune avec ses processus head et sha256sum, et dans entree et annuaire seulement les processus nginx. Lancé sans charge, il ne montre dans calcul que le processus nc.

Fenêtre de terminal
docker compose top

Regardez les lignes de calcul : elles montrent qui consomme dans le service, ici le calcul d'empreinte, ce que confirme la colonne PIDS de docker stats (9 processus dans l'extrait ci-dessus, contre 1 hors charge ; ce nombre varie d'un relevé à l'autre). La colonne C de top est le pourcentage de CPU instantané de chaque processus, sans aucune notion de plafond, et la colonne TIME reste à 00:00:00 pour des processus aussi courts. top aide donc à trouver le processus fautif dans un service déjà désigné, pas à désigner le service.

Pourquoi le service qui écrit le plus de journaux n'est-il pas le saturé ?

Section intitulée « Pourquoi le service qui écrit le plus de journaux n'est-il pas le saturé ? »

Parce qu'un service bavard est un service qui reçoit beaucoup de requêtes, pas un service qui peine à les traiter. Pendant la même charge, les lignes écrites et la croissance du fichier de journal de chaque service étaient les suivantes.

ServiceLignes écritesCroissance du fichier json-file
entree312de 2 968 à 73 154 octets (environ 70 Ko de plus)
annuaire312de 2 975 à 73 788 octets (environ 71 Ko de plus)
calcul24de 708 à 3 537 octets (environ 2,8 Ko de plus)

Les lignes viennent de docker compose logs --no-log-prefix --since "$debut" <service> | wc -l, avec debut=$(date -u +%Y-%m-%dT%H:%M:%SZ) pris juste avant la charge. Les tailles viennent de sudo stat -c %s sur le fichier désigné par LogPath. Regardez le rapport : entree et annuaire ont écrit treize fois plus de lignes que calcul, et environ vingt-cinq fois plus d'octets, alors que ce sont eux qui ne sont pas saturés. Le composant saturé est ici le plus discret. Attribuer une saturation au plus gros journal désigne le mauvais service ; seuls les compteurs de CPU départagent.

Pourquoi un journal json-file peut-il remplir le disque ?

Section intitulée « Pourquoi un journal json-file peut-il remplir le disque ? »

Parce que le pilote json-file n'a, par défaut, aucune borne de taille : le fichier d'un conteneur grandit tant que le conteneur existe. Pour le mesurer, envoyez 3 000 requêtes ping sur entree et regardez le fichier et le nombre de lignes lisibles.

Fenêtre de terminal
seq 3000 | xargs -P8 -I{} curl -s -o /dev/null http://127.0.0.1:8090/ping
sudo stat -c %s "$(docker inspect -f '{{.LogPath}}' "$(docker compose ps -q entree)")"
docker compose logs entree | wc -l

Le fichier est passé de 73 154 à 747 805 octets, et docker compose logs entree rend 3 331 lignes : aucune n'est perdue, mais le fichier n'a pas de borne.

Ces 3 000 requêtes ont ajouté environ 675 Ko. Un service qui reçoit des requêtes en continu écrit ainsi sans fin dans /var/lib/docker/containers/, jusqu'à remplir le disque de l'hôte. Un disque plein peut gêner les autres conteneurs, qui écrivent sur le même disque, pas seulement celui qui journalise. Borner ces fichiers est le sujet d'une autre leçon de la formation.

Que faire quand la lecture des journaux ne donne rien d'utile ?

Section intitulée « Que faire quand la lecture des journaux ne donne rien d'utile ? »

Le tableau part de ce que vous observez, donne la cause probable, puis ce que vous faites.

Ce que vous observezCause probableCe que vous faites
annuaire sort aussitôt avec [emerg] host not found in upstream "calcul", puis entree répond 502nginx résout le nom de son service amont au démarrage ; calcul n'existait pas encore ou était tombéLisez docker compose logs annuaire, relancez calcul d'abord, puis annuaire
docker compose logs mélange les services dans un ordre qui changeSans -f, les lignes sont groupées par serviceAjoutez --timestamps et lisez les dates
Les lignes de calcul n'ont pas de dateLe message n'en contient pasAjoutez --timestamps
Après --no-log-prefix, impossible de savoir qui a écritLe préfixe est retiréMettez le nom du service dans le message, comme le champ service du JSON
La durée est la même dans entree et annuaireLes deux écrivent à la fin de la requêteLisez le début et la fin dans le journal du service qui travaille
nr_throttled vaut 0 sur un service chargéCe service n'a pas de quota, nr_periods reste à 0Lisez cpu.max d'abord ; sans quota, usage_usec montre ce que le service consomme, pas ce qu'il attend
docker stats affiche une limite de mémoire énormeC'est la mémoire de la machine, aucune limite n'est poséePosez mem_limit si vous voulez un plafond
Un script s'arrête après un docker compose execexec lit l'entrée standard du script, avec ou sans -TAjoutez < /dev/null à la commande exec
Une modification de service.sh n'a aucun effetUn éditeur qui remplace le fichier change son identité, le conteneur garde l'ancien contenu montéRecréez le conteneur avec docker compose up -d --force-recreate calcul

La démonstration laisse trois conteneurs, un réseau, deux images et le dossier ~/guichet. La commande docker compose down supprime les conteneurs et le réseau, et -v les volumes. Les fichiers de journaux partent avec les conteneurs.

Fenêtre de terminal
cd ~/guichet
docker compose down -v --remove-orphans
docker rmi nginx:1.30.6-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949
cd ~ && rm -r guichet
rm -f /tmp/calcul.out /tmp/ping.out

Après cela, docker ps -a ne montre plus de conteneur guichet-.... Gardez l'image Alpine si vous vous en servez ailleurs, sinon retirez-la de la même façon.

Le lab vous donne une API de devis qui associe une passerelle et un calculateur, avec une charge de calcul à supporter. Vous configurez la stack avec Compose, sans écrire de code applicatif : vous conservez l'identifiant de chaque requête dans les journaux des deux composants, vous posez des quotas de CPU durs, vous mesurez une charge bornée et vous désignez le composant saturé à partir des compteurs du cgroup, pas à partir du plus gros journal. Vous empêchez aussi les journaux de contenir l'en-tête d'autorisation ou un témoin secret.

La validation lit l'état de la stack plutôt que vos commandes : identifiants frais présents de bout en bout dans les traces et la réponse, quotas effectifs lus dans les cgroups, compteurs de bridage qui augmentent sur le bon composant, absence du secret dans les journaux, et refus d'une variante dont les journaux restent trop bavards. Elle exige aussi que la stack soit recréée depuis votre déclaration Compose.

  1. docker compose logs regroupe les lignes par service, dans un ordre qui change : sans --timestamps, ce n'est pas une chronologie. --since filtre par durée ou par date, --tail s'applique à chaque service, --no-log-prefix retire le nom du service, donc la ligne doit dire elle-même qui l'a écrite.
  2. Un identifiant de requête recopié dans chaque service permet de la suivre avec un simple grep.
  3. Un journal d'accès qui ne note que la durée totale ($request_time) répète cette durée d'un service à l'autre. $upstream_response_time sépare le temps passé en amont, et le journal du service qui travaille, avec début et fin, situe le temps.
  4. Ne journalisez jamais un en-tête d'autorisation : le secret est copié dans la sortie de Compose et dans le fichier json-file du conteneur. S'il a fuité, renouvelez-le, et recréez le conteneur pour que la correction s'applique.
  5. Une saturation de CPU se prouve avec cpu.max (le plafond), puis la hausse de nr_periods, nr_throttled et throttled_usec dans cpu.stat avant et après une charge. Un service sans quota a toujours nr_throttled à 0.
  6. docker stats est un instantané, docker compose ps ne dit rien de la charge, docker compose top montre quels processus consomment dans un service déjà désigné.
  7. Le service qui écrit le plus de journaux n'est pas forcément celui qui est saturé.
  8. Sans borne, le journal json-file d'un conteneur grossit tant que le conteneur existe, et peut remplir le disque de l'hôte.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

8 questions
8 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn