Skip to content

ADR 025 — Gouvernance des ressources : requests/limits mesurĂ©s, QoS, LimitRange et ResourceQuota (2026-06-24)

Statut

AcceptĂ© (2026-06-24). Right-sizing (DĂ©cision 1/2) + LimitRange au bootstrap (DĂ©cision 3) validĂ©s live from-scratch le 2026-06-25 (no BestEffort dans les 8 namespaces possĂ©dĂ©s). ResourceQuota reportĂ© Sprint 6. La validation a rĂ©vĂ©lĂ© que des requests honnĂȘtes sur-souscrivent le nƓud unique (requests mĂ©moire Ă  99 %, prometheus non planifiable) → #114 (capacitĂ©) devient un prĂ©requis dur ; #112 reste ouvert et sera clos avec #114 (voir « Validation live »). Issue #112, nĂ© de l'incident INC-060. ADR compagnon : ADR 026 (segmentation des node groups + Spot, #114) traite le volet « capacitĂ© matĂ©rielle » ; le prĂ©sent ADR traite le volet « discipline des ressources ».

Contexte

L'incident INC-060 a coupĂ© tout l'ingress (~10 min) sur le nƓud unique t3.medium : un redĂ©ploiement de Grafana a dĂ©clenchĂ© un pic mĂ©moire qui a fait basculer un nƓud sur-engagĂ© (limits.memory cumulĂ©es = 168 % de la capacitĂ©). Le dĂ©clencheur (prĂ©install des plugins Grafana) est traitĂ© Ă  part. La cause racine est l'overcommit sans garde-fou : les pods se planifient sur des requests sous-Ă©valuĂ©es, mais la pression au runtime vient des limits, et rien n'empĂȘche leur somme de dĂ©passer la capacitĂ© physique.

