Ce que le cluster gardait vraiment — mesuré, et c'est pire que l'issue ne le supposait
L'issue disait « le tampon du kubelet tourne ». On a demandé à chaque journal jusqu'où il remonte, le 2026-09-07 à 15:05 UTC :
conteneur
pod démarré
journal remonte à
kube-system/traefik
il y a 12 jours
10 minutes
tools/clickhouse-0
il y a 8 jours
2 minutes
tools/prometheus-server
il y a 7 h
son démarrage (pod peu bavard)
tools/grafana
il y a 45 jours
10 jours (pod peu bavard)
Les deux composants les plus bavards du cluster gardent entre deux et dix minutes d'histoire.
Ce chiffre est tombé d'un désaccord entre deux instruments, et le désaccord était le résultat : une fenêtre de 10 min extrapolée ×6 donnait 13,1 Mo/h, la fenêtre d'1 h mesurait 7,9 Mo/h — 0,60 fois. Par conteneur, le rapport 1 h / 10 min vaut ~6 partout (débit stable) sauf traefik (1,18) et clickhouse (3,98) : leur fenêtre d'une heure était tronquée. D'où la vérification directe ci-dessus.
L'agent : Alloy, et pourquoi pas Promtail
Relevé sur l'index Helm de Grafana lui-même (curl https://grafana.github.io/helm-charts/index.yaml) :
chart
versions
dernière publication
deprecated: dans l'index
promtail
141
2025-10-31
true
alloy
57
2026-08-27
absent
loki
344
2026-08-10
absent
Promtail n'est pas « bientôt en fin de vie » : son chart porte déjà le drapeau, et sa dernière publication a plus de dix mois. Installer à neuf un agent que l'amont a rangé, pour économiser de la mémoire dont le montant restait à vérifier, c'est acheter une dette à échéance connue. ARM64 vérifié au registre (manifeste multi-plateforme) et empiriquement — les pods tournent sur les Pi.
La rétention : 30 jours
Débit corrigé de la troncature (max(1 h, 10 min × 6) par conteneur) : 13 333 645 octets/h = 305 Mio/jour bruts.
volume
30 j bruts
8,94 Gio
30 j compressés (facteur 5, prudent)
1,79 Gio
30 j compressés (facteur 10, typique)
0,89 Gio
Volume posé : 10 Gi sur Longhorn (5 à 10× de marge). Le plus serré des trois nœuds Longhorn a 99 Gio libres.
Pourquoi 30 et pas 7 : les deux pannes qui motivent ce lot ont duré des semaines (la mise à l'abri, kadans#1033) ou trois jours (#36). Une rétention de 7 jours aurait perdu le début des deux. 30 jours couvre le délai réel entre « ça casse » et « quelqu'un s'en aperçoit ».
⚠ Ce qui rendrait ce chiffre faux : c'est la photo d'un mardi après-midi. Elle ne dit rien d'un pod parti en boucle d'erreur, cas où un seul conteneur peut dépasser tout le reste du cluster. La marge est là pour ça, pas pour la croissance ordinaire.
Les plafonds, parce que pi2 est à 108 %
Les défauts amont du chart Loki sont écrits pour un cluster de serveurs. Laissés tels quels sur trois Raspberry Pi, ils auraient posé :
défaut amont
ce que ça donne ici
chunksCache.allocatedMemory: 8192
un memcached réclamant 8 Gio sur un nœud qui en alloue 5,6
resultsCache.enabled: true
un second memcached
deploymentMode: SimpleScalable
read + write + backend, trois jeux
gateway.enabled: true
un nginx de plus
lokiCanary.enabled: true
un DaemonSet de plus
replication_factor: 3
trois exemplaires de chaque flux
storage.type: s3
un magasin objet qu'on n'a pas
Chaque extinction est nommée avec sa raison dans loki/values.yaml, plutôt que déduite d'un deploymentMode qui « devrait » suffire.
Deux découvertes faites en déployant, pas en lisant
admin_api_directory est accepté par le chart et REFUSÉ par le binaire OSS. C'est un champ de l'édition entreprise. Résultat : failed parsing config: field admin_api_directory not found in type common.FilesystemConfig, CrashLoopBackOff, 3 redémarrages en 4 min. Le chart ne valide pas la config qu'il écrit — seul le démarrage réel le dit.
Le chart ajoute un side-car loki-sc-rules avec resources: {} — ni requête ni limite, dans un pod par ailleurs plafonné, sur pi2. Un conteneur non plafonné dans un pod plafonné annule le plafond. Éteint : le ruler est à 0 réplique, ce side-car surveillait des ConfigMaps inexistantes.
Un troisième relevé, hors périmètre mais à savoir
Le planificateur ne peut pas protéger pi2 : il ne voit que les requests, et sur ce cluster elles ne sont presque pas déclarées.
nœud
mémoire demandée (vue du planificateur)
mémoire réellement utilisée
pi1
8 %
75 %
pi2
17 %
108 %
pi3
18 %
58 %
C'est pourquoi Loki a été placé sur pi2, le nœud le plus chargé — le planificateur le croyait le plus libre. Les charts de ce lot déclarent, eux, des requêtes honnêtes.
La CI
⚠ La matrice de helmcharts.yaml est codée en dur : un chart absent de la liste n'est jamais construit et sa PR est verte quand même (le défaut raconté dans #32). loki et alloy y sont ajoutés — sans ça, ce lot n'aurait aucune preuve.
Les mesures d'empreinte après déploiement, la démonstration de survie au redémarrage et la table des sabotages arrivent en commentaire de cette PR — ils demandent que la pile tourne.
Closes #38
## Ce que le cluster gardait vraiment — mesuré, et c'est pire que l'issue ne le supposait
L'issue disait « le tampon du kubelet tourne ». On a demandé à chaque journal jusqu'où il remonte, le 2026-09-07 à 15:05 UTC :
| conteneur | pod démarré | journal remonte à |
|---|---|---|
| `kube-system/traefik` | il y a **12 jours** | **10 minutes** |
| `tools/clickhouse-0` | il y a **8 jours** | **2 minutes** |
| `tools/prometheus-server` | il y a 7 h | son démarrage (pod peu bavard) |
| `tools/grafana` | il y a 45 jours | 10 jours (pod peu bavard) |
Les deux composants les plus bavards du cluster gardent **entre deux et dix minutes** d'histoire.
Ce chiffre est tombé d'un désaccord entre deux instruments, et le désaccord était le résultat : une fenêtre de 10 min extrapolée ×6 donnait 13,1 Mo/h, la fenêtre d'1 h mesurait 7,9 Mo/h — 0,60 fois. Par conteneur, le rapport 1 h / 10 min vaut ~6 partout (débit stable) **sauf** traefik (1,18) et clickhouse (3,98) : leur fenêtre d'une heure était **tronquée**. D'où la vérification directe ci-dessus.
## L'agent : Alloy, et pourquoi pas Promtail
Relevé sur l'index Helm de Grafana lui-même (`curl https://grafana.github.io/helm-charts/index.yaml`) :
| chart | versions | dernière publication | `deprecated:` dans l'index |
|---|---|---|---|
| `promtail` | 141 | **2025-10-31** | **`true`** |
| `alloy` | 57 | 2026-08-27 | absent |
| `loki` | 344 | 2026-08-10 | absent |
Promtail n'est pas « bientôt en fin de vie » : son chart **porte déjà le drapeau**, et sa dernière publication a plus de dix mois. Installer à neuf un agent que l'amont a rangé, pour économiser de la mémoire dont le montant restait à vérifier, c'est acheter une dette à échéance connue. ARM64 vérifié au registre (manifeste multi-plateforme) **et** empiriquement — les pods tournent sur les Pi.
## La rétention : 30 jours
Débit corrigé de la troncature (`max(1 h, 10 min × 6)` par conteneur) : **13 333 645 octets/h = 305 Mio/jour bruts**.
| | volume |
|---|---|
| 30 j bruts | 8,94 Gio |
| 30 j compressés (facteur 5, prudent) | 1,79 Gio |
| 30 j compressés (facteur 10, typique) | 0,89 Gio |
Volume posé : **10 Gi** sur Longhorn (5 à 10× de marge). Le plus serré des trois nœuds Longhorn a 99 Gio libres.
**Pourquoi 30 et pas 7** : les deux pannes qui motivent ce lot ont duré des **semaines** (la mise à l'abri, kadans#1033) ou trois jours (#36). Une rétention de 7 jours aurait perdu le début des deux. 30 jours couvre le délai réel entre « ça casse » et « quelqu'un s'en aperçoit ».
⚠ **Ce qui rendrait ce chiffre faux** : c'est la photo d'un mardi après-midi. Elle ne dit rien d'un pod parti en boucle d'erreur, cas où un seul conteneur peut dépasser tout le reste du cluster. La marge est là pour ça, pas pour la croissance ordinaire.
## Les plafonds, parce que pi2 est à 108 %
Les défauts amont du chart Loki sont écrits pour un cluster de serveurs. Laissés tels quels sur trois Raspberry Pi, ils auraient posé :
| défaut amont | ce que ça donne ici |
|---|---|
| `chunksCache.allocatedMemory: 8192` | un memcached réclamant **8 Gio** sur un nœud qui en alloue 5,6 |
| `resultsCache.enabled: true` | un second memcached |
| `deploymentMode: SimpleScalable` | read + write + backend, trois jeux |
| `gateway.enabled: true` | un nginx de plus |
| `lokiCanary.enabled: true` | un DaemonSet de plus |
| `replication_factor: 3` | trois exemplaires de chaque flux |
| `storage.type: s3` | un magasin objet qu'on n'a pas |
Chaque extinction est **nommée avec sa raison** dans `loki/values.yaml`, plutôt que déduite d'un `deploymentMode` qui « devrait » suffire.
## Deux découvertes faites en déployant, pas en lisant
1. **`admin_api_directory` est accepté par le chart et REFUSÉ par le binaire OSS.** C'est un champ de l'édition entreprise. Résultat : `failed parsing config: field admin_api_directory not found in type common.FilesystemConfig`, CrashLoopBackOff, 3 redémarrages en 4 min. **Le chart ne valide pas la config qu'il écrit** — seul le démarrage réel le dit.
2. **Le chart ajoute un side-car `loki-sc-rules` avec `resources: {}`** — ni requête ni limite, dans un pod par ailleurs plafonné, sur pi2. **Un conteneur non plafonné dans un pod plafonné annule le plafond.** Éteint : le `ruler` est à 0 réplique, ce side-car surveillait des ConfigMaps inexistantes.
## Un troisième relevé, hors périmètre mais à savoir
Le planificateur **ne peut pas** protéger pi2 : il ne voit que les *requests*, et sur ce cluster elles ne sont presque pas déclarées.
| nœud | mémoire *demandée* (vue du planificateur) | mémoire **réellement utilisée** |
|---|---|---|
| pi1 | 8 % | **75 %** |
| pi2 | **17 %** | **108 %** |
| pi3 | 18 % | 58 % |
C'est pourquoi Loki a été placé **sur pi2**, le nœud le plus chargé — le planificateur le croyait le plus libre. Les charts de ce lot déclarent, eux, des requêtes honnêtes.
## La CI
⚠ La matrice de `helmcharts.yaml` est **codée en dur** : un chart absent de la liste n'est jamais construit et sa PR est verte quand même (le défaut raconté dans #32). `loki` et `alloy` y sont ajoutés — sans ça, ce lot n'aurait **aucune** preuve.
---
*Les mesures d'empreinte après déploiement, la démonstration de survie au redémarrage et la table des sabotages arrivent en commentaire de cette PR — ils demandent que la pile tourne.*
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Le cluster n'avait AUCUNE collecte de journaux : la seule lecture possible
était `kubectl logs`, c'est-à-dire le tampon du kubelet, qui tourne. Mesuré
le 2026-09-07 en demandant à chaque journal jusqu'où il remonte :
kube-system/traefik pod démarré il y a 12 j — journal remontant à 10 MIN
tools/clickhouse-0 pod démarré il y a 8 j — journal remontant à 2 MIN
Les deux composants les plus bavards du cluster gardent entre deux et dix
minutes d'histoire. C'est pire que ce que l'issue supposait.
Ce lot pose Loki (binaire unique, stockage fichier sur Longhorn) et Alloy
(l'agent, un pod par nœud), et branche la source de données au Grafana qui
tourne déjà.
L'AGENT : Alloy, pas Promtail — et c'est mesuré, pas une préférence. L'index
Helm de Grafana marque le chart `promtail` `deprecated: true`, dernière
publication 2025-10-31. Alloy : 2026-08-27. On n'installe pas à neuf un agent
que l'amont a déjà rangé.
LA RÉTENTION : 30 jours, sur un débit mesuré de ~305 Mio/jour bruts pour tout
le cluster, soit 0,9 à 1,8 Gio compressés. 7 jours auraient perdu le début des
deux pannes qui motivent ce lot (kadans#1033, des SEMAINES ; tools#36, 3 jours).
LES PLAFONDS NE SONT PAS OPTIONNELS : pi2 est mesuré à 108 % de sa mémoire
allouable. Les défauts amont du chart Loki y auraient posé un memcached
réclamant 8 Gio (`chunksCache.allocatedMemory: 8192`), un second memcached, un
nginx, un DaemonSet canari et trois jeux de répliques. Chaque extinction est
nommée dans `loki/values.yaml`, avec sa raison.
⚠ La CI de ce dépôt monte une matrice CODÉE EN DUR : un chart absent de la
liste n'est jamais construit et sa PR est verte quand même (tools#32). `loki`
et `alloy` y sont ajoutés — sans ça, ce lot n'aurait aucune preuve.
Deux découvertes faites en déployant, pas en lisant :
- `admin_api_directory` est accepté par le chart mais REFUSÉ par le binaire
OSS (champ d'édition entreprise) : CrashLoopBackOff jusqu'à ce qu'on l'ôte ;
- le chart ajoute un side-car `loki-sc-rules` SANS aucune limite de
ressources — un conteneur non plafonné dans un pod plafonné annule le
plafond. Éteint (le ruler est à 0 réplique).
Closes#38🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Trois faits mesurés au déploiement, qu'aucune lecture du chart n'aurait donnés.
1. LE CLUSTER TOURNE SOUS DOCKER, PAS SOUS CONTAINERD.
`kubectl get nodes -o …containerRuntimeVersion` :
pi1 docker://29.1.3
pi2 docker://28.4.0
pi3 docker://27.4.0
Le commentaire du premier jet affirmait le contraire. Il était faux.
2. LES JOURNAUX NE SONT PAS SOUS /var/log — CE SONT DES LIENS.
/var/log/pods/<ns>_<pod>_<uid>/<conteneur>/0.log
-> /mnt/arcodange/docker/containers/<id>/<id>-json.log
La racine Docker est déplacée sur un disque externe. Monter /var/log donne
les liens et PAS leurs cibles : `ls` les montre, `stat` échoue dessus. Le
drapeau `dockercontainers` du chart monterait /var/lib/docker/containers,
qui n'existe pas ici. On monte la vraie racine, en lecture seule.
Chemin vérifié identique sur pi1 et pi3, pod par pod.
3. LE FORMAT EST DU JSON DOCKER, DONC `stage.docker`, PAS `stage.cri`.
Avec le mauvais décodeur, l'horodatage stocké serait celui de la LECTURE et
non celui de l'écriture — or dater une panne à la minute est précisément
l'usage qui motive ce lot (tools#36).
⚠⚠ CE QUI REND CE DÉFAUT DANGEREUX : IL SE PRÉSENTE COMME UNE SANTÉ PARFAITE.
Les six composants d'Alloy se déclaraient `healthy`, aucune erreur de poussée,
Loki répondait `ready`, les trois agents étaient 2/2 Running. La seule chose
qui parlait était `loki_source_file_files_active_total = 0` et l'export vide de
`local.file_match`. Une pile de journaux qui rassure sans rien garder : le
défaut même que l'issue #38 demande de ne pas reproduire.
Le hostPath est en `type: Directory` et non `DirectoryOrCreate`, exprès — si un
nœud n'a pas ce chemin, son agent doit refuser de démarrer et le DIRE, plutôt
que de monter un dossier vide et de rapporter zéro en se déclarant sain.
Corrige aussi le second conteneur non plafonné, jumeau de celui de Loki :
`config-reloader` arrive avec une REQUÊTE de 50Mi et AUCUNE limite. Une requête
sans limite n'est pas un plafond, c'est une réservation — et sur un DaemonSet
cela vaut sur les trois nœuds. On ne peut pas l'éteindre sans perdre le
rechargement à chaud : on le plafonne (32Mi / 64Mi).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
L'étiquette `node` était absente de Loki sur une collecte par ailleurs saine :
`__meta_kubernetes_node_name` n'existe QUE pour `role = "node"`. Sous
`role = "pod"`, elle est vide — et un relabel dont la source est vide ne pose
pas l'étiquette, SANS erreur ni avertissement.
On ne s'en aperçoit qu'en DEMANDANT les valeurs de l'étiquette
(`/loki/api/v1/label/node/values`) et en trouvant la liste vide. Les six
composants d'Alloy restaient `healthy`, 68 fichiers activement suivis, les
lignes arrivaient. Seule l'étiquette par laquelle on aurait cherché « que
disait pi2 cette nuit-là » manquait.
Vérifié après correction : `node` rend pi1, pi2, pi3.
⚠ Relevé au passage, à ne pas oublier : le `config-reloader` n'a PAS rechargé
Alloy quand la ConfigMap a changé. Le fichier monté portait bien la correction
(horodaté 15:31), le composant en mémoire exécutait toujours l'ancienne règle.
Il a fallu un `rollout restart` pour que la nouvelle config prenne. Le
rechargement à chaud n'est donc pas une garantie — après un changement de
configuration d'Alloy, VÉRIFIER la propriété visée plutôt que supposer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
C'est le défaut le plus coûteux de ce lot, et celui qui ressemblait le moins à
ce qu'il était.
SYMPTÔME : `kubectl logs` montrait 19 lignes du pod témoin, Loki en montrait 0.
L'agent se déclarait sain, ses six composants `healthy`, 74 fichiers activement
suivis, le journal disait « start tailing file » sur le bon chemin, et TOUS ses
compteurs `loki_write_dropped_entries_total` valaient zéro — quelle que soit la
raison (ingester_error, line_too_long, queue_is_full, rate_limited,
stream_limited). Seul `loki_write_sent_entries_total = 0` disait qu'il ne
passait rien, sans dire pourquoi.
CAUSE, lisible uniquement dans le journal de Loki :
POST /loki/api/v1/push (500)
failed to create stream: no schema config found for time 1786622254.804
entry ... has timestamp too old: 2026-04-13T13:17:53Z
`schemaConfig.configs[0].from` était posé à la date du jour. Or `from` dit
« à partir de quand ce schéma s'applique », PAS « depuis quand on garde ».
Sous cette date, Loki n'a aucun schéma pour les horodatages antérieurs.
Et l'agent lit aussi les fichiers ROTATÉS encore sur le disque (4.log, 9.log,
10.log…), qui portent des lignes vieilles de plusieurs mois — l'une de
2026-04-13. ⚠ Une poussée étant un LOT, le 500 emporte les lignes FRAÎCHES qui
voyageaient avec les vieilles. D'où : rien n'arrive, et rien ne l'explique côté
agent.
La date passe à 2020-01-01, antérieure à tout ce que ces machines peuvent
porter. Ce qui borne la conservation reste `retention_period` (720 h) et le
compacteur, pas le schéma.
⚠ À NOTER, PARCE QUE C'EST L'ISSUE #38 EN ABYME : cette cause n'était lisible
que dans le journal du pod Loki — qu'on ne pouvait lire que parce qu'il n'avait
pas redémarré. Le défaut qui empêchait de garder les journaux ne se
diagnostiquait que par un journal non gardé.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Second blocage complet, découvert juste après celui du schéma et de la même
famille : un réglage lu comme « combien de temps on garde » alors qu'il dit
« quelles horloges on accepte ».
Posé à 720 h pour « coller » à la rétention, il refusait chaque lot contenant
une ligne de plus de 30 jours :
entry ... has timestamp too old: 2026-04-13T13:47:05Z,
oldest acceptable timestamp is: 2026-08-08T15:41:50Z
Or l'agent lit les fichiers rotatés encore présents sur le disque, dont
certains remontent à AVRIL. Et comme un lot définitivement refusé est REJOUÉ,
la file ne se vide jamais : mesuré sur l'agent de pi3,
`loki_write_sent_entries_total` valait 0 — il n'avait JAMAIS rien transmis,
pour 74 fichiers activement suivis.
Ce garde-fou existe contre les horloges décalées. On l'ouvre à 8760 h, bien
au-delà de ce que le disque peut porter. Ce qui borne la conservation reste
`retention_period` (720 h) plus le compacteur.
⚠ Conséquence assumée, écrite dans les valeurs : au premier démarrage l'agent
remonte l'historique du disque, et le compacteur supprime ensuite ce qui
dépasse 30 jours. On paie une ingestion inutile UNE fois, contre une file qui
se vide — et on récupère au passage l'histoire déjà écrite.
⚠ Un état bloqué ne se débloque pas tout seul : après avoir corrigé Loki, il a
fallu REDÉMARRER les agents. Le compteur d'un agent resté sur son ancien lot ne
repart pas parce que le serveur, lui, va mieux.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Relevé sur la pile qui tourne (`kubectl top pods --containers`), pendant la
reprise de l'historique :
agent de pi1 214 Mi ← 84 % du plafond de 256 Mi
agent de pi2 98 Mi
agent de pi3 83 Mi
Aucun OOMKill constaté (0 redémarrage sur les trois), mais 84 % n'est pas une
marge. Un agent tué perd sa position de lecture, et un trou dans la collecte
est exactement ce que ce lot existe pour supprimer.
⚠ Relever un PLAFOND ne consomme rien : il borne le pire cas. C'est la REQUÊTE
(128 Mi, inchangée) que le planificateur réserve — donc rien n'est retiré à
pi2, le nœud tendu.
⚠ L'écart entre les trois nœuds n'est pas du bruit : il suit le nombre de
fichiers suivis, donc le nombre de conteneurs du nœud.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
La pile tourne. Voici ce qui est mesuré, et ce qui ne l'est pas.
La pile a été déployée à la main sur le cluster (helm install depuis ces mêmes répertoires — donc exactement ce qu'ArgoCD rendra après fusion) pour pouvoir mesurer et éprouver. Rien n'a été appliqué avec Tofu, rien n'a touché Vault.
1. La propriété de #38 : un message survit-il au pod qui l'a écrit ?
Oui. Vérifié, code de sortie 0.
Le protocole, et pourquoi chaque pas y est :
JETON = survie-1788795959-23800
--- 1. un pod jetable écrit la ligne, puis dort ---
pod temoin-journal : phase=Running sur pi3
--- 3. la ligne existe-t-elle seulement ? (kubectl logs, pod VIVANT) ---
kubectl logs voit le jeton : 2 fois
--- 2. Loki l'a-t-il ingérée ? ---
Loki, pod VIVANT : 8 ligne(s)
--- 4. on SUPPRIME le pod, et on attend que kubectl logs n'en puisse plus rien ---
pod supprimé (code 0)
kubectl logs échoue désormais : oui
--- 5. LE VERDICT : Loki répond-il encore, le pod n'existant plus ? ---
Loki, pod MORT : 28 ligne(s)
✅ PROPRIÉTÉ TENUE — la ligne survit au pod qui l'a écrite.
CODE_DE_SORTIE=0
⚠ Le pas 4 est celui qu'on est tenté de sauter. Sans lui on démontrerait seulement que Loki sait lire un pod vivant — ce que kubectl logs fait déjà, et qui n'est pas la propriété qu'on achète. Le test refuse de conclure tant que kubectl logs répond encore.
2. L'empreinte, avant / après — pi2 compris
Séries d'échantillons de kubectl top nodes (30 s d'intervalle), pas un instant unique.
nœud
AVANT (n=12)
APRÈS (n=7)
écart
pi1
6098 Mi — 75,2 %
6376 Mi — 78,9 %
+278 Mi, +3,7 pts
pi2
6269 Mi — 108,1 %
6412 Mi — 110,7 %
+143 Mi, +2,6 pts
pi3
4742 Mi — 58,0 %
4899 Mi — 60,1 %
+157 Mi, +2,1 pts
Oui, l'ajout pousse pi2 plus haut : de 108,1 % à 110,7 % de son allouable. Je ne l'enjolive pas. Deux nuances mesurées, qui ne l'effacent pas :
ce pourcentage porte sur l'allouable (5772 Mi), pas sur la capacité (7820 Mi) ; en valeur absolue pi2 utilise 6412 Mi sur 7820, soit ~1,4 Gio de marge physique réelle ;
Loki a été placé sur pi2, le nœud le plus chargé, parce que le planificateur ne voit que les requests — et sur ce cluster elles ne sont presque pas déclarées (pi1 8 %, pi2 17 %, pi3 18 % demandés, contre 75/108/58 % réellement utilisés). Le planificateur ne peut pas protéger pi2. Déplacer Loki sur pi3 est une ligne de nodeSelector si vous le voulez ; je ne l'ai pas fait de moi-même, parce qu'épingler un nœud dans un chart est le genre de chose qui pourrit.
Par conteneur, à l'état stabilisé :
conteneur
mémoire
plafond
loki
221 Mi
512 Mi
alloy (pi1)
303 Mi
384 Mi
alloy (pi2)
109 Mi
384 Mi
alloy (pi3)
83 Mi
384 Mi
config-reloader (×3)
7–8 Mi
64 Mi
Zéro redémarrage, zéro OOMKill sur les quatre pods.
⚠ Le plafond de l'agent a dû être relevé de 256 à 384 Mi, et c'est une mesure qui l'a dit : l'agent de pi1 est monté à 303 Mi — au-dessus des 256 Mi du premier jet. Il aurait été tué. Relever un plafond ne consomme rien (c'est la requête, 128 Mi, que le planificateur réserve), donc rien n'est retiré à pi2.
Disque : le volume Longhorn de Loki porte 468 Mio pour 1 273 677 lignes ingérées — l'historique de reprise inclus. Sur 10 Gi demandés.
0 ligne, contre 43 pour le vrai jeton, même requête, même instant
✅ la preuve de survie n'est pas complaisante
⚠⚠ Le sabotage A ne mord pas, et c'est le résultat le plus important de cette table
helm template a rendu 0 sur un schemaConfig corrompu. Autrement dit : la CI de ce dépôt prouve que le chart SE REND, elle ne prouve rien de ce que Loki fera de la configuration produite. Un vert ici n'est pas un vert fonctionnel.
Ce n'est pas une hypothèse — c'est arrivé deux fois dans ce lot, et les deux fois la CI était verte :
admin_api_directory — accepté par le chart, refusé par le binaire OSS (champ d'édition entreprise) → CrashLoopBackOff ;
schemaConfig.from à la date du jour → POST /push (500), plus rien n'entre.
Je n'ai pas ajouté de gate qui démarrerait vraiment Loki sur la config rendue. C'est le manque le plus sérieux de cette PR, et je préfère le nommer que le maquiller.
4. Les cinq défauts trouvés en déployant, qu'aucune lecture n'aurait donnés
#
défaut
ce qui l'a révélé
1
admin_api_directory refusé par le binaire
CrashLoopBackOff, 3 redémarrages en 4 min
2
side-car loki-sc-rulessans aucune limite
kubectl get pod -o jsonpath sur les resources
3
config-reloader d'Alloy avec une requête mais pas de limite
idem — une requête sans limite n'est pas un plafond
4
cluster sous Docker, racine déplacée sur /mnt/arcodange/docker
ls -l : les journaux sont des liens ; stat échoue sur la cible
5
schemaConfig.from + reject_old_samples_max_age
le journal de Loki, seul endroit qui le disait
⚠⚠ Le défaut 4 est le plus traître de tous : tout avait l'air sain. Les six composants d'Alloy healthy, aucune erreur de poussée, Loki ready, les trois agents 2/2 Running. Le seul relevé qui parlait était loki_source_file_files_active_total = 0 et l'export vide de local.file_match. Une pile de journaux qui rassure sans rien garder — précisément ce que #38 demande de ne pas reproduire.
⚠ Et le défaut 5 est l'issue #38en abyme : sa cause n'était lisible que dans le journal du pod Loki, qu'on ne pouvait lire que parce qu'il n'avait pas redémarré.
5. Grafana
La source de données n'est pas encore déployée (la ConfigMap vient de main). Ce qui est vérifié, depuis le pod Grafana lui-même, à l'URL exacte que déclare la source :
Les skipped sont le comportement voulu : ces charts ne sont pas touchés. Les quatre qui le sont ont tourné, pas été sautés — et le sabotage B a prouvé qu'ils savent rougir.
⚠ main est rouge indépendamment de cette PR (run 6730, tête dc48d48, la fusion de #39). Ce n'est pas causé par ce lot.
7. Ce que je n'ai PAS vérifié — à lire avant de fusionner
Aucune gate ne démarre Loki sur la config rendue. Le sabotage A le prouve : la CI laisse passer une config sémantiquement cassée. Les deux pannes de ce lot seraient repassées.
Le comportement d'ArgoCD n'est pas éprouvé. J'ai déployé avec helm install depuis les mêmes répertoires ; je n'ai pas vu ArgoCD adopter les releases existantes. L'adoption devrait se faire, mais je ne l'ai pas constatée.
La rétention n'a pas été observée s'exécuter.retention_enabled: true et retention_period: 720h sont dans la config rendue, mais aucune donnée n'a encore 30 jours ici : je n'ai jamais vu le compacteur supprimer quoi que ce soit. La propriété « le volume ne grossit pas indéfiniment » reste non démontrée.
Les premières lignes d'un pod peuvent être perdues. Mesuré : le pod écrit à 15:35:47, l'agent s'attache à 15:35:52 — cinq secondes plus tard, et ce qui précède n'est pas repris. ⚠ Ce sont exactement les lignes d'un plantage au démarrage, donc les plus précieuses. Non corrigé.
La tenue dans la durée est inconnue. Tout ceci tient sur environ deux heures d'observation. Rien ne dit ce que donnent l'agent et Loki après plusieurs jours, ni sous une avalanche de journaux (un pod en boucle d'erreur), cas où un seul conteneur peut dépasser tout le reste du cluster.
Le débit est une photo d'un mardi après-midi, pas une moyenne. La marge de 5 à 10× sur le volume est là pour ça.
/mnt/arcodange/docker/containers est vérifié sur pi1 et pi3, pod par pod. Sur pi2 je l'ai déduit du fait que son agent collecte — je n'ai pas lu le lien moi-même sur ce nœud.
Le rechargement à chaud d'Alloy ne s'est pas déclenché quand sa ConfigMap a changé : le fichier monté portait la correction, le composant en mémoire exécutait l'ancienne règle. Il a fallu un rollout restart. Je n'ai pas cherché pourquoi.
pi2 met plusieurs minutes à terminer un pod (jusqu'à 7 min sur un busybox), au point de bloquer un déploiement progressif. Constaté deux fois, non diagnostiqué — c'est probablement un signe de santé de ce nœud, pas de ce lot.
## La pile tourne. Voici ce qui est mesuré, et ce qui ne l'est pas.
La pile a été déployée à la main sur le cluster (`helm install` depuis ces mêmes répertoires — donc exactement ce qu'ArgoCD rendra après fusion) pour pouvoir mesurer et éprouver. **Rien n'a été appliqué avec Tofu, rien n'a touché Vault.**
---
## 1. La propriété de #38 : un message survit-il au pod qui l'a écrit ?
**Oui. Vérifié, code de sortie 0.**
Le protocole, et pourquoi chaque pas y est :
```
JETON = survie-1788795959-23800
--- 1. un pod jetable écrit la ligne, puis dort ---
pod temoin-journal : phase=Running sur pi3
--- 3. la ligne existe-t-elle seulement ? (kubectl logs, pod VIVANT) ---
kubectl logs voit le jeton : 2 fois
--- 2. Loki l'a-t-il ingérée ? ---
Loki, pod VIVANT : 8 ligne(s)
--- 4. on SUPPRIME le pod, et on attend que kubectl logs n'en puisse plus rien ---
pod supprimé (code 0)
kubectl logs échoue désormais : oui
--- 5. LE VERDICT : Loki répond-il encore, le pod n'existant plus ? ---
Loki, pod MORT : 28 ligne(s)
✅ PROPRIÉTÉ TENUE — la ligne survit au pod qui l'a écrite.
CODE_DE_SORTIE=0
```
⚠ **Le pas 4 est celui qu'on est tenté de sauter.** Sans lui on démontrerait seulement que Loki sait lire un pod *vivant* — ce que `kubectl logs` fait déjà, et qui n'est pas la propriété qu'on achète. Le test refuse de conclure tant que `kubectl logs` répond encore.
---
## 2. L'empreinte, avant / après — pi2 compris
Séries d'échantillons de `kubectl top nodes` (30 s d'intervalle), pas un instant unique.
| nœud | AVANT (n=12) | APRÈS (n=7) | écart |
|---|---|---|---|
| pi1 | 6098 Mi — **75,2 %** | 6376 Mi — **78,9 %** | +278 Mi, +3,7 pts |
| **pi2** | 6269 Mi — **108,1 %** | 6412 Mi — **110,7 %** | **+143 Mi, +2,6 pts** |
| pi3 | 4742 Mi — 58,0 % | 4899 Mi — 60,1 % | +157 Mi, +2,1 pts |
**Oui, l'ajout pousse pi2 plus haut** : de 108,1 % à 110,7 % de son allouable. Je ne l'enjolive pas. Deux nuances mesurées, qui ne l'effacent pas :
- ce pourcentage porte sur l'**allouable** (5772 Mi), pas sur la **capacité** (7820 Mi) ; en valeur absolue pi2 utilise 6412 Mi sur 7820, soit ~1,4 Gio de marge physique réelle ;
- Loki a été placé **sur pi2**, le nœud le plus chargé, parce que le planificateur ne voit que les *requests* — et sur ce cluster elles ne sont presque pas déclarées (pi1 8 %, pi2 17 %, pi3 18 % demandés, contre 75/108/58 % réellement utilisés). **Le planificateur ne peut pas protéger pi2.** Déplacer Loki sur pi3 est une ligne de `nodeSelector` si vous le voulez ; je ne l'ai pas fait de moi-même, parce qu'épingler un nœud dans un chart est le genre de chose qui pourrit.
Par conteneur, à l'état stabilisé :
| conteneur | mémoire | plafond |
|---|---|---|
| `loki` | 221 Mi | 512 Mi |
| `alloy` (pi1) | **303 Mi** | 384 Mi |
| `alloy` (pi2) | 109 Mi | 384 Mi |
| `alloy` (pi3) | 83 Mi | 384 Mi |
| `config-reloader` (×3) | 7–8 Mi | 64 Mi |
**Zéro redémarrage, zéro OOMKill** sur les quatre pods.
⚠ **Le plafond de l'agent a dû être relevé de 256 à 384 Mi, et c'est une mesure qui l'a dit** : l'agent de pi1 est monté à 303 Mi — **au-dessus** des 256 Mi du premier jet. Il aurait été tué. Relever un plafond ne consomme rien (c'est la *requête*, 128 Mi, que le planificateur réserve), donc rien n'est retiré à pi2.
**Disque** : le volume Longhorn de Loki porte **468 Mio** pour **1 273 677 lignes** ingérées — l'historique de reprise inclus. Sur 10 Gi demandés.
---
## 3. La table des sabotages
| # | ce qu'on sabote | attendu | mesuré | verdict |
|---|---|---|---|---|
| **Témoin** | rien — chart intact | rend | `helm template` **code 0** | ✅ le témoin est vert |
| **A** | `schemaConfig.configs` remplacé par une chaîne | job CI rouge | `helm template` **code 0** | ❌ **NE MORD PAS** |
| **B** | YAML malformé dans `loki/values.yaml` | job CI rouge | **code 1**, `cannot load values.yaml: … line 236` | ✅ mord |
| **B en CI** | la même chose, poussée en PR (#41) | `Application charts loki` échoue | job **failure**, étape `Helm template` **failure** ; `alloy`, `chart`, `grafana` restent **success** | ✅ **la gate est CÂBLÉE et discriminante** |
| **C** | interroger Loki avec un jeton jamais écrit | 0 ligne | **0** ligne, contre **43** pour le vrai jeton, même requête, même instant | ✅ la preuve de survie n'est pas complaisante |
### ⚠⚠ Le sabotage A ne mord pas, et c'est le résultat le plus important de cette table
`helm template` a rendu **0** sur un `schemaConfig` corrompu. Autrement dit : **la CI de ce dépôt prouve que le chart SE REND, elle ne prouve rien de ce que Loki fera de la configuration produite.** Un vert ici n'est pas un vert fonctionnel.
Ce n'est pas une hypothèse — c'est arrivé **deux fois** dans ce lot, et les deux fois la CI était verte :
1. `admin_api_directory` — accepté par le chart, refusé par le binaire OSS (champ d'édition entreprise) → CrashLoopBackOff ;
2. `schemaConfig.from` à la date du jour → `POST /push (500)`, plus rien n'entre.
Je n'ai **pas** ajouté de gate qui démarrerait vraiment Loki sur la config rendue. C'est le manque le plus sérieux de cette PR, et je préfère le nommer que le maquiller.
---
## 4. Les cinq défauts trouvés en déployant, qu'aucune lecture n'aurait donnés
| # | défaut | ce qui l'a révélé |
|---|---|---|
| 1 | `admin_api_directory` refusé par le binaire | CrashLoopBackOff, 3 redémarrages en 4 min |
| 2 | side-car `loki-sc-rules` **sans aucune limite** | `kubectl get pod -o jsonpath` sur les `resources` |
| 3 | `config-reloader` d'Alloy avec une **requête mais pas de limite** | idem — une requête sans limite n'est pas un plafond |
| 4 | cluster sous **Docker**, racine déplacée sur `/mnt/arcodange/docker` | `ls -l` : les journaux sont des **liens** ; `stat` échoue sur la cible |
| 5 | `schemaConfig.from` + `reject_old_samples_max_age` | le journal de **Loki**, seul endroit qui le disait |
⚠⚠ **Le défaut 4 est le plus traître de tous : tout avait l'air sain.** Les six composants d'Alloy `healthy`, aucune erreur de poussée, Loki `ready`, les trois agents `2/2 Running`. Le seul relevé qui parlait était `loki_source_file_files_active_total = 0` et l'export vide de `local.file_match`. **Une pile de journaux qui rassure sans rien garder** — précisément ce que #38 demande de ne pas reproduire.
⚠ Et le défaut 5 est l'issue #38 **en abyme** : sa cause n'était lisible que dans le journal du pod Loki, qu'on ne pouvait lire **que parce qu'il n'avait pas redémarré**.
---
## 5. Grafana
La source de données n'est pas encore déployée (la ConfigMap vient de `main`). Ce qui **est** vérifié, depuis le pod Grafana lui-même, à l'URL exacte que déclare la source :
```
$ kubectl exec grafana-c5655c5d8-b6dwz -c grafana -- \
wget -qO- http://loki.tools.svc.cluster.local:3100/loki/api/v1/label/namespace/values
{"status":"success","data":["argocd","cert-manager","cms","dance-lessons-coach","erp",
"erp-sandbox","kadans","kube-system","longhorn-system","prospection",
"telegram-gateway","tools","url-shortener","webapp"]}
```
14 namespaces, dont **`kadans`** — l'application dont la panne muette a motivé ce lot.
---
## 6. CI — vérifiée à l'API, contextes présents et nommés
Tête `bb14dc38`, statut combiné **`success`**, **13 contextes présents** (aucun absent) :
| contexte | état |
|---|---|
| `Detect changed charts` | **success** |
| `Application charts loki` | **success** (16 s) |
| `Application charts alloy` | **success** (51 s) |
| `Application charts chart` | **success** (36 s) |
| `Application charts grafana` | **success** (55 s) |
| `Library charts tool` | skipped |
| crowdsec, hashicorp-vault, minio, pgbouncer, pgcat, prometheus, redis | skipped |
Les `skipped` sont le comportement voulu : ces charts ne sont pas touchés. **Les quatre qui le sont ont tourné, pas été sautés** — et le sabotage B a prouvé qu'ils savent rougir.
⚠ `main` est **rouge indépendamment de cette PR** (run 6730, tête `dc48d48`, la fusion de #39). Ce n'est pas causé par ce lot.
---
## 7. Ce que je n'ai PAS vérifié — à lire avant de fusionner
1. **Aucune gate ne démarre Loki sur la config rendue.** Le sabotage A le prouve : la CI laisse passer une config sémantiquement cassée. Les deux pannes de ce lot seraient repassées.
2. **Le comportement d'ArgoCD n'est pas éprouvé.** J'ai déployé avec `helm install` depuis les mêmes répertoires ; je n'ai pas vu ArgoCD adopter les releases existantes. L'adoption devrait se faire, mais je ne l'ai pas constatée.
3. **La rétention n'a pas été observée s'exécuter.** `retention_enabled: true` et `retention_period: 720h` sont dans la config rendue, mais aucune donnée n'a encore 30 jours ici : **je n'ai jamais vu le compacteur supprimer quoi que ce soit.** La propriété « le volume ne grossit pas indéfiniment » reste **non démontrée**.
4. **Les premières lignes d'un pod peuvent être perdues.** Mesuré : le pod écrit à 15:35:47, l'agent s'attache à 15:35:52 — **cinq secondes** plus tard, et ce qui précède n'est pas repris. ⚠ Ce sont exactement les lignes d'un plantage au démarrage, donc les plus précieuses. Non corrigé.
5. **La tenue dans la durée est inconnue.** Tout ceci tient sur environ deux heures d'observation. Rien ne dit ce que donnent l'agent et Loki après plusieurs jours, ni sous une avalanche de journaux (un pod en boucle d'erreur), cas où un seul conteneur peut dépasser tout le reste du cluster.
6. **Le débit est une photo d'un mardi après-midi**, pas une moyenne. La marge de 5 à 10× sur le volume est là pour ça.
7. **`/mnt/arcodange/docker/containers` est vérifié sur pi1 et pi3**, pod par pod. Sur pi2 je l'ai déduit du fait que son agent collecte — je n'ai pas lu le lien moi-même sur ce nœud.
8. **Le rechargement à chaud d'Alloy ne s'est pas déclenché** quand sa ConfigMap a changé : le fichier monté portait la correction, le composant en mémoire exécutait l'ancienne règle. Il a fallu un `rollout restart`. Je n'ai pas cherché pourquoi.
9. **pi2 met plusieurs minutes à terminer un pod** (jusqu'à 7 min sur un busybox), au point de bloquer un déploiement progressif. Constaté deux fois, non diagnostiqué — c'est probablement un signe de santé de ce nœud, pas de ce lot.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Je l'avais listé comme déduit ; il est maintenant lu directement sur pi2, comme sur les deux autres :
$ kubectl exec alloy-29srr -c alloy -- sh -c 'readlink $(ls /var/log/pods/*/*/*.log | head -1)'
/mnt/arcodange/docker/containers/eb7c39e3…/eb7c39e3…-json.log
Le chemin /mnt/arcodange/docker/containers est donc vérifié pod par pod sur les trois nœuds, pas extrapolé. Les huit autres points de la liste restent valables tels quels.
État final laissé sur le cluster
pod
nœud
état
redémarrages
loki-0
pi2
1/1 Running
0
alloy-hxrmf
pi1
2/2 Running
0
alloy-29srr
pi2
2/2 Running
0
alloy-2kgf6
pi3
2/2 Running
0
Le pod jetable temoin-journal est supprimé. La branche de sabotage arcodange/loki-sabotage-ci et sa PR #41 sont fermées et supprimées.
⚠ La pile a été installée à la main et reste en service — elle collecte dès maintenant. À la fusion, ArgoCD devrait adopter les deux releases Helm existantes plutôt que d'en créer de nouvelles ; c'est le point 2 de la liste des non-vérifiés, et il vaut la peine d'y jeter un œil au premier sync.
### Correction du point 7 de la liste « non vérifié »
Je l'avais listé comme déduit ; il est maintenant **lu directement sur pi2**, comme sur les deux autres :
```
$ kubectl exec alloy-29srr -c alloy -- sh -c 'readlink $(ls /var/log/pods/*/*/*.log | head -1)'
/mnt/arcodange/docker/containers/eb7c39e3…/eb7c39e3…-json.log
```
**Le chemin `/mnt/arcodange/docker/containers` est donc vérifié pod par pod sur les trois nœuds**, pas extrapolé. Les huit autres points de la liste restent valables tels quels.
### État final laissé sur le cluster
| pod | nœud | état | redémarrages |
|---|---|---|---|
| `loki-0` | pi2 | 1/1 Running | 0 |
| `alloy-hxrmf` | pi1 | 2/2 Running | 0 |
| `alloy-29srr` | pi2 | 2/2 Running | 0 |
| `alloy-2kgf6` | pi3 | 2/2 Running | 0 |
Le pod jetable `temoin-journal` est supprimé. La branche de sabotage `arcodange/loki-sabotage-ci` et sa PR #41 sont fermées et supprimées.
⚠ **La pile a été installée à la main et reste en service** — elle collecte dès maintenant. À la fusion, ArgoCD devrait adopter les deux releases Helm existantes plutôt que d'en créer de nouvelles ; c'est le point 2 de la liste des non-vérifiés, et il vaut la peine d'y jeter un œil au premier `sync`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
⚠ Correction des chiffres d'empreinte — j'ai publié une série INCOMPLÈTE
La série « après » du premier commentaire a été lue alors qu'elle tournait encore : n=7 sur 14. La série complète est arrivée depuis, et elle déplace tous les chiffres vers le haut. Comme l'écart va dans le sens qui minore le coût, et sur le nœud que je signale précisément comme tendu, il faut le corriger plutôt que le laisser.
nœud
publié (n=7)
complet (n=14)
écart de ma mesure
pi1
6376 Mi — 78,9 %
6400 Mi — 79,1 %
+24 Mi
pi2
6412 Mi — 110,7 %
6450 Mi — 111,3 %
+38 Mi
pi3
4899 Mi — 60,1 %
4904 Mi — 60,2 %
+5 Mi
Le tableau avant / après qui fait foi
nœud
AVANT (n=12)
APRÈS (n=14)
écart réel
min–max après
pi1
6098 Mi — 75,2 %
6400 Mi — 79,1 %
+302 Mi, +3,9 pts
6297–6630
pi2
6269 Mi — 108,1 %
6450 Mi — 111,3 %
+181 Mi, +3,2 pts
6361–6525
pi3
4742 Mi — 58,0 %
4904 Mi — 60,2 %
+162 Mi, +2,2 pts
4883–4959
Soit ~645 Mio ajoutés sur l'ensemble du cluster, et pi2 passe de 108,1 % à 111,3 % de son allouable — +3,2 points, pas +2,6 comme je l'avais écrit.
Le reste de l'analyse ne bouge pas : le pourcentage porte sur l'allouable (5772 Mi) et non sur la capacité (7820 Mi), donc pi2 conserve ~1,4 Gio de marge physique, et il n'y a eu aucun OOMKill ni redémarrage. Mais le coût réel sur le nœud tendu est un peu plus élevé que ce que j'ai annoncé, et c'est le chiffre n=14 qui vaut.
⚠ La leçon vaut d'être notée : lire une série qui tourne encore, c'est publier une moyenne partielle sans le dire. Les min/max ci-dessus montrent que pi1 varie de 333 Mi d'un échantillon à l'autre — une fenêtre courte n'y suffit pas.
### ⚠ Correction des chiffres d'empreinte — j'ai publié une série INCOMPLÈTE
La série « après » du premier commentaire a été lue alors qu'elle tournait encore : **n=7 sur 14**. La série complète est arrivée depuis, et elle déplace tous les chiffres **vers le haut**. Comme l'écart va dans le sens qui *minore* le coût, et sur le nœud que je signale précisément comme tendu, il faut le corriger plutôt que le laisser.
| nœud | publié (n=7) | **complet (n=14)** | écart de ma mesure |
|---|---|---|---|
| pi1 | 6376 Mi — 78,9 % | **6400 Mi — 79,1 %** | +24 Mi |
| **pi2** | 6412 Mi — 110,7 % | **6450 Mi — 111,3 %** | **+38 Mi** |
| pi3 | 4899 Mi — 60,1 % | **4904 Mi — 60,2 %** | +5 Mi |
### Le tableau avant / après qui fait foi
| nœud | AVANT (n=12) | APRÈS (n=14) | écart réel | min–max après |
|---|---|---|---|---|
| pi1 | 6098 Mi — 75,2 % | 6400 Mi — 79,1 % | **+302 Mi, +3,9 pts** | 6297–6630 |
| **pi2** | 6269 Mi — 108,1 % | 6450 Mi — **111,3 %** | **+181 Mi, +3,2 pts** | 6361–6525 |
| pi3 | 4742 Mi — 58,0 % | 4904 Mi — 60,2 % | **+162 Mi, +2,2 pts** | 4883–4959 |
Soit **~645 Mio ajoutés sur l'ensemble du cluster**, et **pi2 passe de 108,1 % à 111,3 %** de son allouable — +3,2 points, pas +2,6 comme je l'avais écrit.
Le reste de l'analyse ne bouge pas : le pourcentage porte sur l'allouable (5772 Mi) et non sur la capacité (7820 Mi), donc pi2 conserve ~1,4 Gio de marge physique, et il n'y a eu **aucun OOMKill ni redémarrage**. Mais le coût réel sur le nœud tendu est un peu plus élevé que ce que j'ai annoncé, et c'est le chiffre `n=14` qui vaut.
⚠ La leçon vaut d'être notée : **lire une série qui tourne encore, c'est publier une moyenne partielle sans le dire.** Les min/max ci-dessus montrent que pi1 varie de 333 Mi d'un échantillon à l'autre — une fenêtre courte n'y suffit pas.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #38
Ce que le cluster gardait vraiment — mesuré, et c'est pire que l'issue ne le supposait
L'issue disait « le tampon du kubelet tourne ». On a demandé à chaque journal jusqu'où il remonte, le 2026-09-07 à 15:05 UTC :
kube-system/traefiktools/clickhouse-0tools/prometheus-servertools/grafanaLes deux composants les plus bavards du cluster gardent entre deux et dix minutes d'histoire.
Ce chiffre est tombé d'un désaccord entre deux instruments, et le désaccord était le résultat : une fenêtre de 10 min extrapolée ×6 donnait 13,1 Mo/h, la fenêtre d'1 h mesurait 7,9 Mo/h — 0,60 fois. Par conteneur, le rapport 1 h / 10 min vaut ~6 partout (débit stable) sauf traefik (1,18) et clickhouse (3,98) : leur fenêtre d'une heure était tronquée. D'où la vérification directe ci-dessus.
L'agent : Alloy, et pourquoi pas Promtail
Relevé sur l'index Helm de Grafana lui-même (
curl https://grafana.github.io/helm-charts/index.yaml) :deprecated:dans l'indexpromtailtruealloylokiPromtail n'est pas « bientôt en fin de vie » : son chart porte déjà le drapeau, et sa dernière publication a plus de dix mois. Installer à neuf un agent que l'amont a rangé, pour économiser de la mémoire dont le montant restait à vérifier, c'est acheter une dette à échéance connue. ARM64 vérifié au registre (manifeste multi-plateforme) et empiriquement — les pods tournent sur les Pi.
La rétention : 30 jours
Débit corrigé de la troncature (
max(1 h, 10 min × 6)par conteneur) : 13 333 645 octets/h = 305 Mio/jour bruts.Volume posé : 10 Gi sur Longhorn (5 à 10× de marge). Le plus serré des trois nœuds Longhorn a 99 Gio libres.
Pourquoi 30 et pas 7 : les deux pannes qui motivent ce lot ont duré des semaines (la mise à l'abri, kadans#1033) ou trois jours (#36). Une rétention de 7 jours aurait perdu le début des deux. 30 jours couvre le délai réel entre « ça casse » et « quelqu'un s'en aperçoit ».
⚠ Ce qui rendrait ce chiffre faux : c'est la photo d'un mardi après-midi. Elle ne dit rien d'un pod parti en boucle d'erreur, cas où un seul conteneur peut dépasser tout le reste du cluster. La marge est là pour ça, pas pour la croissance ordinaire.
Les plafonds, parce que pi2 est à 108 %
Les défauts amont du chart Loki sont écrits pour un cluster de serveurs. Laissés tels quels sur trois Raspberry Pi, ils auraient posé :
chunksCache.allocatedMemory: 8192resultsCache.enabled: truedeploymentMode: SimpleScalablegateway.enabled: truelokiCanary.enabled: truereplication_factor: 3storage.type: s3Chaque extinction est nommée avec sa raison dans
loki/values.yaml, plutôt que déduite d'undeploymentModequi « devrait » suffire.Deux découvertes faites en déployant, pas en lisant
admin_api_directoryest accepté par le chart et REFUSÉ par le binaire OSS. C'est un champ de l'édition entreprise. Résultat :failed parsing config: field admin_api_directory not found in type common.FilesystemConfig, CrashLoopBackOff, 3 redémarrages en 4 min. Le chart ne valide pas la config qu'il écrit — seul le démarrage réel le dit.loki-sc-rulesavecresources: {}— ni requête ni limite, dans un pod par ailleurs plafonné, sur pi2. Un conteneur non plafonné dans un pod plafonné annule le plafond. Éteint : lerulerest à 0 réplique, ce side-car surveillait des ConfigMaps inexistantes.Un troisième relevé, hors périmètre mais à savoir
Le planificateur ne peut pas protéger pi2 : il ne voit que les requests, et sur ce cluster elles ne sont presque pas déclarées.
C'est pourquoi Loki a été placé sur pi2, le nœud le plus chargé — le planificateur le croyait le plus libre. Les charts de ce lot déclarent, eux, des requêtes honnêtes.
La CI
⚠ La matrice de
helmcharts.yamlest codée en dur : un chart absent de la liste n'est jamais construit et sa PR est verte quand même (le défaut raconté dans #32).lokietalloyy sont ajoutés — sans ça, ce lot n'aurait aucune preuve.Les mesures d'empreinte après déploiement, la démonstration de survie au redémarrage et la table des sabotages arrivent en commentaire de cette PR — ils demandent que la pile tourne.
🤖 Generated with Claude Code
Le cluster n'avait AUCUNE collecte de journaux : la seule lecture possible était `kubectl logs`, c'est-à-dire le tampon du kubelet, qui tourne. Mesuré le 2026-09-07 en demandant à chaque journal jusqu'où il remonte : kube-system/traefik pod démarré il y a 12 j — journal remontant à 10 MIN tools/clickhouse-0 pod démarré il y a 8 j — journal remontant à 2 MIN Les deux composants les plus bavards du cluster gardent entre deux et dix minutes d'histoire. C'est pire que ce que l'issue supposait. Ce lot pose Loki (binaire unique, stockage fichier sur Longhorn) et Alloy (l'agent, un pod par nœud), et branche la source de données au Grafana qui tourne déjà. L'AGENT : Alloy, pas Promtail — et c'est mesuré, pas une préférence. L'index Helm de Grafana marque le chart `promtail` `deprecated: true`, dernière publication 2025-10-31. Alloy : 2026-08-27. On n'installe pas à neuf un agent que l'amont a déjà rangé. LA RÉTENTION : 30 jours, sur un débit mesuré de ~305 Mio/jour bruts pour tout le cluster, soit 0,9 à 1,8 Gio compressés. 7 jours auraient perdu le début des deux pannes qui motivent ce lot (kadans#1033, des SEMAINES ; tools#36, 3 jours). LES PLAFONDS NE SONT PAS OPTIONNELS : pi2 est mesuré à 108 % de sa mémoire allouable. Les défauts amont du chart Loki y auraient posé un memcached réclamant 8 Gio (`chunksCache.allocatedMemory: 8192`), un second memcached, un nginx, un DaemonSet canari et trois jeux de répliques. Chaque extinction est nommée dans `loki/values.yaml`, avec sa raison. ⚠ La CI de ce dépôt monte une matrice CODÉE EN DUR : un chart absent de la liste n'est jamais construit et sa PR est verte quand même (tools#32). `loki` et `alloy` y sont ajoutés — sans ça, ce lot n'aurait aucune preuve. Deux découvertes faites en déployant, pas en lisant : - `admin_api_directory` est accepté par le chart mais REFUSÉ par le binaire OSS (champ d'édition entreprise) : CrashLoopBackOff jusqu'à ce qu'on l'ôte ; - le chart ajoute un side-car `loki-sc-rules` SANS aucune limite de ressources — un conteneur non plafonné dans un pod plafonné annule le plafond. Éteint (le ruler est à 0 réplique). Closes #38 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 <[email protected]>Trois faits mesurés au déploiement, qu'aucune lecture du chart n'aurait donnés. 1. LE CLUSTER TOURNE SOUS DOCKER, PAS SOUS CONTAINERD. `kubectl get nodes -o …containerRuntimeVersion` : pi1 docker://29.1.3 pi2 docker://28.4.0 pi3 docker://27.4.0 Le commentaire du premier jet affirmait le contraire. Il était faux. 2. LES JOURNAUX NE SONT PAS SOUS /var/log — CE SONT DES LIENS. /var/log/pods/<ns>_<pod>_<uid>/<conteneur>/0.log -> /mnt/arcodange/docker/containers/<id>/<id>-json.log La racine Docker est déplacée sur un disque externe. Monter /var/log donne les liens et PAS leurs cibles : `ls` les montre, `stat` échoue dessus. Le drapeau `dockercontainers` du chart monterait /var/lib/docker/containers, qui n'existe pas ici. On monte la vraie racine, en lecture seule. Chemin vérifié identique sur pi1 et pi3, pod par pod. 3. LE FORMAT EST DU JSON DOCKER, DONC `stage.docker`, PAS `stage.cri`. Avec le mauvais décodeur, l'horodatage stocké serait celui de la LECTURE et non celui de l'écriture — or dater une panne à la minute est précisément l'usage qui motive ce lot (tools#36). ⚠⚠ CE QUI REND CE DÉFAUT DANGEREUX : IL SE PRÉSENTE COMME UNE SANTÉ PARFAITE. Les six composants d'Alloy se déclaraient `healthy`, aucune erreur de poussée, Loki répondait `ready`, les trois agents étaient 2/2 Running. La seule chose qui parlait était `loki_source_file_files_active_total = 0` et l'export vide de `local.file_match`. Une pile de journaux qui rassure sans rien garder : le défaut même que l'issue #38 demande de ne pas reproduire. Le hostPath est en `type: Directory` et non `DirectoryOrCreate`, exprès — si un nœud n'a pas ce chemin, son agent doit refuser de démarrer et le DIRE, plutôt que de monter un dossier vide et de rapporter zéro en se déclarant sain. Corrige aussi le second conteneur non plafonné, jumeau de celui de Loki : `config-reloader` arrive avec une REQUÊTE de 50Mi et AUCUNE limite. Une requête sans limite n'est pas un plafond, c'est une réservation — et sur un DaemonSet cela vaut sur les trois nœuds. On ne peut pas l'éteindre sans perdre le rechargement à chaud : on le plafonne (32Mi / 64Mi). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 <[email protected]>__meta_kubernetes_pod_node_name— un relabel dont la source est vide ne dit rienfromdu schéma n'est pas la date d'installation — un 500 emportait les lignes fraîchesC'est le défaut le plus coûteux de ce lot, et celui qui ressemblait le moins à ce qu'il était. SYMPTÔME : `kubectl logs` montrait 19 lignes du pod témoin, Loki en montrait 0. L'agent se déclarait sain, ses six composants `healthy`, 74 fichiers activement suivis, le journal disait « start tailing file » sur le bon chemin, et TOUS ses compteurs `loki_write_dropped_entries_total` valaient zéro — quelle que soit la raison (ingester_error, line_too_long, queue_is_full, rate_limited, stream_limited). Seul `loki_write_sent_entries_total = 0` disait qu'il ne passait rien, sans dire pourquoi. CAUSE, lisible uniquement dans le journal de Loki : POST /loki/api/v1/push (500) failed to create stream: no schema config found for time 1786622254.804 entry ... has timestamp too old: 2026-04-13T13:17:53Z `schemaConfig.configs[0].from` était posé à la date du jour. Or `from` dit « à partir de quand ce schéma s'applique », PAS « depuis quand on garde ». Sous cette date, Loki n'a aucun schéma pour les horodatages antérieurs. Et l'agent lit aussi les fichiers ROTATÉS encore sur le disque (4.log, 9.log, 10.log…), qui portent des lignes vieilles de plusieurs mois — l'une de 2026-04-13. ⚠ Une poussée étant un LOT, le 500 emporte les lignes FRAÎCHES qui voyageaient avec les vieilles. D'où : rien n'arrive, et rien ne l'explique côté agent. La date passe à 2020-01-01, antérieure à tout ce que ces machines peuvent porter. Ce qui borne la conservation reste `retention_period` (720 h) et le compacteur, pas le schéma. ⚠ À NOTER, PARCE QUE C'EST L'ISSUE #38 EN ABYME : cette cause n'était lisible que dans le journal du pod Loki — qu'on ne pouvait lire que parce qu'il n'avait pas redémarré. Le défaut qui empêchait de garder les journaux ne se diagnostiquait que par un journal non gardé. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5.1 <[email protected]>reject_old_samples_max_agen'est pas la rétention — un lot refusé bloque la file derrière luiLa pile tourne. Voici ce qui est mesuré, et ce qui ne l'est pas.
La pile a été déployée à la main sur le cluster (
helm installdepuis ces mêmes répertoires — donc exactement ce qu'ArgoCD rendra après fusion) pour pouvoir mesurer et éprouver. Rien n'a été appliqué avec Tofu, rien n'a touché Vault.1. La propriété de #38 : un message survit-il au pod qui l'a écrit ?
Oui. Vérifié, code de sortie 0.
Le protocole, et pourquoi chaque pas y est :
⚠ Le pas 4 est celui qu'on est tenté de sauter. Sans lui on démontrerait seulement que Loki sait lire un pod vivant — ce que
kubectl logsfait déjà, et qui n'est pas la propriété qu'on achète. Le test refuse de conclure tant quekubectl logsrépond encore.2. L'empreinte, avant / après — pi2 compris
Séries d'échantillons de
kubectl top nodes(30 s d'intervalle), pas un instant unique.Oui, l'ajout pousse pi2 plus haut : de 108,1 % à 110,7 % de son allouable. Je ne l'enjolive pas. Deux nuances mesurées, qui ne l'effacent pas :
nodeSelectorsi vous le voulez ; je ne l'ai pas fait de moi-même, parce qu'épingler un nœud dans un chart est le genre de chose qui pourrit.Par conteneur, à l'état stabilisé :
lokialloy(pi1)alloy(pi2)alloy(pi3)config-reloader(×3)Zéro redémarrage, zéro OOMKill sur les quatre pods.
⚠ Le plafond de l'agent a dû être relevé de 256 à 384 Mi, et c'est une mesure qui l'a dit : l'agent de pi1 est monté à 303 Mi — au-dessus des 256 Mi du premier jet. Il aurait été tué. Relever un plafond ne consomme rien (c'est la requête, 128 Mi, que le planificateur réserve), donc rien n'est retiré à pi2.
Disque : le volume Longhorn de Loki porte 468 Mio pour 1 273 677 lignes ingérées — l'historique de reprise inclus. Sur 10 Gi demandés.
3. La table des sabotages
helm templatecode 0schemaConfig.configsremplacé par une chaînehelm templatecode 0loki/values.yamlcannot load values.yaml: … line 236Application charts lokiéchoueHelm templatefailure ;alloy,chart,grafanarestent success⚠⚠ Le sabotage A ne mord pas, et c'est le résultat le plus important de cette table
helm templatea rendu 0 sur unschemaConfigcorrompu. Autrement dit : la CI de ce dépôt prouve que le chart SE REND, elle ne prouve rien de ce que Loki fera de la configuration produite. Un vert ici n'est pas un vert fonctionnel.Ce n'est pas une hypothèse — c'est arrivé deux fois dans ce lot, et les deux fois la CI était verte :
admin_api_directory— accepté par le chart, refusé par le binaire OSS (champ d'édition entreprise) → CrashLoopBackOff ;schemaConfig.fromà la date du jour →POST /push (500), plus rien n'entre.Je n'ai pas ajouté de gate qui démarrerait vraiment Loki sur la config rendue. C'est le manque le plus sérieux de cette PR, et je préfère le nommer que le maquiller.
4. Les cinq défauts trouvés en déployant, qu'aucune lecture n'aurait donnés
admin_api_directoryrefusé par le binaireloki-sc-rulessans aucune limitekubectl get pod -o jsonpathsur lesresourcesconfig-reloaderd'Alloy avec une requête mais pas de limite/mnt/arcodange/dockerls -l: les journaux sont des liens ;statéchoue sur la cibleschemaConfig.from+reject_old_samples_max_age⚠⚠ Le défaut 4 est le plus traître de tous : tout avait l'air sain. Les six composants d'Alloy
healthy, aucune erreur de poussée, Lokiready, les trois agents2/2 Running. Le seul relevé qui parlait étaitloki_source_file_files_active_total = 0et l'export vide delocal.file_match. Une pile de journaux qui rassure sans rien garder — précisément ce que #38 demande de ne pas reproduire.⚠ Et le défaut 5 est l'issue #38 en abyme : sa cause n'était lisible que dans le journal du pod Loki, qu'on ne pouvait lire que parce qu'il n'avait pas redémarré.
5. Grafana
La source de données n'est pas encore déployée (la ConfigMap vient de
main). Ce qui est vérifié, depuis le pod Grafana lui-même, à l'URL exacte que déclare la source :14 namespaces, dont
kadans— l'application dont la panne muette a motivé ce lot.6. CI — vérifiée à l'API, contextes présents et nommés
Tête
bb14dc38, statut combinésuccess, 13 contextes présents (aucun absent) :Detect changed chartsApplication charts lokiApplication charts alloyApplication charts chartApplication charts grafanaLibrary charts toolLes
skippedsont le comportement voulu : ces charts ne sont pas touchés. Les quatre qui le sont ont tourné, pas été sautés — et le sabotage B a prouvé qu'ils savent rougir.⚠
mainest rouge indépendamment de cette PR (run 6730, têtedc48d48, la fusion de #39). Ce n'est pas causé par ce lot.7. Ce que je n'ai PAS vérifié — à lire avant de fusionner
helm installdepuis les mêmes répertoires ; je n'ai pas vu ArgoCD adopter les releases existantes. L'adoption devrait se faire, mais je ne l'ai pas constatée.retention_enabled: trueetretention_period: 720hsont dans la config rendue, mais aucune donnée n'a encore 30 jours ici : je n'ai jamais vu le compacteur supprimer quoi que ce soit. La propriété « le volume ne grossit pas indéfiniment » reste non démontrée./mnt/arcodange/docker/containersest vérifié sur pi1 et pi3, pod par pod. Sur pi2 je l'ai déduit du fait que son agent collecte — je n'ai pas lu le lien moi-même sur ce nœud.rollout restart. Je n'ai pas cherché pourquoi.🤖 Generated with Claude Code
Correction du point 7 de la liste « non vérifié »
Je l'avais listé comme déduit ; il est maintenant lu directement sur pi2, comme sur les deux autres :
Le chemin
/mnt/arcodange/docker/containersest donc vérifié pod par pod sur les trois nœuds, pas extrapolé. Les huit autres points de la liste restent valables tels quels.État final laissé sur le cluster
loki-0alloy-hxrmfalloy-29srralloy-2kgf6Le pod jetable
temoin-journalest supprimé. La branche de sabotagearcodange/loki-sabotage-ciet sa PR #41 sont fermées et supprimées.⚠ La pile a été installée à la main et reste en service — elle collecte dès maintenant. À la fusion, ArgoCD devrait adopter les deux releases Helm existantes plutôt que d'en créer de nouvelles ; c'est le point 2 de la liste des non-vérifiés, et il vaut la peine d'y jeter un œil au premier
sync.🤖 Generated with Claude Code
⚠ Correction des chiffres d'empreinte — j'ai publié une série INCOMPLÈTE
La série « après » du premier commentaire a été lue alors qu'elle tournait encore : n=7 sur 14. La série complète est arrivée depuis, et elle déplace tous les chiffres vers le haut. Comme l'écart va dans le sens qui minore le coût, et sur le nœud que je signale précisément comme tendu, il faut le corriger plutôt que le laisser.
Le tableau avant / après qui fait foi
Soit ~645 Mio ajoutés sur l'ensemble du cluster, et pi2 passe de 108,1 % à 111,3 % de son allouable — +3,2 points, pas +2,6 comme je l'avais écrit.
Le reste de l'analyse ne bouge pas : le pourcentage porte sur l'allouable (5772 Mi) et non sur la capacité (7820 Mi), donc pi2 conserve ~1,4 Gio de marge physique, et il n'y a eu aucun OOMKill ni redémarrage. Mais le coût réel sur le nœud tendu est un peu plus élevé que ce que j'ai annoncé, et c'est le chiffre
n=14qui vaut.⚠ La leçon vaut d'être notée : lire une série qui tourne encore, c'est publier une moyenne partielle sans le dire. Les min/max ci-dessus montrent que pi1 varie de 333 Mi d'un échantillon à l'autre — une fenêtre courte n'y suffit pas.
🤖 Generated with Claude Code