Cloud & DevOps15 février 202616 min de lecture

Kubernetes en production : ce qui casse réellement

CPU throttling, evictions, liveness mal réglée, PodDisruptionBudget oublié : les pannes Kubernetes viennent rarement de là où on les attend. Ce que nous corrigeons le plus souvent en mission.

Partager
Kubernetes en production : ce qui casse réellement
KubernetesProductionSREObservabilitéFinOps

Les pannes ne viennent pas de Kubernetes

Sur les incidents Kubernetes que nous auditons, une minorité met en cause l’orchestrateur. La grande majorité vient de quatre champs mal renseignés dans un manifeste : les requests, les limits, la livenessProbe et le budget de perturbation. Optionnels, ils passent la revue de code et fonctionnent en recette — puis décident du comportement du cluster le jour où la charge monte.

Deux contextes illustrent les deux familles de contraintes. La plateforme France Travail, conçue pour le Ministère du Travail, tourne sur Kubernetes / OpenShift : plus de 100 000 utilisateurs actifs, 99,9 % de disponibilité, et une contrainte dominante qui n’est pas la moyenne mais la charge de pointe, avec l’obligation de service public. Chez Neuflize OBC, le backend de l’application bancaire mobile a migré vers Azure AKS, sous monitoring Grafana, Prometheus, Loki, Promtail et Tempo. Tenir un pic d’un côté ; voir avant que le client n’appelle de l’autre.

Requests et limits : la vérité sur le CPU throttling

Ces deux champs ne servent pas à la même chose. La request est une réservation : le scheduler s’en sert pour décider sur quel nœud le pod atterrit. La limit est une contrainte d’exécution appliquée par le noyau, via les cgroups. Confondre les deux produit des clusters à moitié vides qui throttlent quand même.

Le CPU n’est pas plafonné, il est découpé en tranches

Une limite CPU n’est pas un plafond de fréquence. Le noyau alloue au conteneur un quota de temps CPU par période — 100 ms par défaut. Quota épuisé avant la fin de la période, le conteneur est mis en pause jusqu’à la suivante : c’est du throttling, brutal pour tout ce qui est latence-sensible.

Le cas classique : un service Java qui a besoin de brèves rafales multi-cœurs — JVM, JIT, cycles GC — avec une limite à 500 m. Il est interrompu à répétition alors que sa moyenne reste loin sous la limite : le graphe d’utilisation rassure, la p99 explose. D’où container_cpu_cfs_throttled_seconds_total à surveiller.

Ce que nous appliquons

  • Toujours une request CPU, calibrée sur la consommation observée au percentile 90, pas sur une estimation d’architecte.
  • Pas de limite CPU par défaut : la request garantit déjà une part sous contention, la limite throttle même quand le nœud est inactif. On n’en pose une que si le voisinage l’exige — cluster mutualisé, batch capable de saturer un nœud — et nettement au-dessus de la request.
  • Limit mémoire toujours égale à la request : la mémoire est incompressible, dépasser déclenche un OOMKill. Pour la JVM, alignez le runtime sur les cgroups et gardez une marge entre le tas et la limite.
resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    memory: "1Gi"      # égale à la request
    # pas de limits.cpu : la request suffit à garantir la part

Classes QoS et evictions : qui meurt en premier

Ce que vous écrivez dans resources détermine une classe de qualité de service qui décide de l’ordre dans lequel vos pods seront sacrifiés quand un nœud manque de mémoire.

ClasseConditionComportement sous pression
GuaranteedRequests = limits, CPU et mémoire, partoutÉvincé en dernier
BurstableAu moins une request, sans égalité stricteÉvincé selon son dépassement
BestEffortAucune request ni limitÉvincé en premier

Un pod sans resources est donc BestEffort : c’est celui qui saute d’abord, et très souvent celui déployé à la va-vite — l’exporter, le job de synchronisation dont personne ne parle jusqu’au jour où il s’arrête.