À l'approche de Tempo (#108, gourmand) et d'un 2e workload (frontend #109), continuer sans gouvernance rejouerait l'incident.

DĂ©cision 1 — Dimensionner sur la mesure, pas sur l'intuition

On relÚve la consommation réelle par pod (régime stable et pic) avec l'outillage déjà en place (Prometheus + kube-state-metrics + node-exporter) :

  • MĂ©moire : container_memory_working_set_bytes (c'est la mĂ©trique qui dĂ©clenche l'OOMKill, pas le RSS).
  • CPU : rate(container_cpu_usage_seconds_total[5m]).
  • Aide Ă  la recommandation : VPA en mode recommender (Off) ou Goldilocks (dashboard de reco par namespace), sans appliquer automatiquement.

DĂ©cision 2 — RĂ©gler mĂ©moire et CPU diffĂ©remment (compressible vs non)

  • MĂ©moire (non compressible → dĂ©passement = OOMKill) : request = working set en rĂ©gime + marge ; limit couvre le pic mesurĂ© (+ ~15-20 %). Sur les composants critiques, request = limit → classe QoS Guaranteed (les derniers Ă©vincĂ©s sous pression).
  • CPU (compressible → dĂ©passement = throttling, pas de kill) : request = usage p95 ; limit large ou absente sur le latency-sensitive (le throttling CPU fait souvent plus de mal qu'un lĂ©ger dĂ©passement). ArbitrĂ© selon ce que le check kube-linter exige en CI.

DĂ©cision 3 — Border par namespace : LimitRange + ResourceQuota

  • LimitRange (defaults par namespace) : tout pod sans requests/limits hĂ©rite de valeurs par dĂ©faut → plus de pod BestEffort qui se faufile. Placement (corrigĂ© en live, voir plus bas) : un LimitRange n'agit qu'Ă  l'admission du pod (defaults injectĂ©s Ă  la crĂ©ation, jamais rĂ©troactivement). Il doit donc exister avant les pods → il est créé au bootstrap (Ansible), avant tout composant, et non par une Application GitOps qui se synchronise trop tard. Poulet/Ɠuf assumĂ© : une gouvernance qui doit prĂ©cĂ©der ArgoCD ne peut pas ĂȘtre gĂ©rĂ©e par ce mĂȘme ArgoCD.
  • ResourceQuota plafonnant la somme des limits.memory par namespace (reportĂ© Sprint 6) → c'est le garde-fou qui rendrait l'overcommit mĂ©moire structurellement impossible Ă  l'admission. Nuance acquise en live : la somme des limits n'est pas la cause directe d'INC-060 (les limits ne rĂ©servent rien) ; le quota reste utile comme plafond dur, mais le vrai levier anti-incident immĂ©diat a Ă©tĂ© le dĂ©clencheur (prĂ©install Grafana, !195) et la capacitĂ© (#114).

Conséquences

  • L'incident INC-060 ne peut plus se reproduire par overcommit : un dĂ©ploiement qui ferait dĂ©passer le quota est refusĂ© Ă  l'admission, au lieu de tuer le nƓud au runtime.
  • Les valeurs (requests/limits, plafond de quota) se calent et se valident en live (MR3) : un quota trop serrĂ© bloquerait les dĂ©ploiements, donc il se vĂ©rifie sur le cluster rĂ©el, pas seulement par helm template.
  • Classe QoS explicite sur le critique → comportement d'Ă©viction prĂ©visible sous pression.
  • Alternatives Ă©cartĂ©es : monter un nƓud plus gros sans gouvernance (dĂ©place le mur sans le supprimer) ; Karpenter tout de suite (ne corrige pas l'overcommit runtime, il rĂ©agit au Pending — donc inutile sans requests honnĂȘtes ; reportĂ© Sprint 6).

Validation live (2026-06-25)

Boot from-scratch, mesures par pod via Prometheus (metrics-server réparé en parallÚle, #115/INC-061).

  • DĂ©clencheur (!195) ✅ : cluster montĂ© propre, api 200, aucune cascade NodeNotReady au redĂ©ploiement Grafana (prĂ©install plugins dĂ©sactivĂ©). C'est le gain anti-incident immĂ©diat.
  • Right-sizing (!196) ✅ : valeurs mesurĂ©es bien dĂ©ployĂ©es (fastapi req 100m/128Mi limit 256Mi ; argocd-application-controller req 512Mi limit 1Gi — le runaway BestEffort Ă  ~800Mi est dĂ©sormais capĂ© ; grafana et prometheus req 384Mi limit 512Mi).
  • Nuance honnĂȘte sur l'overcommit : la somme des limits.memory du nƓud est montĂ©e (168 % → 217 %), pas baissĂ©e. Ce n'est pas un Ă©chec : c'est l'effet mĂ©canique d'avoir donnĂ© une limite explicite (1Gi) Ă  un gros pod jusque-lĂ  non bornĂ© (BestEffort = 0 dans la somme). Borner un runaway fait monter la somme des limits tout en rĂ©duisant le risque rĂ©el. La mĂ©trique qui compte est ailleurs : requests mĂ©moire Ă  91 % → nƓud tendu, ce qui confirme que le vrai fix capacitĂ© est #114, pas le right-sizing seul.
  • LimitRange (!197 → !199) — trou trouvĂ© puis corrigĂ©, reprouvĂ© from-scratch ✅ : livrĂ© en GitOps (sync-wave 2), il arrivait aprĂšs les composants dĂ©jĂ  dĂ©marrĂ©s → ~14 pods plateforme (argocd, cert-manager, ESO, external-dns) restaient BestEffort (un LimitRange n'agit qu'Ă  l'admission). CorrigĂ© (!199) : LimitRange dĂ©placĂ© au bootstrap Ansible, créé avant tout composant. 2e boot from-scratch (2026-06-25) : plus AUCUN BestEffort dans les 8 namespaces possĂ©dĂ©s, fastapi nĂ© restricted (PSA) une seule fois, LimitRange prĂ©sent partout avant les pods.

La gouvernance honnĂȘte rĂ©vĂšle la sur-souscription (le vrai enseignement)

ConsĂ©quence directe et assumĂ©e du LimitRange efficace : chaque pod jusque-lĂ  BestEffort rĂ©serve dĂ©sormais le defaultRequest (64Mi). Sur le boot from-scratch, le nƓud unique t3.medium est montĂ© Ă  99 % de requests mĂ©moire (3286Mi / ~3319 allouables) → prometheus-kps-prometheus-0 (request 384Mi) ne peut plus ĂȘtre planifiĂ© (FailedScheduling: Insufficient memory). À noter : MemoryPressure: False — c'est de la sur-rĂ©servation (somme des requests), pas un manque de RAM rĂ©elle (usage ~69 %) ; les pods ne consomment pas ces 64Mi, ils bloquent juste le scheduler.

DĂ©cision (assumĂ©e avec Youssef) : ne PAS band-aider en baissant le defaultRequest Ă  un plancher-jeton (16Mi) qu'il faudrait remonter ensuite. On garde des requests honnĂȘtes (64Mi) : c'est future-correct (dĂšs que la capacitĂ© existe, tout se planifie sans rien retoucher). La gouvernance par rĂ©servations et la capacitĂ© sont couplĂ©es : on ne peut pas rĂ©server un plancher pour chaque pod sur un nƓud dĂ©jĂ  plein.

→ #114 (ADR 026) passe de « souhaitable » Ă  prĂ©requis dur, prouvĂ© par la mesure (requests Ă  99 %). #112 reste ouvert : la gouvernance est livrĂ©e et validĂ©e (no BestEffort, runaways capĂ©s), mais la stack ne planifie entiĂšrement qu'aprĂšs #114 (capacitĂ©). On clĂŽt #112 avec #114.

Évolution possible

  • ADR 026 (#114) : segmenter en node groups core (on-demand) / observability (Spot) pour isoler les workloads gourmands du trafic.
  • Karpenter (Sprint 6) : une fois les requests fiables, l'autoscaling des nƓuds devient pertinent (il se dĂ©clenche sur le Pending, qui dĂ©pend des requests).

Date : 2026-06-24 Sprint : 5 Issue : #112 (INC-060)