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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Lire les journaux d'une stack, les ordonner avec
--timestampset les filtrer avec--since,--tailet--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.statetdocker stats. - Mesurer la croissance d'un journal json-file sans borne et comprendre le risque pour le disque.
Prérequis
Section intitulée « Prérequis »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.
Quelle stack sert de démonstration ?
Section intitulée « Quelle stack sert de démonstration ? »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.
mkdir -p ~/guichetcd ~/guichetname: guichetservices: 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:rocalcul 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/shid=inconnuwhile IFS= read -r l; do l=${l%?} [ -z "$l" ] && break case "$l" in [Xx]-[Rr]equest-[Ii][Dd]:*) id=${l#*: };; esacdoneecho "calcul id=$id debut" >&2r=$(head -c 20000000 /dev/zero | sha256sum)echo "calcul id=$id fin" >&2printf '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.
cd ~/guichetdocker compose up -dcurl -s -i http://127.0.0.1:8090/calculcurl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/pingRegardez 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.
Comment lire les journaux de toute la stack ?
Section intitulée « Comment lire les journaux de toute la stack ? »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.
docker compose logsentree-1 | /docker-entrypoint.sh: Configuration complete; ready for start upentree-1 | 2026/11/02 21:06:22 [notice] 1#1: nginx/1.30.6entree-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.
docker compose logs --timestamps --tail 2 calculLes 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.
Comment ne garder que les lignes utiles ?
Section intitulée « Comment ne garder que les lignes utiles ? »Avec quatre options, qui se combinent. Le tableau les résume, puis un exemple les montre à l'œuvre.
| Option | Ce qu'elle fait | À savoir |
|---|---|---|
--since 5s | Ne montre que les lignes des 5 dernières secondes | Accepte aussi une date comme 2026-11-02T20:39:22Z ou un horodatage Unix |
--tail 2 | Ne montre que les 2 dernières lignes de chaque service | --tail s'applique service par service, pas au total |
-f | Suit les journaux en continu | Rejoue d'abord tout l'historique, sauf avec --tail 0 |
--no-log-prefix | Retire `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.
timeout 4 docker compose logs -f --tail 0Pendant 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.
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.
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 debutcalcul-1 | 2026-11-02T21:06:25.921101617Z calcul id=a7dd1f757575d4d7ed1f5523b234ec4e finannuaire-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 quand | Ce qu'il dit du temps |
|---|---|---|
entree | À la fin de la requête | La durée totale vue du client |
annuaire | À la fin de la requête | La 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 |
calcul | Au début et à la fin du travail | La 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.
T=TEMOIN-bearer-s3cr3t-42curl -s -o /dev/null -H "Authorization: Bearer $T" http://127.0.0.1:8090/pingdocker 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; done0000Avec 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:rodocker compose -f compose.yaml -f compose.fuite.yaml up -dcurl -s -o /dev/null -H "Authorization: Bearer $T" http://127.0.0.1:8090/pingdocker compose logs entree | grep "$T"c=$(docker compose ps -q entree)sudo grep "$T" /var/lib/docker/containers/$c/$c-json.logentree-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.
docker inspect -f '{{.LogPath}} {{.HostConfig.LogConfig.Type}}' $(docker compose ps -q entree)/var/lib/docker/containers/<identifiant>/<identifiant>-json.log json-fileLa 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.
Comment savoir quel service est saturé ?
Section intitulée « Comment savoir quel service est saturé ? »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.
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' ' '; echodone[entree] cpu.max: max 100000usage_usec 70104 nr_periods 0 nr_throttled 0 throttled_usec 0[annuaire] cpu.max: max 100000usage_usec 75324 nr_periods 0 nr_throttled 0 throttled_usec 0[calcul] cpu.max: 50000 100000usage_usec 473451 nr_periods 15 nr_throttled 8 throttled_usec 647419Regardez 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.
( 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 ) &waitsort /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 sLes 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.
| Service | Hausse de usage_usec | Hausse de nr_periods | Hausse de nr_throttled | Hausse de throttled_usec |
|---|---|---|---|---|
entree | 99 177 | 0 | 0 | 0 |
annuaire | 65 090 | 0 | 0 | 0 |
calcul | 1 718 101 | 37 | 33 | 3 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é.
Pourquoi docker stats ne suffit-il pas ?
Section intitulée « Pourquoi docker stats ne suffit-il pas ? »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.
docker stats --no-stream $(docker compose ps -q)CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS070354b9f32e guichet-annuaire-1 0.11% 3.43MiB / 2.84GiB 0.12% 176kB / 145kB 0B / 4.1kB 3fce8a928de17 guichet-calcul-1 50.58% 1.762MiB / 2.84GiB 0.06% 10.3kB / 7.42kB 0B / 0B 9a486c3347bbf guichet-entree-1 1.46% 3.531MiB / 2.84GiB 0.12% 289kB / 315kB 0B / 4.1kB 3Regardez 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.
docker compose topRegardez 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.
| Service | Lignes écrites | Croissance du fichier json-file |
|---|---|---|
entree | 312 | de 2 968 à 73 154 octets (environ 70 Ko de plus) |
annuaire | 312 | de 2 975 à 73 788 octets (environ 71 Ko de plus) |
calcul | 24 | de 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.
seq 3000 | xargs -P8 -I{} curl -s -o /dev/null http://127.0.0.1:8090/pingsudo stat -c %s "$(docker inspect -f '{{.LogPath}}' "$(docker compose ps -q entree)")"docker compose logs entree | wc -lLe 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 observez | Cause probable | Ce que vous faites |
|---|---|---|
annuaire sort aussitôt avec [emerg] host not found in upstream "calcul", puis entree répond 502 | nginx 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 change | Sans -f, les lignes sont groupées par service | Ajoutez --timestamps et lisez les dates |
Les lignes de calcul n'ont pas de date | Le message n'en contient pas | Ajoutez --timestamps |
Après --no-log-prefix, impossible de savoir qui a écrit | Le 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 annuaire | Les deux écrivent à la fin de la requête | Lisez 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 à 0 | Lisez 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 énorme | C'est la mémoire de la machine, aucune limite n'est posée | Posez mem_limit si vous voulez un plafond |
Un script s'arrête après un docker compose exec | exec lit l'entrée standard du script, avec ou sans -T | Ajoutez < /dev/null à la commande exec |
Une modification de service.sh n'a aucun effet | Un é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 |
Comment tout nettoyer après la démonstration ?
Section intitulée « Comment tout nettoyer après la démonstration ? »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.
cd ~/guichetdocker compose down -v --remove-orphansdocker rmi nginx:1.30.6-alpine@sha256:0e52bd054126cdc040b1276c0af347aeb02a41b9cfe305d6094767ac30e97949cd ~ && rm -r guichetrm -f /tmp/calcul.out /tmp/ping.outAprè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.
Mettre en pratique
Section intitulée « Mettre en pratique »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.
À retenir
Section intitulée « À retenir »docker compose logsregroupe les lignes par service, dans un ordre qui change : sans--timestamps, ce n'est pas une chronologie.--sincefiltre par durée ou par date,--tails'applique à chaque service,--no-log-prefixretire le nom du service, donc la ligne doit dire elle-même qui l'a écrite.- Un identifiant de requête recopié dans chaque service permet de la suivre avec un simple
grep. - 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_timesépare le temps passé en amont, et le journal du service qui travaille, avec début et fin, situe le temps. - 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.
- Une saturation de CPU se prouve avec
cpu.max(le plafond), puis la hausse denr_periods,nr_throttledetthrottled_usecdanscpu.statavant et après une charge. Un service sans quota a toujoursnr_throttledà 0. docker statsest un instantané,docker compose psne dit rien de la charge,docker compose topmontre quels processus consomment dans un service déjà désigné.- Le service qui écrit le plus de journaux n'est pas forcément celui qui est saturé.
- Sans borne, le journal json-file d'un conteneur grossit tant que le conteneur existe, et peut remplir le disque de l'hôte.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »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
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Sauvegarder et restaurer une application
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications