Révision des déploiements Thanos
Règles de terrain issues de l'exploitation d'une vingtaine de stacks Thanos monolocataires derrière un querier global sur GKE (Thanos 0.42, caches 0.35+). Source de vérité, avec chiffres et raisonnement :
- Architecture, perf et FinOps : https://wiki.jdelgado.fr/monitoring/lgtm/thanos/
- Caches : https://wiki.jdelgado.fr/monitoring/lgtm/thanos-cache/
- Concurrence de lecture : https://wiki.jdelgado.fr/monitoring/lgtm/thanos-query-concurrency/
L'ensemble complet des règles (R1-R102, flags, valeurs par défaut, métriques, ancres) se trouve dans references/rules.md. Lisez la section de chaque composant que la modification touche avant de conclure.
Procédure de révision
- Notez la version de Thanos (tag de l'image). Les flags et leurs valeurs par défaut changent : vérifiez chaque flag par rapport à celle-ci (règle R92).
- Avec un chart wrapper (bitnami
thanos, un chart umbrella local,extraFlags/argsécrits à la main), lisez les flags sur les manifests rendus (helm templatepar stack), pas sur les valeurs : les couches se surchargent mutuellement et certains flags n'existent que dans un template. - Les noms spécifiques au déploiement (les labels sur lesquels les stacks sont élaguées, les labels de locataire, les labels de cluster ou de datacenter) proviennent de la configuration elle-même. Lorsque le diff ne les montre pas, demandez le dépôt GitOps au lieu de supposer un nom.
- Pour chaque composant présent, parcourez sa check-list ci-dessous, puis sa section dans
references/rules.md. - Calculez l'invariant de concurrence avec les valeurs réelles chaque fois que l'un de ses 4 flags est défini ou laissé par défaut.
- Vérifiez les règles d'alerte en fonction de la façon dont le composant s'exécute réellement (CronJob, spot, pods Pending).
- Rapportez dans le format de sortie ci-dessous.
Check-list par composant
Invariant de concurrence (query frontend + querier + store gateway). --query-range.max-query-parallelism (par défaut 14) x min(selectors, --query.max-concurrent-select) (par défaut 4) doit rester inférieur à min(--store.grpc.series-max-concurrency, --query.max-concurrent) (tous deux par défaut 20). Les valeurs par défaut le brisent déjà (56 vs 20). Une fois que le parallélisme seul atteint le plafond de la gate, une seule requête sature la gate sur chaque stack. Un --query.max-concurrent-select élevé bien au-dessus de 4 supprime le plafond : le multiplicateur devient le nombre brut de selecteurs. Une gate dimensionnée pour limiter la RSS du store gateway (définie après des OOMKills) est une limite mémoire, conservez-la. L'invariant lie les requêtes larges : les requêtes mesurées ont en moyenne 1,5 selecteurs, et la saturation de la gate provenait de plusieurs requêtes concurrentes sur le shard historique, pas d'une seule requête large. Un querier global atteint les queriers régionaux via StoreAPI, ce qui contourne leurs gates PromQL. Corrigez les deux multiplicateurs ensemble, y compris le frontend global, calculez --query.max-concurrent sur l'ensemble du pool de queriers, et commentez chaque multiplicateur avec le plafond aval qu'il doit respecter.
Querier.
- La réponse partielle est activée par défaut (
--query.partial-responsepar défaut àtrue) : un flag absent n'est pas une valeur par défaut sûre. Elle supprime les stores qui génèrent une erreur, pas les lents :--store.response-timeoutpar défaut à0ms(désactivé), donc un store lent fait échouer toute la requête à--query.timeout(par défaut 2m) au lieu d'être supprimé. - La réponse partielle sur un querier utilisé par le ruler ou l'alerting peut manquer silencieusement des alertes, et les réponses partielles ne sont jamais mises en cache. Activez-la par requête uniquement sur les sources de données Grafana humaines.
--selector-labelne doit jamais être une valeur par défaut de template : un stack ingérant 2 valeurs du label renvoie VIDE pour la seconde (perte de données silencieuse). Optez par stack, label à valeur unique que les requêtes portent réellement. Vérifiez chaque stack où il est défini par rapport à ce que ce stack ingère réellement.--grpc-compressionest déprécié après v0.43.0. Son remplacement,client_config.compressionpar endpoint dans--endpoint.sd-config, est ignoré à moins que cet endpoint ne définisse également son propretls_config(par exempleenabled: false) : le flag global l'emporte sinon.
Store gateway.
- N'augmentez jamais
--store.grpc.series-max-concurrencypour guérir la saturation : la gate met en file d'attente, elle ne refuse pas. La saturation se manifeste par des timeouts, la plupart des minutes sont bien en dessous du plafond, et l'augmenter supprime la seule limite pendant les pics. Trouvez le client dans le journal lent du query frontend. --store.limits.request-series=2000000est une valeur copiée qui ne se déclenche jamais (plus grand appel mesuré : 200000 séries). Mesurez d'abordthanos_bucket_store_series_data_touched_bucket{data_type="series"}.thanos_bucket_store_queries_dropped_totalà 0 ne prouve rien : il ne compte que les limiteurs qui sont à 0 (désactivés) par défaut.- Les shards temporels doivent se chevaucher ; des bornes
--min-time/--max-timejointes bout à bout garantissent une fenêtre aveugle lorsqu'un réplica est manquant.--min-timeest la borne la plus ancienne. Un--max-timepositif (par exemple3h) n'est pas une borne supérieure : le shard sert jusqu'à maintenant, par-dessus Receive. - Pas de
emptyDirsur le disque de démarrage du nœud : le téléchargement de l'index-header le sature (30 min de récupération). PVC dédié avec IOPS/débit provisionnés ; sur Hyperdisk Balanced, la capacité n'achète pas les performances. --sync-block-durationen dessous de la valeur par défaut de 15m coûte à peine quelque chose derrière un bucket de mise en cache (seuls les appelsiteren dépendent, ~1 USD/mois à l'échelle de la flotte mesuré) : ne vaut pas un déploiement dédié, corrigez-le au prochain redémarrage planifié.- Conservez le lazy reader, jamais de téléchargement lazy de l'index-header (p99 ~224 s). Avec le timeout d'inactivité par défaut de 5m, la première requête après chaque intervalle paie un rechargement complet ; le flag caché
--store.index-header-lazy-reader-idle-timeoutporté à une journée de travail (10h) a réduit le temps de chargement de 84 % sur un stack, au prix de la mémoire résidente. Flag caché : revérifiez lors de la mise à niveau.
Caches.
- Bucket de mise en cache partagé entre locataires sur Thanos < v0.35.0 : collision de clés, le store gateway reste
Readyet ne sert aucune donnée historique. LeprefixRedis isole sur n'importe quelle version. Un canary monolocataire ne peut pas le détecter. - Cache d'index et bucket de mise en cache sur le même Redis : séparez
db(indexdb: 1). - Ne dimensionnez pas un cache partagé à partir des tailles de cache IN-MEMORY (elles mesurent la durée de vie du pod). Dimensionnez sur l'ensemble de travail sur la fenêtre TTL (P95 d'une sous-requête glissante >= 24 h avec pas explicite), divisé par le chevauchement mesuré entre les réplicas d'un shard, jamais par le nombre de pods. Les réplicas chargent les mêmes blocs mais mettent en cache des chunks différents : 2 réplicas mesurés à 1,4, pas 2.
- Les sous-plages du bucket de mise en cache vivent 24 h (
chunk_subrange_ttl) ;expirationde Redis ne les régit pas. - Le fichier de cache du query frontend est analysé strictement : pas de
ttl, pas deenabled_items, le TTL estexpirationsousconfig:. Clés inconnues : le composant refuse de démarrer. - Redis derrière Sentinel : service Sentinel +
master_name, sinon les écritures vont vers l'ancien master après un basculement.
Receive.
- Reste à la demande avec une classe de priorité dédiée : le seul composant contenant des données non encore téléversées. Tout le reste va sur spot.
- Dimensionnez la limite mémoire sur le pic de séries actives.
head_series_limitne paie que dans un Receive partagé multi-locataire ou avec réplication : avec un réplica, un 429 bloque chaque écriture du locataire, un OOM ne coûte qu'un redémarrage et une relecture WAL. Il est inerte sansmeta_monitoring_urljoignable. Les limites de requête renvoient 413 (Prometheus abandonne les données), la limite de séries renvoie 429 (réessayé). - Anneau de hachage Ketama, pas
hashmod, lorsque la taille du pool changera. - Bornez le heap Go :
GOMEMLIMITà partir de la limite mémoire du pod, ou--enable-auto-gomemlimit(désactivé par défaut, 0,9 de la limite du conteneur). Go définit GOMAXPROCS à partir du cgroup depuis la version 1.25, jamais GOMEMLIMIT. Un OOM de Receive signifie une relecture WAL avant de servir à nouveau.
Compactor.
- En tant que CronJob :
concurrencyPolicy: Forbidest obligatoire (2 compacteurs sur un même bucket corrompent les blocs). - Les jauges
thanos_compact_todo_*ne sont enregistrées qu'avec--wait: en mode CronJob, une alerte sur elles ne se déclenche jamais. Le stockThanosCompactIsDownse déclenche 23 h/24. Remplacez par des règles CronJob de kube-state-metrics et un proxy de backlogthanos_blocks_meta_synceddu store gateway. Toute alerte sur une métrique du compactor (thanos_compact_group_compactions_failures_total...) ne se déclenche jamais en mode CronJob : pas de cible de scraping entre les exécutions. - Jamais de règle de cycle de vie du bucket ou de classe de stockage d'archive sur un bucket Thanos : seul le compactor connaît les dépendances des blocs ; la boucle de réessai a multiplié la facture API par 10 par rapport à l'économie.
- Niveaux de rétention alignés sur ce que les store gateways servent.
Transversal.
- Tous les composants d'un stack dans une seule zone : le trafic inter-zone est facturé sur chaque flèche du chemin de données. Une contrainte de répartition par zone sur un stack est un bug de coût, un compromis délibéré à indiquer autrement.
- Alerte de disponibilité du store sur les réplicas desired vs ready du StatefulSet avec
for: 30m:up == 0ne se déclenche jamais pour un pod Pending évincé, etfor: 5mpage lors d'une récupération spot normale.
Diagnostic de saturation
Utilisez les procédures D1-D10 dans references/rules.md. Cœur de D1 : in_flight = throughput x duration (loi de Little), mesuré par pod ; la durée domine généralement (x190 vs x4.5 sur le terrain). Lisez les panneaux de gate avec max_over_time(...[$__interval:1m]), la latence à p999/p9999, la taille de la flotte avec count(group by (pod) (up == 1)).
Format de sortie
Résultats classés par sévérité : perte de données ou résultats erronés silencieux, puis risque de panne (une configuration avec laquelle le composant refuse de démarrer compte ici), puis coût, puis hygiène. Chaque résultat :
- champ ou flag, avec la ligne
- le problème et sa conséquence concrète
- la correction, comme la valeur ou l'extrait exact
- l'URL du wiki avec ancre, copiée depuis
references/rules.md(les IDs de règle sont internes, laissez-les de côté)