Guide Kubernetes
Services sans état sur Kubernetes : concevoir, déployer et monter en charge en toute sécurité
Un service sans état peut être remplacé à tout moment sans perdre le contexte durable nécessaire à l'exécution correcte du travail. Concevez d'abord cette propriété avant d'ajouter des réplicas.
Sur cette page
Kubernetes rend le remplacement normal : les pods sont redémarrés, replanifiés, mis à niveau et mis à l'échelle. Un service qui dépend de la mémoire, du disque local ou de l'identité réseau d'un pod finira par échouer de façons difficiles à reproduire. Une conception sans état transforme ces événements de cycle de vie en opérations courantes — la même propriété dont dépend une couche d'orchestration d'edge managée, puisque les décisions de Performance et de Sécurité prises en amont de l'origine ne restent correctes que si chaque instance remplaçable répond de la même façon à une requête.
Sans état ne signifie pas qu'un service ne possède aucun état. Cela signifie que l'état métier durable réside dans un système externe adapté et que toute instance peut traiter une requête à l'aide de cet état. Un pod peut mettre des données en cache ou détenir une connexion en cours, mais l'exactitude ne doit pas dépendre de la survie de ces données transitoires. Séparer ainsi l'état durable de l'état jetable est le cœur de la conception de service sans état, et le reste de ce guide détaille comment l'appliquer sur Kubernetes sans perdre en débit ni en exactitude.
Identifier l'état avant de le déplacer
Listez tout élément qu'un processus conserve au-delà d'une requête unique : sessions de connexion, progression d'upload, propriété des jobs, compteurs de limitation de débit, clés d'idempotence, fichiers générés, données de routage WebSocket et entrées de cache. Pour chaque élément, déterminez s'il est durable, reconstructible ou jetable.
- Stockez les enregistrements durables dans une base de données, un stockage d'objets ou une file avec un modèle explicite de rétention et de cohérence.
- Stockez l'état éphémère partagé, tel qu'une session distribuée ou un bail, dans un stockage partagé spécialisé lorsqu'il est réellement nécessaire.
- Conservez les caches reconstructibles en local uniquement si un cache froid ne nuit pas à l'exactitude ni ne surcharge l'origine.
- Considérez les systèmes de fichiers locaux aux pods comme temporaires, sauf si une charge de travail utilise délibérément un volume persistant et ses contraintes opérationnelles.
Évitez d'utiliser des sessions persistantes pour masquer un état. L'affinité peut améliorer la localité du cache, mais elle ne protège pas contre le redémarrage d'un pod, un rollout ou une défaillance de zone. Si une action utilisateur doit atteindre la même instance pour réussir, le système a une contrainte de disponibilité et de mise à l'échelle qui doit être explicitée.
Déplacer l'état hors du pod crée un problème de second ordre : plusieurs réplicas peuvent désormais se disputer la lecture, la modification et l'écriture du même enregistrement externe. Une conception de service sans état ne supprime pas le contrôle de concurrence, elle le déplace. Utilisez un verrou optimiste (colonne de version ou précondition ETag) pour les mises à jour de type lecture-modification-écriture, une contrainte d'unicité en base pour la sémantique « réclamer ce travail une seule fois », et un bail distribué de courte durée uniquement lorsqu'un seul propriétaire doit réellement exécuter une tâche à la fois. Traitez tout limiteur de débit en mémoire comme approximatif, sauf s'il s'appuie sur le même magasin que lit chaque réplica.
Construire des services sans état sur Kubernetes qui survivent au remplacement
Commencez par une image immuable et un contrat d'exécution réduit et explicite. Le processus doit se lier au port configuré, exposer des endpoints de santé, lire la configuration de l'environnement de déploiement et émettre les logs vers la sortie standard ou un collecteur. Il ne doit pas attendre un nœud, une IP publique ou un répertoire applicatif inscriptible particulier.
Utilisez un Deployment pour les services HTTP ou gRPC indépendamment remplaçables. Attribuez-lui des demandes de ressources fondées sur une demande stable mesurée et des limites choisies pour empêcher une charge de travail de nuire aux autres. Une limite CPU peut provoquer du throttling et une forte latence de queue ; mesurez avant de l'appliquer partout. Les limites mémoire sont une protection essentielle, mais un redémarrage par manque de mémoire n'est pas un plan de capacité.
Probes de readiness, de liveness et de démarrage
Configurez les probes pour refléter des questions distinctes :
- Démarrage : l'application a-t-elle terminé son initialisation bornée ?
- Readiness : cette instance peut-elle accepter le trafic qui lui est attribué maintenant ?
- Liveness : le processus est-il irrémédiablement bloqué et mérite-t-il un redémarrage ?
Gardez les endpoints de probe peu coûteux et indépendants de l'authentification utilisateur. Ne leur faites pas exécuter de requêtes coûteuses ni appeler chaque dépendance aval optionnelle. Lors d'une panne, une vérification de liveness sensible aux dépendances peut provoquer une tempête de redémarrages ; la readiness peut retirer une instance du trafic lorsqu'elle ne peut pas servir en sécurité, tandis que les circuit breakers et les solutions de repli protègent les appels aux dépendances.
Kubernetes applique des valeurs par défaut prudentes pour les probes : periodSeconds: 10, timeoutSeconds: 1, failureThreshold: 3. Une probe de démarrage maintient la liveness et la readiness inactives jusqu'à son premier succès, ce qui rend supportable une JVM lente à démarrer sans faux redémarrage. Retenez la conséquence de ces valeurs : une probe de liveness déclare un conteneur mort après failureThreshold * periodSeconds, soit 30 secondes par défaut — une valeur qui doit rester inférieure à terminationGracePeriodSeconds, sous peine de tuer un processus bloqué avant que sa séquence d'arrêt n'ait eu sa chance.
Répartir les réplicas entre zones et nœuds
Un Deployment doté de plusieurs réplicas n'offre aucune protection si l'ordonnanceur les place tous sur un seul nœud ou dans une seule zone. topologySpreadConstraints borne directement ce déséquilibre : un maxSkew de 1 avec topologyKey: topology.kubernetes.io/zone et whenUnsatisfiable: DoNotSchedule plafonne l'écart de répartition entre zones et transforme la règle en exigence stricte plutôt qu'en simple préférence. Associez-la à un Pod Disruption Budget, et fixez unhealthyPodEvictionPolicy de façon délibérée : sa valeur par défaut, IfHealthyBudget, peut bloquer le drain d'un nœud derrière un pod qui tourne mais ne passera jamais la readiness de lui-même, car l'évincer ferait tomber l'application protégée sous son nombre de réplicas sains désiré.
Un Pod Disruption Budget n'est pas une garantie de disponibilité
Un Pod Disruption Budget limite les perturbations volontaires, sous réserve des conditions du cluster. Il n'empêche pas les crashs applicatifs, les pénuries de capacité, les défaillances de nœuds ou un mauvais rollout. Concevez les réplicas, la répartition topologique et l'arrêt progressif pour les modes de défaillance auxquels vous devez résister.
Drainez correctement les requêtes et le travail
Kubernetes retire un conteneur après son délai de grâce de terminaison, mais l'ordre du cycle de vie compte : le kubelet démarre ce délai, exécute preStop, puis n'envoie SIGTERM que si le conteneur tourne toujours. L'application doit coopérer. À la terminaison, faites immédiatement échouer la readiness, cessez d'accepter de nouvelles connexions et laissez aux requêtes en cours un temps borné pour se terminer. Alignez le drainage du load balancer, le comportement pre-stop, le délai d'arrêt de l'application et terminationGracePeriodSeconds afin qu'ils ne se contredisent pas.
Pour les workers asynchrones, utilisez des files avec accusé de réception et des handlers idempotents. Un worker peut être arrêté après avoir reçu un message mais avant d'avoir enregistré son résultat. Rendez les livraisons en double sûres à l'aide d'une clé d'idempotence durable ou du pattern transactional outbox. Ne prolongez le bail d'un message que tant que le worker est sain et routez le travail qui échoue à répétition vers un processus de dead-letter ayant un responsable.
Les connexions longue durée méritent un plan distinct. Les WebSockets, les réponses en streaming et les uploads importants ne peuvent pas être drainés comme un appel d'API de 50 ms. Définissez un âge maximal de connexion, communiquez le comportement de reconnexion aux clients et déployez progressivement. Pour une audience mondiale, réfléchissez à l'emplacement de la terminaison des connexions et de l'état applicatif ; une couche de connexion en périphérie peut réduire la latence aller-retour, mais le protocole d'origine nécessite toujours une reconnexion sûre et une récupération d'état.
Si le pod exécute un sidecar, l'ordre de son arrêt compte autant que celui du conteneur principal. Les sidecars natifs — déclarés comme initContainers avec restartPolicy: Always, stables depuis Kubernetes 1.33 — démarrent avant le conteneur principal et s'arrêtent après lui, dans l'ordre inverse. C'est cet ordonnancement qui rend fiable un proxy de service mesh ou un expéditeur de logs pendant l'arrêt ; un simple second conteneur sans ordre défini n'offre aucune garantie équivalente.
Mise à l'échelle horizontale des pods sans déplacer le goulot d'étranglement
La mise à l'échelle horizontale des pods, pilotée par le HorizontalPodAutoscaler, peut ajouter des réplicas selon le CPU, la mémoire ou des métriques personnalisées. Choisissez un signal lié à la capacité utile. La profondeur ou l'âge de file, les requêtes actives ou la concurrence peuvent être plus significatifs que le CPU pour un service limité par les E/S. Validez la boucle complète : fraîcheur des métriques, délai de montée en charge, capacité des nœuds, durée de pull d'image, préchauffage de l'application et limites de connexions aval.
L'API autoscaling/v2 sépare la décision de mise à l'échelle du rythme auquel elle s'applique. Par défaut, le contrôleur ignore un changement calculé qui reste dans une bande de tolérance d'environ 10 % autour du nombre de réplicas courant, pour éviter les oscillations dues au bruit des métriques. Le champ behavior façonne ensuite indépendamment la montée et la descente : une fenêtre de stabilisation courte en montée met la capacité en place rapidement lors d'un pic, tandis qu'une fenêtre longue en descente — cinq minutes par défaut — évite de libérer des pods dont le trafic aura de nouveau besoin l'instant suivant. Gardez en tête que la mise à l'échelle horizontale des pods n'aide que si un nouveau pod peut atteindre l'état prêt, et obtenir son budget de connexions aval, avant que le trafic ayant déclenché la montée en charge n'arrive.
Définissez une limite de concurrence dans l'application ou la couche proxy. Sinon, davantage de trafic peut créer un nombre non borné de goroutines, threads, promises ou appels de base de données dans chaque réplica. La contre-pression est une fonctionnalité d'exactitude : mettez le travail en file, écartez le travail de faible priorité, renvoyez une réponse réessayable ou servez un résultat mis en cache ou dégradé avant que le service n'atteigne la défaillance.
La planification de capacité doit inclure les ressources partagées. Si chaque nouveau pod ouvre vingt connexions à la base de données, un autoscaler qui passe de dix à cent pods demande deux mille connexions. Limitez les pools par pod, utilisez un proxy conscient de la base de données lorsque c'est pertinent et alertez sur le temps d'attente du pool, pas uniquement sur le CPU des pods.
Rendre la latence visible de la périphérie à la dépendance
Mesurez la latence vue par l'utilisateur ainsi que les métriques du cluster. Un pod sain peut tout de même produire une expérience lente à cause du DNS, de TLS, des cache misses, du routage régional, d'une chaîne d'API sérielle ou d'une base de données surchargée. Propagez le contexte de trace W3C depuis l'ingress ou la périphérie vers les clients de service et de dépendance, puis attachez la route, la version de release, la région et le résultat à la télémétrie.
Suivez la latence p50, p95 et p99, le taux d'erreur, la saturation, l'âge de file, l'attente du pool de connexions et le taux de cache hit. Corrélez-les avec les événements de rollout et d'autoscaling. Les recommandations OpenTelemetry pour Kubernetes constituent un bon point de départ pour collecter des signaux sans faire des logs l'unique source de vérité.
Soyez attentif à l'identité du pod utilisée comme étiquette de métrique : un nom de pod ou une IP crée une nouvelle série temporelle à chaque remplacement de réplica, et une flotte qui oscille entre dix et cent pods peut vite transformer un ensemble de métriques modeste en problème de cardinalité. Gardez l'identité du pod dans les logs et les traces, où elle sert vraiment à déboguer une requête précise, pas dans l'ensemble d'étiquettes que chaque réplica écrit à chaque scrape.
Pour le trafic public pouvant être mis en cache, utilisez des règles Cache-Control et de clé de cache correctes en périphérie. Explicitez les routes authentifiées, personnalisées et de mutation. La mise en cache en périphérie réduit la charge à l'origine uniquement lorsque l'invalidation, les variations et les sémantiques d'autorisation sont correctes ; elle ne doit jamais mettre une réponse en cache simplement parce que l'origine était lente.
Corrélez la télémétrie du cluster et celle de la périphérie
Les tableaux de bord du cluster montrent la santé au niveau des pods ; ils ne montrent pas ce qu'un utilisateur a réellement vécu en périphérie. Une couche d'orchestration managée qui observe les deux — via une vue partagée comme MYO — peut corréler un déploiement de service avec la minute exacte où le taux d'erreur ou de cache hit en périphérie a basculé, sur l'ensemble des fournisseurs devant l'origine, plutôt que de laisser cette lecture à la reconstitution manuelle après coup.
Tester le contrat de remplacement
Avant de qualifier un service de sans état, exécutez des tests contrôlés :
- Supprimez un pod pendant un trafic proche du réel et vérifiez que les requêtes sont drainées ou réessayées en sécurité.
- Déployez une nouvelle version tout en conservant des requêtes longues et du travail en file.
- Augmentez et réduisez les réplicas en observant les connexions de base de données et la latence de queue.
- Simulez un délai d'attente de dépendance et vérifiez les échéances, les circuit breakers et le comportement de repli.
- Retirez un nœud ou une zone dans un environnement hors production et confirmez les hypothèses de topologie et de capacité.
- Mettez un nœud en cordon puis drainez-le avec
kubectl drain, ou utilisez un outil de chaos engineering, pour confirmer que le Pod Disruption Budget etunhealthyPodEvictionPolicyse comportent comme prévu sous une pression d'éviction réelle, et pas seulement sur le papier du manifeste.
Testez également un démarrage à froid. Une image qui met des minutes à être téléchargée ou initialisée ne peut pas répondre rapidement à un pic de trafic, même si l'autoscaler choisit le bon nombre de réplicas.
Scénario de référence : l'API de paiement d'Acme Shop
Reprenons Acme Shop comme fil rouge, cette fois sur son service checkout-api, qui doit pouvoir être drainé ou remplacé sans jamais perdre une décision de paiement. Il enregistre une clé d'idempotence et l'intention de commande dans PostgreSQL avant de publier le travail vers une file, met en cache localement les fiches produit à durée de vie courte, et stocke le jeton de reconnexion WebSocket hors du pod — quatre éléments d'état classés durable, durable, reconstructible et éphémère, chacun migré un à la fois par lecture double jusqu'à la fermeture de la fenêtre de rollback.
Dans le bac à sable, terminationGracePeriodSeconds est fixé à 45 secondes et l'endpoint /drain rend le pod « not ready » avant de vider les requêtes en vol, SIGTERM n'intervenant qu'en repli. Le service tourne sur trois zones, avec topologySpreadConstraints (maxSkew: 1) associé à un Pod Disruption Budget fixant minAvailable à 2 et unhealthyPodEvictionPolicy à AlwaysAllow, pour qu'un pod déjà en échec ne bloque pas un drain de nœud. Un kubectl rollout restart de test confirme que la commande reste enregistrée exactement une fois en base pendant le drainage ; en cas d'échec, l'équipe revient au ReplicaSet précédent et laisse l'enregistrement intact pour réconciliation. Les modes de défaillance restent instructifs au-delà d'Acme Shop : redémarrage qui duplique un traitement (handler non idempotent), requêtes qui échouent pendant un rollout (readiness restée vraie trop longtemps), erreurs de base de données à la mise à l'échelle (pool par pod trop large), réplicas concentrés dans une seule zone (topologySpreadConstraints absent).
Références faisant autorité
- The Twelve-Factor App: Processes
- Kubernetes: Deployments
- Kubernetes: Pod lifecycle and termination
- Kubernetes: Horizontal Pod Autoscaling
- Kubernetes: Pod topology spread constraints
- Kubernetes: Disruptions and Pod Disruption Budgets
- Kubernetes: Sidecar containers
- OpenTelemetry on Kubernetes
Gardez chaque pod remplaçable rapide, sûr et visible
Échangez avec Optimi sur la corrélation des événements de rollout, d'autoscaling et de drainage Kubernetes avec la Performance, la Sécurité et la Visibilité du chemin de livraison placé devant eux — suivies ensemble dans MYO, quel que soit le nombre de pods, de zones ou de fournisseurs derrière votre origine.
Évaluer la résilience de la livraison