Sous pression mémoire, le kubelet évince avant que le OOM killer n’intervienne ; le contrôleur recrée le pod, potentiellement sur un nœud tout aussi chargé, et la cascade commence. Trois réflexes : réserver des ressources au système et au kubelet ; donner une priorityClassName distincte aux composants de plateforme, pour qu’un batch ne préempte pas un ingress controller ; traiter toute rafale d’évictions comme un défaut de dimensionnement. Une éviction, c’est une réservation qui mentait.

Probes : readiness, liveness, startup

Les trois probes répondent à trois questions différentes. Les confondre transforme une lenteur passagère en panne totale.

  • readinessProbe — « puis-je recevoir du trafic ? » En échec, le pod sort des endpoints du Service mais continue de tourner. Réversible.
  • livenessProbe — « ce processus est-il irrécupérable ? » En échec, le conteneur est tué. Destructif : ne doit couvrir qu’un état dont seul un redémarrage sort, typiquement un deadlock.
  • startupProbe — « le démarrage est-il terminé ? » Tant qu’elle n’a pas réussi, les deux autres sont suspendues.

La panne auto-infligée

La base de données ralentit ; l’endpoint de liveness, qui l’interroge « pour être sûr », dépasse son timeout ; Kubernetes tue le conteneur ; les requêtes basculent sur les répliques restantes, qui dépassent à leur tour. En moins de deux minutes, le déploiement entier est en CrashLoopBackOff pour une dépendance qui n’est jamais tombée. L’incident aurait dû être une dégradation ; il est devenu une indisponibilité.

Les règles que nous imposons

  • La liveness ne teste que le processus : un endpoint local, sans appel réseau. La readiness, elle, peut vérifier les dépendances.
  • Seuils asymétriques : liveness patiente (failureThreshold élevé), readiness réactive. Une liveness qui déclenche en dix secondes est presque toujours un piège.
  • Une startupProbe plutôt qu’un initialDelaySeconds deviné une fois et jamais réajusté.
  • Un preStop et une période de grâce cohérents : sans temporisation avant le SIGTERM, le pod cesse d’accepter du trafic alors que certains nœuds n’ont pas propagé le retrait — d’où des 502 à chaque déploiement.

PodDisruptionBudget : ce qui vous protège des mises à jour

Un PodDisruptionBudget déclare combien de répliques doivent rester disponibles pendant une perturbation volontaire : drain, montée de version, consolidation par l’autoscaler. Sans PDB, un kubectl drain vide le nœud sans se demander s’il emporte les trois dernières répliques d’un service.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api

Deux erreurs symétriques. Pas de PDB du tout : la mise à jour devient une opération à risque, faite de nuit, donc reportée — et un cluster qu’on n’ose plus mettre à jour est un problème de sécurité. Un PDB qui bloque tout : minAvailable égal au nombre de répliques, ou un PDB sur un déploiement à réplique unique, et plus aucune éviction n’est possible. Un PDB ne crée pas de disponibilité, il en préserve.

Le PDB ne couvre que le volontaire : une panne de nœud, une indisponibilité de zone, un OOMKill ne passent pas par l’API d’éviction. Pour ces cas, ce sont les topologySpreadConstraints et l’anti-affinité qui répartissent les répliques entre zones et entre nœuds. Les deux dispositifs sont nécessaires.

Autoscaling : aucun réglage ne rattrape un mauvais dimensionnement

Trois briques souvent confondues : l’HPA fait varier le nombre de répliques selon une métrique ; le VPA ajuste requests et limits selon l’historique ; le Cluster Autoscaler (ou Karpenter) ajoute et retire des nœuds quand des pods restent en attente.

Premier point : l’HPA s’appuie sur la request. Une cible d’utilisation CPU à 70 % signifie 70 % de la request, pas 70 % du cœur physique. Si la request est fantaisiste, l’autoscaling l’est aussi.

Deuxième point : HPA et VPA sur la même ressource se contredisent — l’un ajoute des répliques pendant que l’autre grossit les pods — et le système oscille. La combinaison praticable reste HPA sur les répliques, VPA en recommandation seule.

Troisième point, le plus important : l’autoscaling n’est pas instantané. Collecte de la métrique, décision, démarrage du pod, et s’il faut un nœud, provisionnement et enrôlement : on raisonne en minutes. Un pic qui monte en trente secondes ne sera pas rattrapé.

C’est la contrainte d’une plateforme de service public comme France Travail, où l’affluence n’est pas une moyenne mensuelle mais un créneau connu à l’avance. La réponse n’est pas de régler l’HPA plus agressivement : c’est de tenir un socle dimensionné pour la pointe, avec des pods de réservation à priorité basse que la charge réelle préempte. Deux garde-fous complètent : une stabilisation à la baisse assez longue pour éviter le battement, et un minReplicas jamais égal à 1 sur un service exposé.

Observabilité : quatre signaux, une seule fenêtre

Le réflexe habituel consiste à instrumenter l’infrastructure — CPU des nœuds, mémoire, nombre de pods — puis à découvrir pendant l’incident qu’aucun de ces graphes ne dit si les utilisateurs sont servis. Commencez par les quatre signaux dorés :

  • Latence — distribution, pas moyenne. Une p50 correcte avec une p99 à plusieurs secondes est un incident en cours.
  • Trafic — requêtes par seconde, par route et par version.
  • Erreurs — taux de 5xx, mais aussi les 200 au résultat vide ou dégradé.
  • Saturation — la ressource la plus contrainte : throttling CPU, pool de connexions, profondeur de file.

Sur le périmètre bancaire mobile de Neuflize OBC, la stack déployée est celle que nous recommandons par défaut : Prometheus pour les métriques, Loki alimenté par Promtail pour les logs, Tempo pour les traces, corrélés dans Grafana avec de l’alerting proactif. L’intérêt n’est pas la liste d’outils mais la corrélation : partir d’un pic de latence, ouvrir une trace du même intervalle, sauter aux logs du pod.

Deux principes : alerter sur les symptômes, pas sur les causes — « le taux d’erreur du paiement dépasse le budget » réveille quelqu’un, « le CPU du nœud 7 est à 85 % » ne devrait réveiller personne ; et chaque alerte a une procédure, faute de quoi elle devient un bruit qui fait ignorer les vraies.

Sécurité : quatre couches non négociables

RBAC

Moindre privilège par namespace et par ServiceAccount, avec des Role plutôt que des ClusterRole. Le point le plus négligé : désactiver le montage automatique du jeton de ServiceAccount sur les pods qui n’appellent jamais l’API Kubernetes — la grande majorité. Un jeton monté dans un conteneur compromis est une élévation de privilèges offerte.

NetworkPolicy

Par défaut, tout pod joint tout pod du cluster. Le modèle correct est un default deny en entrée par namespace, puis des autorisations explicites. Vérifiez que votre CNI les applique : sans plugin compatible, l’objet est accepté par l’API et n’a aucun effet — une politique qui rassure sans protéger.

Admission

Ce qui n’est pas contrôlé à l’admission finit en production. Activez les Pod Security Standards en restricted sur les namespaces applicatifs : pas de conteneur privilégié, pas d’escalade, racine en lecture seule, exécution sans root. Complétez par des politiques maison : interdire les images en latest, exiger un registre autorisé, refuser un déploiement sans requests.

Secrets

Un objet Secret est encodé en base64, ce qui n’est pas du chiffrement. Activez le chiffrement au repos et n’écrivez rien dans Git : passez par un gestionnaire externe, synchronisé par un opérateur ou monté via un pilote CSI. Ajoutez une détection de secrets et un scan des images dans la CI.

Sécurité intégrée au pipeline plutôt qu’auditée en fin de course : c’est la logique appliquée sur la plateforme de contrôles cyber conçue pour Société Générale CIB.

