feat(k3s) — ramasse-miettes d'images du kubelet à 65 % → 55 % : il nettoyait APRÈS la ligne de Longhorn (75 %) #64

Open
arcodange wants to merge 1 commits from arcodange/kubelet-image-gc into main
Owner

Pourquoi

Le kubelet ne supprime les images inutilisées qu'à 85 % d'occupation du disque des images, et il s'arrête à 80 %. Ce disque est le disque externe (/mnt/arcodange), que Longhorn partage et qu'il retire de l'ordonnancement dès 75 % (storage-minimal-available-percentage = 25, relevé). Le nettoyage laissait donc le disque en permanence au-dessus de la ligne de Longhorn.

Relevé le 2026-09-25 vers 18 h 30 UTC (/stats/summary et /configz de chaque kubelet, nodes.longhorn.io) :

Nœud Rôle k3s Disque des images (491 Go) Disque Longhorn Seuils du kubelet
pi1 server 75 % Schedulable 85 / 80
pi2 agent 53 % Schedulable 85 / 80
pi3 agent 82 % Schedulable=False 85 / 80

Signalé sur pi3 : environ 270 Go d'images Docker, dont 272 anciennes versions du front Kadans (~450 Mo chacune).

Le réglage

--kubelet-arg=image-gc-high-threshold=65 et --kubelet-arg=image-gc-low-threshold=55, ajoutés à extra_server_args (pi1) et à extra_agent_args (pi2, pi3) dans playbooks/system/system_k3s.yml. Le rôle serveur/agent se décide à l'exécution : le premier hôte trié devient le serveur. kubectl get nodes le confirme : pi1 est control-plane.

La forme reste sans guillemets, comme les deux --kubelet-arg déjà en place (le piège du \= du 11/08 est décrit juste au-dessus dans le fichier). 65 % laisse 10 points de marge sous la ligne de Longhorn.

Le ramasse-miettes du kubelet agit-il avec Docker ? Oui, et il agit déjà

  • --docker fait passer le kubelet par le cri-dockerd embarqué dans k3s : containerRuntimeEndpoint: unix:///run/k3s/cri-dockerd/cri-dockerd.sock (dans /configz, sur les 3 nœuds).
  • cri-dockerd rend au kubelet le système de fichiers des images : imageFs.capacityBytes = 491,1 Go, soit le disque externe, sur les 3 nœuds. C'est sur ce pourcentage que le ramasse-miettes décide.
  • Il exécute aussi les suppressions : kubelet_image_garbage_collected_total{reason="space"} 371 sur pi3, depuis le démarrage de son kubelet (2026-04-13). Il a donc déjà franchi 85 %, nettoyé jusqu'à 80 %, et le disque est remonté depuis. Le seuil était le problème, pas le mécanisme.
  • Aucun minuteur docker image prune n'est donc nécessaire. On garderait cette solution de repli si le mécanisme cessait d'agir. ⚠ Un until= y porte sur la date de création de l'image, pas sur celle de son téléchargement.
  • L'ADR docs/adr/20260407-docker-storage-gitea-runner.md attribuait la disparition des images du runner au « garbage collection automatique de Docker ». Docker n'en a pas pour les images : c'était ce ramasse-miettes du kubelet, quand le data-root était encore sur la carte SD (à 89 %).

Ce qu'il ne supprime pas :

  • une image référencée par un conteneur, même arrêté. cri-dockerd appelle RemoveImage sans force et Docker refuse. L'épingle pin-ci-node-playwright (rôle ci_base_image), le runner et les conteneurs compose de Gitea et Postgres sont donc protégés ;
  • le cache de build, les couches des conteneurs, les volumes, les réplicas Longhorn.

Sans le disque externe

