From b66e79aadca40b95f73d92cf1338daedb31c0df5 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Mon, 7 Sep 2026 17:19:20 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Ce=20qu'un=20pod=20a=20dit=20lui=20survit?= =?UTF-8?q?=20=E2=80=94=20Loki=20et=20Alloy,=20sous=20plafond=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitea/workflows/helmcharts.yaml | 4 +- alloy/.helmignore | 23 +++ alloy/Chart.yaml | 16 +++ alloy/values.yaml | 175 +++++++++++++++++++++++ chart/values.yaml | 6 + grafana/values.yaml | 15 ++ loki/.helmignore | 23 +++ loki/Chart.yaml | 35 +++++ loki/values.yaml | 234 +++++++++++++++++++++++++++++++ 9 files changed, 529 insertions(+), 2 deletions(-) create mode 100644 alloy/.helmignore create mode 100644 alloy/Chart.yaml create mode 100644 alloy/values.yaml create mode 100644 loki/.helmignore create mode 100644 loki/Chart.yaml create mode 100644 loki/values.yaml diff --git a/.gitea/workflows/helmcharts.yaml b/.gitea/workflows/helmcharts.yaml index 03d3ec5..1f92c39 100644 --- a/.gitea/workflows/helmcharts.yaml +++ b/.gitea/workflows/helmcharts.yaml @@ -293,9 +293,9 @@ jobs: needs: [filter-chart,library-charts] strategy: matrix: - # ⚠ Les NEUF charts applicatifs du dépôt — voir l'avertissement du job + # ⚠ Les ONZE charts applicatifs du dépôt — voir l'avertissement du job # `library-charts` ci-dessus. Un chart absent de cette liste n'est # JAMAIS construit, et sa PR est verte quand même. # chart: ${{ fromJson(needs.filter-chart.outputs.application_charts) }} - chart: [chart, crowdsec, grafana, hashicorp-vault, minio, pgbouncer, pgcat, prometheus, redis] + chart: [alloy, chart, crowdsec, grafana, hashicorp-vault, loki, minio, pgbouncer, pgcat, prometheus, redis] type: [application] \ No newline at end of file diff --git a/alloy/.helmignore b/alloy/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/alloy/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/alloy/Chart.yaml b/alloy/Chart.yaml new file mode 100644 index 0000000..a650593 --- /dev/null +++ b/alloy/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +name: alloy +description: >- + L'agent qui ramasse les journaux de chaque nœud et les pousse vers Loki. + Un DaemonSet — donc trois pods, un par Raspberry Pi. + +# Même raison que pour `loki/` : aucune dépendance au registre Helm interne, +# que le runner de CI ne résout pas (relevé du 2026-09-07). Voir loki/Chart.yaml. +dependencies: + - name: alloy + version: 1.12.1 + repository: https://grafana.github.io/helm-charts + +type: application +version: 0.1.0 +appVersion: "v1.19.2" diff --git a/alloy/values.yaml b/alloy/values.yaml new file mode 100644 index 0000000..1b25cac --- /dev/null +++ b/alloy/values.yaml @@ -0,0 +1,175 @@ +# ----------------------------------------------------------------------------- +# Grafana Alloy — l'agent de collecte, un pod par nœud. +# ----------------------------------------------------------------------------- +# POURQUOI ALLOY ET PAS PROMTAIL (l'arbitrage de tools#38, mesuré le 2026-09-07) +# +# Relevé sur l'index du dépôt 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 « en fin de vie bientôt » : son chart porte le drapeau +# `deprecated: true` dans l'index, et sa dernière publication a plus de dix mois. +# Déployer AUJOURD'HUI, sur une installation NEUVE, un agent que l'amont a déjà +# rangé, c'est se donner une dette à échéance connue pour économiser de la +# mémoire — et l'économie, elle, est à vérifier, pas à supposer. +# +# Le coût réel d'Alloy sur CE matériel est mesuré après déploiement et reporté +# dans la PR ; c'est ce que le plafond `resources` ci-dessous encadre. +# +# ARM64 : vérifié au registre, manifeste multi-plateforme (jeton anonyme +# Docker Hub) — `grafana/alloy:v1.19.2` publie linux/amd64, **linux/arm64**, +# ppc64le, s390x. `grafana/loki:3.6.12` publie amd64, **arm64**, arm/v7. +# ----------------------------------------------------------------------------- +alloy: + controller: + # C'est déjà le défaut du chart ; on l'écrit parce que c'est LA décision + # structurante — un agent par nœud, donc trois fois le coût mémoire. + type: daemonset + + alloy: + # -- Le montage de /var/log : c'est par là que l'agent lit les journaux. + # On tail des FICHIERS plutôt que d'interroger l'API du kubelet : sur trois + # Pi, faire transiter tous les journaux par l'API du cluster ajouterait au + # serveur d'API une charge qu'il n'a pas à porter. + mounts: + varlog: true + # k3s utilise containerd, pas Docker : ce montage-là ne servirait à rien. + dockercontainers: false + + # ⚠ LA MÉMOIRE EST LA RESSOURCE RARE, ET UN DAEMONSET LA PREND TROIS FOIS. + # Mesuré le 2026-09-07 avant tout déploiement : pi1 79 %, pi2 109 %, + # pi3 58 % de leur mémoire ALLOUABLE. pi2 est en surengagement — c'est + # précisément le nœud où un agent de trop bascule la machine. + # + # Le plafond n'est donc pas une formalité : il est ce qui garantit que + # l'agent meurt AVANT le service qu'il observe. Un OOM d'Alloy est un + # incident de collecte ; un OOM de MinIO ou de pgbouncer est un incident + # tout court. + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + # Pas de limite CPU (l'étranglement d'un agent lui fait perdre des + # lignes en silence, ce qui est le défaut qu'on essaie de supprimer). + memory: 256Mi + + # Pas de grappe : chaque agent lit SON nœud, il n'a rien à coordonner. + clustering: + enabled: false + + configMap: + create: true + # ----------------------------------------------------------------------- + # La configuration Alloy. Trois étages : découvrir, lire, pousser. + # ----------------------------------------------------------------------- + content: |- + // ÉTAGE 1 — découvrir les pods de CE nœud, et de lui seul. + // + // ⚠ Sans le filtre par nœud, chacun des trois agents découvrirait les + // pods des TROIS nœuds, chercherait leurs fichiers en local, n'en + // trouverait aucun pour les deux autres, et referait ce travail en + // boucle. Le filtre se pose côté serveur d'API (`field`), pas en + // relabel : ce qui n'est pas envoyé ne coûte rien. + discovery.kubernetes "pods_du_noeud" { + role = "pod" + + selectors { + role = "pod" + field = "spec.nodeName=" + sys.env("HOSTNAME") + } + } + + // ÉTAGE 2 — traduire les métadonnées Kubernetes en étiquettes Loki, + // et fabriquer le chemin du fichier journal. + // + // ⚠ LES ÉTIQUETTES SONT LE SEUL INDEX DE LOKI, ET CHAQUE COMBINAISON + // DISTINCTE EST UN FLUX. On garde ce par quoi on cherche vraiment + // (namespace, pod, conteneur, nœud) et RIEN de ce qui varie sans + // qu'on l'interroge : mettre ici une étiquette à forte cardinalité + // (un identifiant de requête, un horodatage) est la façon classique + // de faire tomber un Loki. Le reste du contenu reste cherchable en + // plein texte, simplement non indexé. + discovery.relabel "journaux_de_pods" { + targets = discovery.kubernetes.pods_du_noeud.targets + + rule { + source_labels = ["__meta_kubernetes_namespace"] + target_label = "namespace" + } + rule { + source_labels = ["__meta_kubernetes_pod_name"] + target_label = "pod" + } + rule { + source_labels = ["__meta_kubernetes_pod_container_name"] + target_label = "container" + } + rule { + source_labels = ["__meta_kubernetes_node_name"] + target_label = "node" + } + rule { + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"] + target_label = "app" + } + + // Le chemin réel posé par le kubelet : + // /var/log/pods/__//0.log + // On vise par l'UID (unique) et le nom du conteneur. + rule { + source_labels = ["__meta_kubernetes_pod_uid", "__meta_kubernetes_pod_container_name"] + separator = "/" + action = "replace" + replacement = "/var/log/pods/*$1/*.log" + target_label = "__path__" + } + } + + // ÉTAGE 3 — lire les fichiers, décoder le format CRI, pousser. + local.file_match "journaux_de_pods" { + path_targets = discovery.relabel.journaux_de_pods.output + } + + loki.source.file "journaux_de_pods" { + targets = local.file_match.journaux_de_pods.targets + forward_to = [loki.process.journaux_de_pods.receiver] + } + + // ⚠ containerd n'écrit PAS du JSON de Docker : chaque ligne est + // « ». Sans `stage.cri`, l'horodatage + // stocké serait celui de la LECTURE et non celui de l'écriture — ce qui + // ruinerait précisément l'usage qui motive ce lot : dater une panne. + loki.process "journaux_de_pods" { + stage.cri {} + + forward_to = [loki.write.loki.receiver] + } + + loki.write "loki" { + endpoint { + url = "http://loki.tools.svc.cluster.local:3100/loki/api/v1/push" + } + } + + # L'agent a besoin de LIRE les pods du serveur d'API pour les découvrir. + # Le chart pose un ClusterRole en lecture seule (pods, nodes, services, + # endpoints) — rien qui écrive. + rbac: + create: true + + serviceAccount: + create: true + + # Pas de collecte de métriques d'Alloy par Prometheus pour l'instant : le + # ServiceMonitor suppose l'opérateur Prometheus, que ce cluster n'a pas + # (le chart `prometheus` d'ici est le chart communautaire, pas l'opérateur). + serviceMonitor: + enabled: false + + ingress: + enabled: false diff --git a/chart/values.yaml b/chart/values.yaml index e6be51b..8f1c18f 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -10,3 +10,9 @@ tools: plausible: {} prometheus: {} minio: {} + # La collecte de journaux (tools#38). Deux applications parce que ce sont + # deux charts amont, et parce qu'on redémarre l'agent sans toucher au + # magasin — mais elles ne valent rien l'une sans l'autre : `alloy` pousse + # vers le service que `loki` publie. + loki: {} + alloy: {} diff --git a/grafana/values.yaml b/grafana/values.yaml index 0b48119..7364f14 100644 --- a/grafana/values.yaml +++ b/grafana/values.yaml @@ -573,6 +573,21 @@ grafana: &grafana_config url: http://prometheus-server.tools.svc.cluster.local access: proxy isDefault: true + # Les journaux du cluster (tools#38). Le chart `loki` d'à côté déploie + # le binaire unique ; `alloy` y pousse ce que disent les pods. + # ⚠ `loki.auth_enabled: false` côté serveur : aucun en-tête + # X-Scope-OrgID à porter ici. Si l'authentification multi-locataire + # était rallumée, cette source de données cesserait de répondre et il + # faudrait lui ajouter `httpHeaderName1: X-Scope-OrgID`. + - name: Loki + type: loki + url: http://loki.tools.svc.cluster.local:3100 + access: proxy + isDefault: false + jsonData: + # La rétention posée dans loki/values.yaml. Grafana s'en sert pour + # ne pas proposer une fenêtre que le magasin ne peut plus servir. + maxLines: 1000 # - name: CloudWatch # type: cloudwatch # access: proxy diff --git a/loki/.helmignore b/loki/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/loki/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/loki/Chart.yaml b/loki/Chart.yaml new file mode 100644 index 0000000..86bc8ff --- /dev/null +++ b/loki/Chart.yaml @@ -0,0 +1,35 @@ +apiVersion: v2 +name: loki +description: >- + Le garde-mémoire des journaux du cluster — Loki en binaire unique, stockage + fichier sur Longhorn. Ce qu'un pod a dit continue de se lire après sa mort. + +# ⚠ PAS de dépendance au registre Helm interne, CONTRAIREMENT à grafana et +# prometheus — et c'est délibéré. +# +# La bibliothèque `tool` ne sert QUE via les gabarits `templates/helm-chart*.yaml`, +# eux-mêmes gardés par `{{- if eq .Values.tool.kind "HelmChart" -}}`. Avec +# `kind: SubChart` — le mode de grafana, de prometheus, et le nôtre — ces +# gabarits ne rendent RIEN : la bibliothèque est un poids mort. +# +# Or ce poids mort a un coût mesuré : le runner de CI ne résout pas +# `gitea.arcodange.lab` (relevé du 2026-09-07, run 6728, `curl` code 6). Un +# repli sur `GITHUB_SERVER_URL` a été posé dans `helmcharts.yaml`, mais il reste +# un point de panne EN AMONT du `helm template`. Si la dépendance ne se +# télécharge pas, l'étape qui éprouve VRAIMENT ce chart ne s'exécute jamais et +# le job meurt sur une cause étrangère au chart. +# +# On ne dépend donc que de `grafana.github.io`, dont le même relevé a constaté +# qu'il répond depuis ce runner. Le jour où l'on veut basculer en +# `kind: HelmChart` (CRD k3s), on rajoute la dépendance ET les deux gabarits. +dependencies: + - name: loki + version: 7.3.0 + repository: https://grafana.github.io/helm-charts + +type: application +version: 0.1.0 +# ⚠ 3.6.11, PAS 3.6.12. L'index Helm annonce `appVersion: 3.6.12` pour le chart +# 7.3.0, mais le chart épingle `loki.image.tag: 3.6.11` dans ses valeurs — et +# c'est le tag qui est tiré. Vérifié sur le pod déployé. +appVersion: "3.6.11" diff --git a/loki/values.yaml b/loki/values.yaml new file mode 100644 index 0000000..0f96b83 --- /dev/null +++ b/loki/values.yaml @@ -0,0 +1,234 @@ +# ----------------------------------------------------------------------------- +# Loki — binaire unique, stockage fichier, sur trois Raspberry Pi. +# ----------------------------------------------------------------------------- +# ⚠⚠ LES DÉFAUTS AMONT SONT ÉCRITS POUR UN CLUSTER DE SERVEURS, PAS POUR CECI. +# Mesurés sur le chart 7.3.0 (`helm show values grafana/loki --version 7.3.0`) : +# +# deploymentMode SimpleScalable → read + write + backend, 3 jeux +# chunksCache.enabled true → un memcached à … +# chunksCache.allocatedMemory 8192 → … 8 Gio, sur un nœud qui en +# alloue 5,6 (pi2) +# resultsCache.enabled true → un second memcached +# gateway.enabled true → un nginx de plus +# lokiCanary.enabled true → un DaemonSet de plus +# test.enabled true → un pod de test +# loki.commonConfig.replication_factor 3 → 3 exemplaires de chaque flux +# loki.storage.type s3 → un magasin objet qu'on n'a pas +# loki.auth_enabled true → multi-locataire (en-tête +# X-Scope-OrgID exigé à chaque appel) +# +# Laisser ces défauts, c'est exactement la deuxième propriété de tools#38 +# violée : « la collecte ne doit pas faire tomber ce qu'elle observe ». Chaque +# `false` ci-dessous est donc une décision, pas une omission. +# ----------------------------------------------------------------------------- +loki: + # -- Un seul processus, une seule réplique. Le reste des modes est éteint plus bas. + deploymentMode: SingleBinary + + loki: + # Locataire unique : personne d'autre que le homelab n'écrit ici. Laisser + # `true` obligerait chaque requête (et chaque source de données Grafana) à + # porter un en-tête X-Scope-OrgID, pour aucun bénéfice. + auth_enabled: false + + commonConfig: + path_prefix: /var/loki + # -- UNE réplique de chaque flux : il n'y a qu'un ingesteur. + # Le défaut amont (3) ferait refuser toute écriture, faute de quorum. + replication_factor: 1 + + storage: + type: filesystem + filesystem: + chunks_directory: /var/loki/chunks + rules_directory: /var/loki/rules + # ⚠ PAS de `admin_api_directory` ici. Le chart l'accepte (il est dans + # ses valeurs par défaut), mais le binaire OSS le REFUSE : + # failed parsing config: field admin_api_directory not found in + # type common.FilesystemConfig + # C'est un champ de l'édition entreprise (GEL). Mesuré : le pod part en + # 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 n'écrit AUCUN schéma par défaut (`schemaConfig: {}`) et refuse + # de démarrer sans. tsdb + v13 est le couple courant de Loki 3.x. + schemaConfig: + configs: + - from: "2026-09-07" + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: loki_index_ + period: 24h + + # ⚠⚠ UNE RÉTENTION QUI NE SUPPRIME RIEN EST UNE RÉTENTION QUI MENT. + # `limits_config.retention_period` seul ne fait RIEN : c'est le compacteur + # qui efface, et il ne le fait que si `retention_enabled` est vrai. Le + # couple est indissociable — l'un sans l'autre laisse le volume grossir + # jusqu'à saturation, en silence. + compactor: + working_directory: /var/loki/compactor + retention_enabled: true + delete_request_store: filesystem + + limits_config: + # -- 30 jours. Voir `RETENTION` en fin de fichier pour la mesure. + retention_period: 720h + # Le défaut amont refuse tout échantillon de plus de 168 h. À la première + # remontée d'un agent resté hors ligne, ces lignes seraient jetées. + reject_old_samples: true + reject_old_samples_max_age: 720h + max_cache_freshness_per_query: 10m + split_queries_by_interval: 15m + query_timeout: 300s + volume_enabled: true + # Débit mesuré du cluster : ~305 Mio/jour TOUS pods confondus, soit + # ~3,6 Kio/s. Les bornes amont (4 Mio/s par flux) sont hors sujet ici ; + # on les laisse, elles ne mordent pas. + allow_structured_metadata: true + + # Pas de vitrine : personne d'extérieur n'interroge ce Loki. + analytics: + reporting_enabled: false + + singleBinary: + replicas: 1 + persistence: + enabled: true + # ⚠ DEUX StorageClass portent le drapeau `(default)` sur ce cluster + # (`local-path` ET `longhorn`, mesuré le 2026-09-07). Un PVC sans classe + # explicite est donc un tirage au sort — et `local-path` ne survit pas au + # déplacement du pod, ce qui viderait la mémoire qu'on vient de bâtir. + # On NOMME la classe. + storageClass: longhorn + # 30 j × 305 Mio/j = 8,9 Gio BRUTS ; Loki compresse (facteur 5 à 10 sur + # du journal), donc 0,9 à 1,8 Gio attendus. 10 Gi laisse 5 à 10× de marge + # pour la croissance de kadans, sans immobiliser le disque. + size: 10Gi + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + # Pas de limite CPU : l'étranglement d'un compacteur est pire que sa + # lenteur. La mémoire, elle, est PLAFONNÉE — c'est la ressource rare + # (pi2 mesuré à 109 % de son allouable le 2026-09-07). + memory: 512Mi + + # --------------------------------------------------------------------------- + # Tout ce qui suit existe pour un déploiement distribué. Ici, chacun de ces + # blocs est du poids mort sur des Raspberry Pi — on les éteint NOMMÉMENT + # plutôt que par un `deploymentMode` qui « devrait » suffire. + # --------------------------------------------------------------------------- + write: { replicas: 0 } + read: { replicas: 0 } + backend: { replicas: 0 } + ingester: { replicas: 0 } + distributor: { replicas: 0 } + querier: { replicas: 0 } + queryFrontend: { replicas: 0 } + queryScheduler: { replicas: 0 } + indexGateway: { replicas: 0 } + compactor: { replicas: 0 } + bloomPlanner: { replicas: 0 } + bloomBuilder: { replicas: 0 } + bloomGateway: { replicas: 0 } + patternIngester: { replicas: 0 } + ruler: { replicas: 0 } + overridesExporter: { replicas: 0 } + + # Les deux memcached. Le premier réclame 8 Gio à lui seul par défaut. + chunksCache: + enabled: false + resultsCache: + enabled: false + + # Un nginx devant un service que seul Grafana interroge, depuis le cluster. + gateway: + enabled: false + + # Un DaemonSet qui écrit des journaux pour vérifier qu'on lit les journaux : + # trois pods de plus sur trois nœuds, pour une propriété que le sabotage de + # la PR démontre mieux (écrire une ligne, tuer le pod, la relire). + lokiCanary: + enabled: false + + test: + enabled: false + + # Le chart sait déployer SON PROPRE MinIO. On en a déjà un, et on n'en veut + # pas ici : le stockage est le disque. + minio: + enabled: false + + monitoring: + dashboards: + enabled: false + rules: + enabled: false + serviceMonitor: + enabled: false + metricsInstance: + enabled: false + selfMonitoring: + enabled: false + grafanaAgent: + installOperator: false + + # Pas d'opérateur de déploiement progressif pour une réplique unique. + rollout_operator: + enabled: false + + # ⚠ UN SECOND CONTENEUR SANS PLAFOND ANNULE LE PLAFOND DU PREMIER. + # Le chart ajoute au pod un side-car `loki-sc-rules` (kiwigrid/k8s-sidecar) + # qui surveille des ConfigMaps de règles d'alerte. Mesuré sur le premier + # déploiement : il arrive avec `resources: {}` — ni requête ni limite — dans + # un pod posé sur pi2, le nœud à 108 % de son allouable. On l'éteint : le + # `ruler` est à 0 réplique, il n'y a aucune règle à ingérer, donc ce side-car + # surveille des ConfigMaps qui n'existent pas. + sidecar: + rules: + enabled: false + +# ----------------------------------------------------------------------------- +# RETENTION — l'arbitrage, et la mesure qui le porte +# ----------------------------------------------------------------------------- +# Mesuré le 2026-09-07 sur ce cluster, par l'API (donc ce que Loki ingérerait) : +# +# fenêtre de 10 min, tous pods : 2 182 015 octets +# fenêtre de 1 h, tous pods : 7 897 505 octets +# +# Les deux instruments ne s'accordent pas (l'heure ne vaut que 0,60 fois six +# fois les dix minutes) — et le désaccord est LE résultat. Par conteneur, le +# rapport 1 h / 10 min vaut ~6 partout (débit stable) SAUF : +# +# kube-system/traefik ratio 1,18 → la fenêtre d'une heure est TRONQUÉE +# tools/clickhouse-0 ratio 3,98 → tronquée aussi +# +# Vérifié directement, en demandant à chaque journal jusqu'où il remonte : +# +# traefik pod démarré il y a 12 j — journal remontant à 10 MINUTES +# clickhouse pod démarré il y a 8 j — journal remontant à 2 MINUTES +# +# Autrement dit, les deux composants les plus bavards du cluster gardent entre +# deux et dix minutes d'histoire. C'est l'issue tools#38 en un chiffre. +# +# Estimation corrigée (max(1 h, 10 min × 6) par conteneur, pour ne pas hériter +# de la troncature) : 13 333 645 octets/h = 305 Mio/jour bruts. +# +# 30 j bruts 8,94 Gio +# 30 j compressés (facteur 5) 1,79 Gio +# 30 j compressés (facteur 10) 0,89 Gio +# +# POURQUOI 30 JOURS et pas 7 : les deux pannes qui motivent ce lot ont duré des +# SEMAINES avant d'être vues (la mise à l'abri de kadans#1033) ou trois jours +# (tools#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 », et +# coûte 1 à 2 Gio — sur 99 Gio libres au plus serré des trois nœuds Longhorn. +# +# ⚠ CE QUI RENDRAIT CE CHIFFRE FAUX : la mesure est une photo d'un mardi +# après-midi. Elle ne dit rien d'un pod qui part en boucle d'erreur, cas où le +# débit d'un seul conteneur peut dépasser à lui seul tout le reste du cluster. +# La marge de 5 à 10× est là pour ça, pas pour la croissance ordinaire. +# ----------------------------------------------------------------------------- -- 2.54.0 From c4720a1dfdf7d2fb89328d07be5e13fa5130c09b Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Mon, 7 Sep 2026 17:29:24 +0200 Subject: [PATCH 2/6] =?UTF-8?q?Ces=20n=C5=93uds=20tournent=20sous=20Docker?= =?UTF-8?q?,=20et=20leur=20racine=20est=20ailleurs=20=E2=80=94=20la=20coll?= =?UTF-8?q?ecte=20lisait=20le=20vide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/__//0.log -> /mnt/arcodange/docker/containers//-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 --- alloy/values.yaml | 86 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/alloy/values.yaml b/alloy/values.yaml index 1b25cac..264d2ba 100644 --- a/alloy/values.yaml +++ b/alloy/values.yaml @@ -30,6 +30,20 @@ alloy: # structurante — un agent par nœud, donc trois fois le coût mémoire. type: daemonset + # Le volume que consomme le montage `alloy.mounts.extra` ci-dessous : la + # racine Docker de ces machines, où aboutissent les liens de /var/log/pods. + volumes: + extra: + - name: docker-containers + hostPath: + path: /mnt/arcodange/docker/containers + # `Directory` (et non `DirectoryOrCreate`) EXPRÈS : si un jour un + # nœud n'a pas ce chemin, on veut que son agent refuse de démarrer + # et le DISE, plutôt que de monter un dossier vide et de rapporter + # zéro journal en se déclarant sain — le défaut exact qu'on vient + # de traverser. + type: Directory + alloy: # -- Le montage de /var/log : c'est par là que l'agent lit les journaux. # On tail des FICHIERS plutôt que d'interroger l'API du kubelet : sur trois @@ -37,9 +51,37 @@ alloy: # serveur d'API une charge qu'il n'a pas à porter. mounts: varlog: true - # k3s utilise containerd, pas Docker : ce montage-là ne servirait à rien. + # ⚠⚠ CE CLUSTER TOURNE SOUS DOCKER, PAS SOUS CONTAINERD — et son magasin + # d'images n'est pas là où le chart le cherche. Les deux faits ensemble + # rendent ce drapeau inutilisable. + # + # Mesuré le 2026-09-07 (`kubectl get nodes -o …containerRuntimeVersion`) : + # pi1 docker://29.1.3 + # pi2 docker://28.4.0 + # pi3 docker://27.4.0 + # + # Et sous /var/log/pods, les journaux ne sont PAS des fichiers : ce sont + # des liens symboliques vers la racine Docker, DÉPLACÉE sur un disque + # externe (identique sur pi1 et pi3, vérifié pod par pod) : + # + # /var/log/pods/__//0.log + # -> /mnt/arcodange/docker/containers//-json.log + # + # `dockercontainers: true` monterait `/var/lib/docker/containers`, qui + # n'existe pas ici. On monte donc la vraie racine, plus bas, en `extra`. dockercontainers: false + # ⚠ SANS CE MONTAGE, LA COLLECTE EST SILENCIEUSEMENT VIDE — et c'est le + # mode d'échec le plus traître de ce lot, parce que TOUT A L'AIR SAIN : + # les six composants d'Alloy se déclarent `healthy`, aucune erreur de + # poussée, Loki répond `ready`. Le seul relevé qui le dit est + # `loki_source_file_files_active_total = 0`, et l'export vide de + # `local.file_match`. `ls` montre les liens, `stat` échoue sur la cible. + extra: + - name: docker-containers + mountPath: /mnt/arcodange/docker/containers + readOnly: true + # ⚠ LA MÉMOIRE EST LA RESSOURCE RARE, ET UN DAEMONSET LA PREND TROIS FOIS. # Mesuré le 2026-09-07 avant tout déploiement : pi1 79 %, pi2 109 %, # pi3 58 % de leur mémoire ALLOUABLE. pi2 est en surengagement — c'est @@ -140,12 +182,17 @@ alloy: forward_to = [loki.process.journaux_de_pods.receiver] } - // ⚠ containerd n'écrit PAS du JSON de Docker : chaque ligne est - // « ». Sans `stage.cri`, l'horodatage - // stocké serait celui de la LECTURE et non celui de l'écriture — ce qui - // ruinerait précisément l'usage qui motive ce lot : dater une panne. + // ⚠⚠ `stage.docker`, PAS `stage.cri` — ces nœuds tournent sous Docker + // (mesuré : docker://29.1.3, 28.4.0, 27.4.0), donc chaque ligne est un + // objet JSON {"log":…,"stream":…,"time":…} et non le format CRI + // « ». + // + // Ce n'est pas cosmétique : sans le bon décodeur, l'horodatage stocké + // serait celui de la LECTURE et non celui de l'écriture, et les lignes + // arriveraient encore encapsulées dans leur JSON. Or dater une panne à + // la minute est PRÉCISÉMENT l'usage qui motive ce lot (tools#36). loki.process "journaux_de_pods" { - stage.cri {} + stage.docker {} forward_to = [loki.write.loki.receiver] } @@ -173,3 +220,30 @@ alloy: ingress: enabled: false + + # ⚠⚠ LE MÊME PIÈGE QUE LE SIDE-CAR DE LOKI, DEUXIÈME OCCURRENCE — et celle-ci + # est plus sournoise, parce qu'elle RESSEMBLE à un plafond. + # + # Le chart ajoute au pod un second conteneur, `config-reloader`, qui recharge + # Alloy quand sa ConfigMap change. Mesuré sur le premier déploiement, ses + # ressources amont sont : + # + # resources: + # requests: { cpu: 10m, memory: 50Mi } + # ← et RIEN d'autre + # + # Une requête SANS limite n'est pas un plafond : c'est une réservation. Le + # conteneur peut croître jusqu'à épuiser le nœud, et comme il partage le + # cgroup du pod, il emporte Alloy avec lui. Sur un DaemonSet, cela vaut sur + # les TROIS nœuds — pi2 compris, mesuré à 108 % de son allouable. + # + # ⚠ On ne peut pas l'éteindre : sans lui, un changement de configuration + # n'atteindrait l'agent qu'au prochain redémarrage du pod. On le PLAFONNE. + configReloader: + enabled: true + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + memory: 64Mi -- 2.54.0 From 1abfe7044b5ec158f9811f34b1a9c78e47040f1d Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Mon, 7 Sep 2026 17:34:40 +0200 Subject: [PATCH 3/6] =?UTF-8?q?`=5F=5Fmeta=5Fkubernetes=5Fpod=5Fnode=5Fnam?= =?UTF-8?q?e`=20=E2=80=94=20un=20relabel=20dont=20la=20source=20est=20vide?= =?UTF-8?q?=20ne=20dit=20rien?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- alloy/values.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/alloy/values.yaml b/alloy/values.yaml index 264d2ba..b2b2578 100644 --- a/alloy/values.yaml +++ b/alloy/values.yaml @@ -151,8 +151,15 @@ alloy: source_labels = ["__meta_kubernetes_pod_container_name"] target_label = "container" } + // ⚠ `__meta_kubernetes_POD_node_name`, PAS `__meta_kubernetes_node_name`. + // Le second n'existe QUE pour `role = "node"` ; sous `role = "pod"` il + // est vide, et un relabel dont la source est vide ne pose pas + // l'étiquette — SANS erreur, sans avertissement, sans rien. Mesuré : + // l'étiquette `node` était simplement absente de Loki, sur une + // collecte par ailleurs saine. On ne s'en aperçoit qu'en DEMANDANT + // les valeurs de l'étiquette et en trouvant la liste vide. rule { - source_labels = ["__meta_kubernetes_node_name"] + source_labels = ["__meta_kubernetes_pod_node_name"] target_label = "node" } rule { -- 2.54.0 From 857a6d15608f3c40008cde0178edec5e0bab426b Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Mon, 7 Sep 2026 17:41:14 +0200 Subject: [PATCH 4/6] =?UTF-8?q?La=20date=20`from`=20du=20sch=C3=A9ma=20n'e?= =?UTF-8?q?st=20pas=20la=20date=20d'installation=20=E2=80=94=20un=20500=20?= =?UTF-8?q?emportait=20les=20lignes=20fra=C3=AEches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- loki/values.yaml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/loki/values.yaml b/loki/values.yaml index 0f96b83..eb64d2d 100644 --- a/loki/values.yaml +++ b/loki/values.yaml @@ -52,9 +52,36 @@ loki: # ⚠ Le chart n'écrit AUCUN schéma par défaut (`schemaConfig: {}`) et refuse # de démarrer sans. tsdb + v13 est le couple courant de Loki 3.x. + # + # ⚠⚠ ET LA DATE `from` NE SE MET PAS AU JOUR DE L'INSTALLATION. + # C'est la faute qu'a faite le premier jet, et elle a coûté cher à + # diagnostiquer parce qu'elle ne ressemble à rien de ce qu'elle est. + # + # `from` dit « à partir de quand ce schéma s'applique », pas « depuis quand + # on garde ». Posée à la date du jour, Loki n'a AUCUN schéma pour les + # horodatages antérieurs et refuse la requête d'écriture ENTIÈRE : + # + # POST /loki/api/v1/push (500) + # failed to create stream: no schema config found for time 1786622254 + # + # Or l'agent lit aussi les fichiers ROTATÉS encore présents sur le disque + # (4.log, 9.log, 10.log…), qui contiennent des lignes vieilles de plusieurs + # mois — une remontait à 2026-04-13. ⚠ Et comme une poussée est un LOT, le + # 500 emporte les lignes FRAÎCHES qui voyageaient avec les vieilles. + # + # Symptôme observé : `kubectl logs` montrait 19 lignes du pod témoin, Loki + # en montrait 0, l'agent se déclarait sain, ses compteurs `dropped` étaient + # tous à zéro — et `loki_write_sent_entries_total` valait 0. Rien ne + # DÉSIGNAIT la cause côté agent : elle n'était lisible que dans le journal + # de Loki. (Journal qu'on ne pouvait lire que parce que le pod n'avait pas + # redémarré. L'ironie est le sujet même de l'issue #38.) + # + # La date est donc largement antérieure à tout ce que le cluster peut + # porter. Ce qui borne la conservation, c'est `retention_period` + + # le compacteur, pas ceci. schemaConfig: configs: - - from: "2026-09-07" + - from: "2020-01-01" store: tsdb object_store: filesystem schema: v13 -- 2.54.0 From 698920ec8d60901a0017ad20b879c819c21ada65 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Mon, 7 Sep 2026 17:45:19 +0200 Subject: [PATCH 5/6] =?UTF-8?q?`reject=5Fold=5Fsamples=5Fmax=5Fage`=20n'es?= =?UTF-8?q?t=20pas=20la=20r=C3=A9tention=20=E2=80=94=20un=20lot=20refus?= =?UTF-8?q?=C3=A9=20bloque=20la=20file=20derri=C3=A8re=20lui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- loki/values.yaml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/loki/values.yaml b/loki/values.yaml index eb64d2d..5e1405b 100644 --- a/loki/values.yaml +++ b/loki/values.yaml @@ -102,10 +102,32 @@ loki: limits_config: # -- 30 jours. Voir `RETENTION` en fin de fichier pour la mesure. retention_period: 720h - # Le défaut amont refuse tout échantillon de plus de 168 h. À la première - # remontée d'un agent resté hors ligne, ces lignes seraient jetées. + # ⚠⚠ CE N'EST PAS UN RÉGLAGE DE CONSERVATION, ET LE CONFONDRE BLOQUE TOUT. + # + # Posé à 720 h pour « coller » à la rétention, il a produit un second + # blocage complet, juste après celui du schéma : l'agent lit aussi les + # fichiers rotatés présents sur le disque, dont certains remontent à + # AVRIL. Loki refusait alors chaque lot — + # + # entry ... has timestamp too old: 2026-04-13T13:47:05Z, + # oldest acceptable timestamp is: 2026-08-08T15:41:50Z + # + # — et comme l'agent REJOUE un lot refusé, la file ne se vidait jamais : + # `loki_write_sent_entries_total` restait à 0 pour toujours. Un lot + # définitivement refusé bloque la file derrière lui. + # + # Ce garde-fou existe contre les horloges DÉCALÉES, pas pour borner la + # conservation. On l'ouvre donc largement au-delà de ce que le disque + # peut porter ; ce qui borne vraiment, c'est `retention_period` + le + # compacteur, plus bas. + # + # ⚠ Conséquence assumée : au tout premier démarrage, l'agent remonte + # l'historique encore sur le disque, et le compacteur supprimera dans + # l'heure ce qui dépasse 30 jours. On paie une ingestion inutile UNE + # fois, en échange d'une file qui se vide. Le bénéfice de bord est + # qu'on récupère au passage l'histoire déjà écrite. reject_old_samples: true - reject_old_samples_max_age: 720h + reject_old_samples_max_age: 8760h max_cache_freshness_per_query: 10m split_queries_by_interval: 15m query_timeout: 300s -- 2.54.0 From bb14dc38d58600811a813da6d6f35f5624b69d48 Mon Sep 17 00:00:00 2001 From: Gabriel Radureau Date: Mon, 7 Sep 2026 17:51:01 +0200 Subject: [PATCH 6/6] =?UTF-8?q?Le=20plafond=20de=20l'agent=20passe=20?= =?UTF-8?q?=C3=A0=20384=20Mi=20=E2=80=94=20256=20laissait=2084=20%=20d'occ?= =?UTF-8?q?upation=20sur=20pi1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- alloy/values.yaml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/alloy/values.yaml b/alloy/values.yaml index b2b2578..d767c3f 100644 --- a/alloy/values.yaml +++ b/alloy/values.yaml @@ -98,7 +98,26 @@ alloy: limits: # Pas de limite CPU (l'étranglement d'un agent lui fait perdre des # lignes en silence, ce qui est le défaut qu'on essaie de supprimer). - memory: 256Mi + # + # ⚠ 384 Mi, ET C'EST UN CHIFFRE MESURÉ, PAS UN CHIFFRE ROND. + # Le premier jet plafonnait à 256 Mi. Relevé sur la pile qui tourne, + # pendant la reprise de l'historique (`kubectl top pods --containers`) : + # + # 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. + # + # ⚠ 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. Un nœud qui + # se remplit fera monter son agent. + memory: 384Mi # Pas de grappe : chaque agent lit SON nœud, il n'a rien à coordonner. clustering: -- 2.54.0