Le coût : le bin packing avant les remises

Une facture Kubernetes tient en trois postes : les nœuds payés, la fraction réellement utilisée, le prix unitaire. On les travaille dans cet ordre : négocier le prix d’une capacité gaspillée ne mène nulle part.

Le remplissage d’abord. Le scheduler place selon les requests : si elles valent le double de la consommation réelle, vos nœuds sont à moitié vides et vous les payez pleins. Le rapport entre la somme des requests et la consommation observée est la première métrique FinOps — à lire sur la capacité allouable, pas brute.

La forme des nœuds ensuite. Trop gros, ils fragmentent ; trop petits, ils multiplient l’overhead et se heurtent aux plafonds de pods par machine. Le compromis se lit sur votre plus gros pod : un nœud doit pouvoir en accueillir plusieurs sans trou inutilisable.

Le prix unitaire enfin. Capacité interruptible pour ce qui tolère un arrêt, engagements sur la part stable et jamais sur le pic, processeurs ARM pour les charges qui s’y prêtent. Sur un audit FinOps AWS mené pour une startup SaaS de série A, l’ensemble right-sizing, Graviton, Savings Plans, cycle de vie S3 et suppression de trois environnements fantômes a réduit la facture de 61 % en deux semaines. Sur le périmètre bancaire mobile de Neuflize OBC, la migration vers AKS s’est accompagnée d’une baisse de 52 % des coûts cloud, à service constant.

Deux précautions : l’interruptible suppose que vos applications encaissent proprement un SIGTERM, le préavis se comptant en dizaines de secondes ; l’ARM suppose des images multi-architectures. Voir notre page Cloud & FinOps.

Charge de pointe et tests de charge

Un cluster se dimensionne sur sa pointe, pas sur sa moyenne. Sur une plateforme nationale comme France Travail, le pic est structurel : les usages se concentrent sur des créneaux connus et l’obligation de service public interdit de tenir une saturation pour acceptable. C’est cette contrainte, plus que le volume, qui dicte l’architecture — et qui rend tenable un objectif de 99,9 % pour plus de 100 000 utilisateurs.

Un test de charge utile n’est pas un tir de vingt minutes sur un environnement neuf. Ce qu’il doit produire :

  • Le point de rupture et la ressource qui rompt en premier — presque toujours une dépendance en aval, pas le cluster.
  • Le comportement au-delà. Une plateforme saine dégrade et rejette proprement ; une plateforme fragile empile les requêtes jusqu’à l’effondrement. Sans limitation de concurrence ni disjoncteur, le pic ne ralentit pas le système : il le fait tomber.
  • Le délai réel de l’autoscaling, chronométré, du dépassement du seuil au premier pod servant du trafic.
  • Le comportement en charge soutenue. Une heure à 80 % révèle les fuites mémoire que dix minutes à 200 % masquent.

Une règle : le test se fait avec les mêmes requests, limites et probes qu’en production. Un tir sans limites ne mesure rien d’exploitable.

Checklist de mise en production

Ce que nous vérifions avant d’autoriser un service à passer en production. Chaque point correspond à un incident déjà vu.

  1. Requests CPU et mémoire sur tous les conteneurs, init containers et sidecars compris.
  2. Limite mémoire égale à la request ; limite CPU absente ou nettement au-dessus ; runtime aligné sur les cgroups.
  3. Aucun pod applicatif en classe BestEffort.
  4. readinessProbe qui teste les dépendances ; livenessProbe locale et patiente ; startupProbe sur les démarrages lents.
  5. preStop et période de grâce cohérents avec la durée maximale d’une requête.
  6. Au moins deux répliques, réparties par topologySpreadConstraints sur zones et nœuds.
  7. PodDisruptionBudget déclaré, et vérifié comme n’empêchant pas un drain ; HPA calibré sur des requests réalistes, minReplicas supérieur à 1.
  8. Tableau de bord par service : latence, trafic, erreurs, saturation, throttling CPU inclus.
  9. Alertes fondées sur des symptômes, reliées à une procédure écrite ; logs, métriques et traces corrélables dans une seule interface.
  10. NetworkPolicy en default deny, avec un CNI qui les applique effectivement.
  11. Pod Security Standards en restricted ; jeton de ServiceAccount non monté quand il est inutile.
  12. Secrets hors du dépôt Git, chiffrement au repos activé, images scannées dans la CI.
  13. Test de charge passé avec la configuration de production, point de rupture documenté.
  14. Procédure de retour arrière testée, pas seulement écrite.