Le réglage ne nomme aucun chemin. Si un Pi démarre sans disque, le data-root retombe sur la carte SD (voir #63), le kubelet y lit alors le disque des images, et le même seuil protège la SD. La contrainte du 2026-09-25 est respectée : ce changement n'ajoute aucune dépendance au disque.

Vérifications locales (aucun hôte touché)

  • ansible-playbook --syntax-check : playbooks/system/system_k3s.yml → code 0 ; playbooks/01_system.yml → code 0.
  • Rendu des variables (YAML chargé) :
    • serveur : --docker --disable traefik --kubelet-arg=container-log-max-files=5 --kubelet-arg=container-log-max-size=10Mi --kubelet-arg=image-gc-high-threshold=65 --kubelet-arg=image-gc-low-threshold=55
    • agent : même suite sans --disable traefik, suivie de {{ kubelet_reserved_args | default('') }}
  • yamllint -d relaxed et ansible-lint --offline : mêmes constats que sur main, aucun nouveau (1 erreur trailing-spaces et 3 avertissements côté yamllint, 15 constats côté ansible-lint, tous hors des lignes modifiées). Le dépôt n'a de configuration ni pour l'un ni pour l'autre, et aucune CI ne couvre ansible/.

À l'application : ce qui va se passer

ansible-playbook -i ansible/arcodange/factory/inventory ansible/arcodange/factory/playbooks/system/system_k3s.yml
  • k3s redémarre sur chaque nœud : l'unité est régénérée. Sur pi1, l'API k3s est donc brièvement indisponible.
  • Premier passage (toutes les 5 min après le redémarrage) : pi3 et pi1 sont au-dessus de 65 %, et le kubelet y supprime des images jusqu'à 55 %. Sur pi3, ça fait jusqu'à ~130 Go d'images supprimées d'un coup sur un disque USB. Mieux vaut l'appliquer dans un moment calme. Aujourd'hui à 18 h 20 UTC, le Docker de pi3 expirait déjà ses créations de conteneurs (operation timeout: context deadline exceeded), cause non établie.
  • ⚠ Si ce qui n'est pas une image inutilisée dépasse déjà 55 % du disque, le kubelet supprime toutes les images inutilisées à chaque passage, puis journalise FreeDiskSpaceFailed. Les images sont retéléchargées au besoin, sans perte. Sur pi3, cri-dockerd annonce 120 Go d'images pour 400 Go occupés, dont ~83 Go de réplicas Longhorn. Le reste peut être du cache de build (les builds CI tournent sur pi3), que le kubelet ne touche pas. docker system df sur pi3 avant d'appliquer le dira.

Vérifier après application

# les seuils sont pris (attendu : 65 et 55 sur les 3 nœuds)
for n in pi1 pi2 pi3; do kubectl --context default get --raw /api/v1/nodes/$n/proxy/configz \
  | jq -c '.kubeletconfig | {n: "'$n'", hi: .imageGCHighThresholdPercent, lo: .imageGCLowThresholdPercent}'; done

# occupation du disque des images (attendu : sous 65 % après un ou deux passages)
kubectl --context default get --raw /api/v1/nodes/pi3/proxy/stats/summary \
  | jq '.node.runtime.imageFs | 100 - (.availableBytes * 100 / .capacityBytes | floor)'

# le compteur de suppressions monte
kubectl --context default get --raw /api/v1/nodes/pi3/proxy/metrics | grep kubelet_image_garbage_collected_total

# Longhorn reprend pi3 (attendu : Schedulable=True)
kubectl --context default -n longhorn-system get nodes.longhorn.io pi3 \
  -o jsonpath='{range .status.diskStatus.*.conditions[*]}{.type}={.status} {end}{"\n"}'

# le ramasse-miettes ne tourne pas à vide (attendu : aucun résultat)
kubectl --context default get events -A --field-selector reason=FreeDiskSpaceFailed

Documentation

vibe/guidebooks/factory-provisioning/ansible/01-system.md : la ligne 7 et son récit mentionnent désormais les arguments du kubelet et le seuil. Last Updated passe au 2026-09-25.

Liens

  • Audit « démarrer sans disque » demandé avec ce réglage : #63 (issue séparée, trop long pour cette description)
  • Même famille : #60 (rétention du registre de paquets, la SD de pi2 pleine d'images kadans)

🤖 Generated with Claude Code

## Pourquoi Le kubelet ne supprime les images inutilisées qu'à **85 %** d'occupation du disque des images, et il s'arrête à **80 %**. Ce disque est le disque externe (`/mnt/arcodange`), que Longhorn partage et qu'il retire de l'ordonnancement dès **75 %** (`storage-minimal-available-percentage` = 25, relevé). Le nettoyage laissait donc le disque **en permanence au-dessus** de la ligne de Longhorn. Relevé le 2026-09-25 vers 18 h 30 UTC (`/stats/summary` et `/configz` de chaque kubelet, `nodes.longhorn.io`) : | Nœud | Rôle k3s | Disque des images (491 Go) | Disque Longhorn | Seuils du kubelet | |---|---|---|---|---| | pi1 | server | **75 %** | Schedulable | 85 / 80 | | pi2 | agent | 53 % | Schedulable | 85 / 80 | | pi3 | agent | **82 %** | **Schedulable=False** | 85 / 80 | Signalé sur pi3 : environ 270 Go d'images Docker, dont 272 anciennes versions du front Kadans (~450 Mo chacune). ## Le réglage `--kubelet-arg=image-gc-high-threshold=65` et `--kubelet-arg=image-gc-low-threshold=55`, ajoutés à `extra_server_args` (pi1) **et** à `extra_agent_args` (pi2, pi3) dans `playbooks/system/system_k3s.yml`. Le rôle serveur/agent se décide à l'exécution : le premier hôte trié devient le serveur. `kubectl get nodes` le confirme : pi1 est `control-plane`. La forme reste **sans guillemets**, comme les deux `--kubelet-arg` déjà en place (le piège du `\=` du 11/08 est décrit juste au-dessus dans le fichier). 65 % laisse 10 points de marge sous la ligne de Longhorn. ## Le ramasse-miettes du kubelet agit-il avec Docker ? Oui, et il agit déjà - `--docker` fait passer le kubelet par le cri-dockerd embarqué dans k3s : `containerRuntimeEndpoint: unix:///run/k3s/cri-dockerd/cri-dockerd.sock` (dans `/configz`, sur les 3 nœuds). - cri-dockerd rend au kubelet le système de fichiers des images : `imageFs.capacityBytes` = 491,1 Go, soit le disque externe, sur les 3 nœuds. C'est sur ce pourcentage que le ramasse-miettes décide. - Il exécute aussi les suppressions : **`kubelet_image_garbage_collected_total{reason="space"} 371` sur pi3**, depuis le démarrage de son kubelet (2026-04-13). Il a donc déjà franchi 85 %, nettoyé jusqu'à 80 %, et le disque est remonté depuis. Le seuil était le problème, pas le mécanisme. - Aucun minuteur `docker image prune` n'est donc nécessaire. On garderait cette solution de repli si le mécanisme cessait d'agir. ⚠ Un `until=` y porte sur la date de **création** de l'image, pas sur celle de son téléchargement. - L'ADR `docs/adr/20260407-docker-storage-gitea-runner.md` attribuait la disparition des images du runner au « garbage collection automatique de Docker ». Docker n'en a pas pour les images : c'était ce ramasse-miettes du kubelet, quand le `data-root` était encore sur la carte SD (à 89 %). Ce qu'il **ne** supprime **pas** : - une image référencée par un conteneur, même arrêté. cri-dockerd appelle `RemoveImage` sans `force` et Docker refuse. L'épingle `pin-ci-node-playwright` (rôle `ci_base_image`), le runner et les conteneurs compose de Gitea et Postgres sont donc protégés ; - le cache de build, les couches des conteneurs, les volumes, les réplicas Longhorn. ## Sans le disque externe Le réglage ne nomme aucun chemin. Si un Pi démarre sans disque, le `data-root` retombe sur la carte SD (voir #63), le kubelet y lit alors le disque des images, et le même seuil protège la SD. La contrainte du 2026-09-25 est respectée : ce changement n'ajoute aucune dépendance au disque. ## Vérifications locales (aucun hôte touché) - `ansible-playbook --syntax-check` : `playbooks/system/system_k3s.yml` → code 0 ; `playbooks/01_system.yml` → code 0. - Rendu des variables (YAML chargé) : - serveur : `--docker --disable traefik --kubelet-arg=container-log-max-files=5 --kubelet-arg=container-log-max-size=10Mi --kubelet-arg=image-gc-high-threshold=65 --kubelet-arg=image-gc-low-threshold=55` - agent : même suite sans `--disable traefik`, suivie de `{{ kubelet_reserved_args | default('') }}` - `yamllint -d relaxed` et `ansible-lint --offline` : **mêmes constats que sur `main`**, aucun nouveau (1 erreur `trailing-spaces` et 3 avertissements côté yamllint, 15 constats côté ansible-lint, tous hors des lignes modifiées). Le dépôt n'a de configuration ni pour l'un ni pour l'autre, et aucune CI ne couvre `ansible/`. ## À l'application : ce qui va se passer ``` ansible-playbook -i ansible/arcodange/factory/inventory ansible/arcodange/factory/playbooks/system/system_k3s.yml ``` - **k3s redémarre sur chaque nœud** : l'unité est régénérée. Sur pi1, l'API k3s est donc brièvement indisponible. - **Premier passage** (toutes les 5 min après le redémarrage) : pi3 et pi1 sont au-dessus de 65 %, et le kubelet y supprime des images jusqu'à 55 %. Sur pi3, ça fait jusqu'à ~130 Go d'images supprimées d'un coup sur un disque USB. Mieux vaut l'appliquer dans un moment calme. Aujourd'hui à 18 h 20 UTC, le Docker de pi3 expirait déjà ses créations de conteneurs (`operation timeout: context deadline exceeded`), cause non établie. - ⚠ **Si ce qui n'est pas une image inutilisée dépasse déjà 55 % du disque**, le kubelet supprime **toutes** les images inutilisées à chaque passage, puis journalise `FreeDiskSpaceFailed`. Les images sont retéléchargées au besoin, sans perte. Sur pi3, cri-dockerd annonce 120 Go d'images pour 400 Go occupés, dont ~83 Go de réplicas Longhorn. Le reste peut être du cache de build (les builds CI tournent sur pi3), que le kubelet ne touche pas. **`docker system df` sur pi3 avant d'appliquer** le dira. ## Vérifier après application ``` # les seuils sont pris (attendu : 65 et 55 sur les 3 nœuds) for n in pi1 pi2 pi3; do kubectl --context default get --raw /api/v1/nodes/$n/proxy/configz \ | jq -c '.kubeletconfig | {n: "'$n'", hi: .imageGCHighThresholdPercent, lo: .imageGCLowThresholdPercent}'; done # occupation du disque des images (attendu : sous 65 % après un ou deux passages) kubectl --context default get --raw /api/v1/nodes/pi3/proxy/stats/summary \ | jq '.node.runtime.imageFs | 100 - (.availableBytes * 100 / .capacityBytes | floor)' # le compteur de suppressions monte kubectl --context default get --raw /api/v1/nodes/pi3/proxy/metrics | grep kubelet_image_garbage_collected_total # Longhorn reprend pi3 (attendu : Schedulable=True) kubectl --context default -n longhorn-system get nodes.longhorn.io pi3 \ -o jsonpath='{range .status.diskStatus.*.conditions[*]}{.type}={.status} {end}{"\n"}' # le ramasse-miettes ne tourne pas à vide (attendu : aucun résultat) kubectl --context default get events -A --field-selector reason=FreeDiskSpaceFailed ``` ## Documentation `vibe/guidebooks/factory-provisioning/ansible/01-system.md` : la ligne 7 et son récit mentionnent désormais les arguments du kubelet et le seuil. Last Updated passe au 2026-09-25. ## Liens - Audit « démarrer sans disque » demandé avec ce réglage : #63 (issue séparée, trop long pour cette description) - Même famille : #60 (rétention du registre de paquets, la SD de pi2 pleine d'images kadans) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
arcodange added 1 commit 2026-09-25 20:37:00 +02:00
Le kubelet ne supprimait les images inutilisées qu'à 85 % d'occupation du
disque des images, et s'arrêtait à 80 %. Ce disque est le disque externe
(/mnt/arcodange), que Longhorn retire de l'ordonnancement dès 75 %
occupés : le nettoyage laissait donc le disque en permanence au-dessus de
la ligne de Longhorn. Relevé le 2026-09-25 sur pi3 : 82 %, disque
Longhorn Schedulable=False, 272 anciennes images du front Kadans.

Ajout de --kubelet-arg=image-gc-high-threshold=65 et
--kubelet-arg=image-gc-low-threshold=55 au serveur (pi1) ET aux agents
(pi2, pi3). Le mécanisme s'applique bien au runtime Docker : k3s --docker
passe par cri-dockerd, qui rend au kubelet le système de fichiers des
images et exécute ses suppressions (371 images déjà supprimées sur pi3,
kubelet_image_garbage_collected_total{reason="space"}).

Indépendant du disque externe : sans lui, les images tombent sur la carte
SD, et le même seuil la protège.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Owner

Lien croisé : le même jour, Prometheus est muet à cause du stockage de pi3 (journaux ext4 abandonnés sur 7 volumes Longhorn entre 18 h 18 et 18 h 23 UTC, charge 58 à 71), pas à cause de sa configuration de scrutation. Voir arcodange-org/tools#59. Ce réglage soulage le disque de pi3, mais ne règle pas à lui seul ce qui arrive au TSDB.

Lien croisé : le même jour, Prometheus est muet à cause du stockage de pi3 (journaux ext4 abandonnés sur 7 volumes Longhorn entre 18 h 18 et 18 h 23 UTC, charge 58 à 71), pas à cause de sa configuration de scrutation. Voir arcodange-org/tools#59. Ce réglage soulage le disque de pi3, mais ne règle pas à lui seul ce qui arrive au TSDB.
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin arcodange/kubelet-image-gc:arcodange/kubelet-image-gc
git checkout arcodange/kubelet-image-gc
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: arcodange-org/factory#64