Helm Charts / Detect changed charts (push) Successful in 6s
Helm Charts / Library charts tool (push) Skipped
Helm Charts / Application charts alloy (push) Skipped
Helm Charts / Application charts chart (push) Skipped
Helm Charts / Application charts crowdsec (push) Skipped
Helm Charts / Application charts grafana (push) Skipped
Helm Charts / Application charts hashicorp-vault (push) Skipped
Helm Charts / Application charts minio (push) Skipped
Helm Charts / Application charts pgbouncer (push) Skipped
Helm Charts / Application charts pgcat (push) Skipped
Helm Charts / Application charts prometheus (push) Skipped
Helm Charts / Application charts redis (push) Skipped
Helm Charts / Application charts loki (push) Successful in 46s
Un startupProbe patient tient le rejeu du WAL, la readinessProbe reste stricte ensuite. 30 échecs en 25 min sur un Loki parfaitement sain, faute d'une seconde — et sans endpoint, la source de données Grafana était inutilisable. Traite le point 2 de #44. Co-Authored-By: Claude Opus 5 <[email protected]> Co-authored-by: Gabriel Radureau <[email protected]>
329 lines
15 KiB
YAML
329 lines
15 KiB
YAML
# -----------------------------------------------------------------------------
|
||
# 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:
|
||
# ── LES SONDES, RÉGLÉES POUR UN RASPBERRY PI (tools#44) ──────────────────
|
||
#
|
||
# ⚠⚠ MESURÉ le 2026-09-07, quelques heures après le premier déploiement :
|
||
# **30 échecs de la sonde de disponibilité en 25 minutes**, tous en
|
||
# `context deadline exceeded` — pendant que Loki tournait parfaitement. Il
|
||
# finissait de rejouer son WAL (`checkpoint done time=4m23s`) sur un nœud
|
||
# tendu. Le défaut amont est `timeoutSeconds: 1` : **une seconde** pour
|
||
# répondre, ce qui est raisonnable sur une machine rapide et intenable ici.
|
||
#
|
||
# Conséquence, et elle n'est pas cosmétique : sans sonde verte, **aucun
|
||
# endpoint derrière le service**, donc la source de données Grafana est
|
||
# inutilisable — un magasin de journaux vivant mais injoignable.
|
||
#
|
||
# ⚠ LE REMÈDE N'EST PAS D'ASSOUPLIR LA DISPONIBILITÉ. On sépare deux
|
||
# questions que le défaut amont confondait :
|
||
# · « Loki a-t-il fini de DÉMARRER ? » → `startupProbe`, patiente et
|
||
# généreuse (jusqu'à 10 min : c'est le rejeu du WAL qu'on attend).
|
||
# · « Loki répond-il MAL ? » → `readinessProbe`, qui reste stricte et
|
||
# ne prend le relais qu'une fois le démarrage acquis.
|
||
# Un délai de 5 s y remplace la seconde amont : c'est le temps qu'un Pi
|
||
# chargé met à servir `/ready`, pas une tolérance à la panne.
|
||
startupProbe:
|
||
httpGet:
|
||
path: /ready
|
||
port: http-metrics
|
||
periodSeconds: 10
|
||
failureThreshold: 60 # 10 min pour rejouer un WAL, puis on abandonne
|
||
timeoutSeconds: 5
|
||
readinessProbe:
|
||
httpGet:
|
||
path: /ready
|
||
port: http-metrics
|
||
periodSeconds: 10
|
||
successThreshold: 1
|
||
failureThreshold: 3
|
||
timeoutSeconds: 5
|
||
# ⚠ `initialDelaySeconds: 0` est ÉCRIT, pas omis. Le chart FUSIONNE cette
|
||
# table avec la sienne au lieu de la remplacer : l'omettre laissait
|
||
# passer le `15` amont — vérifié au `helm template`, qui rendait encore
|
||
# `initialDelaySeconds: 15` sur un premier jet où je croyais l'avoir
|
||
# retiré. C'est le `startupProbe` qui tient la phase de démarrage ;
|
||
# attendre en plus ici ferait patienter deux fois et masquerait laquelle
|
||
# des deux sondes a réellement gardé quoi.
|
||
initialDelaySeconds: 0
|
||
|
||
# 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.
|
||
#
|
||
# ⚠⚠ 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: "2020-01-01"
|
||
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
|
||
# ⚠⚠ 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: 8760h
|
||
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.
|
||
# -----------------------------------------------------------------------------
|