Ces points relèvent moins de Kubernetes que de la discipline d’exploitation — c’est pour cela qu’ils sautent en premier quand une date de livraison approche.

Si vous préparez une mise en production, une montée de version de cluster ou un audit d’exploitabilité, écrivez-nous : réponse sous 24 heures ouvrées, par un associé.

FAQ

Questions fréquentes

Faut-il mettre une limite CPU sur ses pods Kubernetes ?

Pas par défaut sur un service applicatif dont la request CPU est correctement calibrée : la request garantit déjà une part sous contention, alors que la limite déclenche du throttling même quand le nœud est inactif. Une limite CPU se justifie sur un cluster mutualisé entre équipes, sur des workloads batch capables de saturer un nœud, ou pour une refacturation interne — et elle doit alors rester nettement au-dessus de la request. La règle inverse s’applique à la mémoire : limite toujours égale à la request, car un dépassement mémoire ne throttle pas, il tue le conteneur.

Pourquoi mes pods redémarrent-ils alors que l’application fonctionne ?

Le suspect principal est une livenessProbe trop stricte ou qui teste une dépendance externe. Si la probe interroge la base de données et que celle-ci ralentit, Kubernetes tue des conteneurs parfaitement sains, la charge se reporte sur les répliques restantes et l’ensemble du déploiement finit en CrashLoopBackOff. Le second suspect est l’OOMKill : vérifiez le code de sortie du conteneur et la consommation mémoire par rapport à la limite. Une liveness doit rester locale et patiente ; les dépendances se testent en readiness.

HPA ou VPA : que faut-il mettre en place en premier ?

Ni l’un ni l’autre avant d’avoir des requests justes, car l’HPA raisonne en pourcentage de la request : une réservation fausse produit un autoscaling faux. En pratique, on calibre les requests sur la consommation observée — le VPA en mode recommandation seule est utile pour cela — puis on met un HPA sur le nombre de répliques. Évitez d’activer HPA et VPA en mode automatique sur la même ressource : les deux se contredisent et le système oscille.

Comment absorber un pic de charge que l’autoscaling ne rattrape pas ?

En provisionnant avant le pic. Entre la collecte de la métrique, la décision, le démarrage du pod et, s’il le faut, le provisionnement d’un nœud, l’autoscaling se compte en minutes. Pour un pic connu à l’avance — un créneau d’affluence sur une plateforme de service public, par exemple — la réponse est un socle dimensionné pour la pointe, complété par des pods de réservation à priorité basse que la charge réelle préempte instantanément. Pour un pic imprévisible, ce sont la limitation de concurrence et les disjoncteurs qui évitent que la saturation ne devienne un effondrement.

Un PodDisruptionBudget protège-t-il d’une panne de nœud ?

Non. Le PDB ne s’applique qu’aux perturbations volontaires, celles qui passent par l’API d’éviction : drain, mise à jour du cluster, consolidation par l’autoscaler. Une panne matérielle, une indisponibilité de zone ou un OOMKill ne passent pas par ce chemin. Pour l’imprévu, ce sont les topologySpreadConstraints et l’anti-affinité qui répartissent les répliques entre zones et entre nœuds. Le PDB gère le prévu, l’étalement topologique gère le reste.

Partager