L'incident VPN début décembre : une analyse rétrospective
TL;DR : la version courte
Le 2 décembre 2025, nous avons reçu une alerte indiquant qu'un composant système critique (Redis) était à court d'espace disque. La résolution du problème de capacité en soi était simple. Les vrais problèmes ont commencé lorsque nous avons essayé de « faire le ménage » après coup et d'adapter la configuration à la réalité.
Comme ce composant fonctionne sous Kubernetes et est configuré de manière à ne pas permettre un ajustement simple des ressources, nous avons tenté une solution de contournement qui semblait sûre : recréer l'objet contrôleur sans supprimer les pods en cours d'exécution. Kubernetes a interprété ce changement différemment de ce que nous avions prévu et a redémarré l'ensemble du cluster Redis. Le processus de redémarrage n'était pas assez « prudent », il s'est déroulé trop rapidement et a entraîné une dégradation partielle du cluster. Pendant que nous réparions les nœuds dégradés, un service backend a été submergé par le trafic de reconnexion et a commencé à rencontrer des erreurs de mémoire insuffisante. Après une mise à l'échelle et un réglage, le service a été rétabli. Le temps d'indisponibilité total a été d'environ 50 minutes.
Ce qui с'est en fait passé
Nous avons reçu une alerte indiquant que plusieurs instances du cluster Redis étaient à court d'espace disque disponible. Mais dans notre cas, augmenter l'espace disque pose certains problèmes. En effet, dans Kubernetes, les éléments tels que Redis sont généralement créés via un StatefulSet. Celui-ci est spécialement conçu pour les charges de travail qui nécessitent des identités et un stockage stables.
Cependant, certaines parties du StatefulSet sont verrouillées, ce qui peut poser problème. Les StatefulSets définissent le stockage via des volumeClaimTemplates. Quelques détails importants :
- le modèle de stockage est effectivement verrouillé une fois créé
- vous ne pouvez pas simplement le modifier sur place pour augmenter la taille du disque
Ainsi, même si vous savez exactement ce que vous voulez, Kubernetes ne vous permettra pas de modifier ce champ spécifique dans la spécification StatefulSet.
Un correctif immédiat : augmentation de capacité sans réinitialisation
Notre stockage interne prend en charge l'extension de volume en ligne. Cela signifie que nous pouvons augmenter la capacité du disque sous un pod en cours d'exécution sans le redémarrer.
Nous avons donc redimensionné directement les PersistentVolumeClaims (PVC). Cela a permis de résoudre le problème de surcharge du disque rapidement et en toute sécurité.
Mais cela a créé un nouveau problème :
- la configuration déclarative (Git + modèle StatefulSet) indiquait toujours l'ancienne taille du disque
- l'état réel des PVC/PV dans le cluster reflétait désormais la nouvelle taille du disque
Cette incompatibilité est un exemple classique de « dérive de configuration ».
La tentative de « nettoyage » : ajuster le code à la réalité
Le modèle de stockage StatefulSet étant immuable, la seule façon d'aligner l'état déclaré avec l'état réel consiste à :
- Supprimer l'objet StatefulSet.
- Le recréer avec le modèle de stockage mis à jour.
Bien sûr, la suppression du StatefulSet entraîne généralement la suppression des ressources secondaires. Nous avons donc utilisé une astuce Kubernetes :
Suppression orpheline : « licencier le responsable, maintenir l'usine opérationnelle »
Nous avons supprimé le StatefulSet à l'aide d'une suppression orpheline / non en cascade. L'idée était la suivante :
- Kubernetes oublie l'objet « responsable »...
- ... mais les pods (et leurs disques) restent actifs.
Nous avons ensuite recréé le StatefulSet afin que Git et le cluster soient à nouveau cohérents.
Cela semblait sûr, mais Kubernetes en a eu une interprétation différente.
L'imprévu : Kubernetes a redémarré l'ensemble du cluster Redis
Après avoir recréé le StatefulSet, Kubernetes a réconcilié la charge de travail en fonction des nouvelles spécifications. Même si les disques avaient déjà été redimensionnés, Kubernetes « ignorait » que toutes les modifications avaient déjà été appliquées. Il a donc lancé le processus de réconciliation, ce qui a entraîné le redémarrage complet du cluster Redis.
Un cluster de bases de données/caches distribué peut survivre à une séquence de redémarrage, mais uniquement si celui-ci est effectué avec prudence.
Cela nous amène au plus grand amplificateur de cet incident.
## Pourquoi la situation s'est aggravée : les redémarrages étaient trop optimistes.
Notre comportement de redémarrage n'était pas assez prudent :
- aucune vérification du démarrage et de la disponibilité
- un pod simplement « en cours d'exécution » était considéré comme « suffisamment opérationnel » (alors qu'en réalité, il ne pouvait toujours pas gérer la charge réelle des requêtes)
- Kubernetes passait trop rapidement au pod suivant
En clair : notre processus de redémarrage n'attendait pas vraiment que Redis soit en état de marche avant de passer à l'étape suivante.
Au début, cela ne semblait pas catastrophique : le service backend se plaignait des connexions Redis, mais les graphiques globaux du service ne s'effondraient pas immédiatement.
Cluster Redis dégradé : les répliques sont restées « bloquées » sur les anciens maîtres
Une fois la situation stabilisée, le cluster Redis s'est retrouvé partiellement dégradé :
- certains nœuds répliques ne pouvaient pas se reconnecter correctement
- ils conservaient des informations obsolètes sur le maître auquel ils appartenaient
À ce stade, la récupération nécessitait des opérations manuelles sur le cluster Redis :
- oublier les identités de nœuds obsolètes sur les maîtres
- réinitialiser les répliques affectées
- les reconnecter au cluster et attendre que la réplication rattrape son retard
Cette procédure a pris plus de temps que prévu. Et alors qu'elle était encore en cours, nous avons rencontré une deuxième défaillance.
Impact secondaire : surcharge du service backend et pannes dues à un manque de mémoire
Pendant que les nœuds Redis se reconnectaient et se synchronisaient, le service backend a été confronté à une avalanche de reconnexions et à une charge élevée. Cela s'est traduit par une cascade de pannes dues à un manque de mémoire.
Deux facteurs ont aggravé la situation :
- nous avons initialement mal identifié le composant qui était réellement en manque de mémoire
- la mise à l'échelle du service backend a été retardée en raison de la capacité de réserve limitée dans le cadre des contraintes actuelles en matière de ressources
Une fois le composant défaillant correctement identifié, nous avons stabilisé le système en :
- adaptant le service backend
- augmentant les limites de mémoire pour le composant concerné
- ... et en assouplissant temporairement les sondes pour permettre aux instances d'entrer immédiatement dans le service afin de faciliter la récupération. Sinon, cela aurait eu un effet en cascade : une fois que le contrôle du pod indique qu'un pod est « prêt », celui-ci est rapidement surchargé de trafic et redevient « non prêt ». Cela augmente la charge sur le pod suivant, la situation se répète, et ainsi de suite.
Après cela, le service s'est progressivement rétabli et le trafic des clients est revenu à la normale. La durée totale de l'indisponibilité a été d'environ 50 minutes : 8 h 10 UTC - 9 h 00 UTC.
La chronologie des événements
7 h 00 UTC : augmentation de la taille du disque dans Git.
7 h 05 UTC : mise à jour manuelle de la taille du disque pour tous les PVC du cluster.
7 h 10 UTC : tous les pods ont terminé le redimensionnement du disque en ligne.
8 h 10 UTC : suppression et recréation des sts orphelins.
8 h 10-8 h 15 UTC : Kubernetes a rapidement effectué un redémarrage complet de StatefulSet. C'est à ce moment-là que les problèmes ont commencé à apparaître.
8 h 15-8 h 35 UTC : restauration manuelle du cluster — recréation et réaffectation des répliques.
8 h 35-8 h 50 UTC : traitement des erreurs OOM de l'application. Une fois ces problèmes résolus, le service a été rétabli avec certaines limitations (réponses lentes, par exemple).
10 h 00 UTC : les solutions de contournement temporaires utilisées pour le redémarrage d'urgence du service ont été annulées. Le service est entièrement rétabli et opérationnel.
Les leçons apprises
1) Ce n'est pas le redimensionnement du disque qui a causé l'incident
La correction de la capacité était simple. L'incident a commencé avec l'étape d'alignement d'état et la recréation du contrôleur.
2) Les champs immuables vous poussent vers des workflows risqués
Lorsque le système ne vous permet pas de modifier un champ, la solution consiste souvent à le remplacer, ce qui peut déclencher un comportement de réconciliation à grande échelle.
3) Les sondes de démarrage et la disponibilité stricte ne sont pas seulement « agréables à avoir »
Elles constituent les freins d'un redémarrage en cours. Sans elles, les systèmes distribués redémarrent de la manière la plus fragile possible. C'est la pire partie de cet incident : la présence de sondes aurait empêché Kubernetes de redémarrer le pod suivant tant que le pod actuel n'était pas vraiment prêt.
4) Les plans de reprise doivent tenir compte d'une réparation lente du cluster
Les reconnexions et les rattrapages de réplication prennent du temps. Les services dépendants doivent être conçus pour se dégrader progressivement pendant cette période.
## Que va-t-on faire ?
1) Mettre en place des sondes de démarrage manquantes
Des sondes de démarrage simples telles que « ne pas marquer le pod comme « prêt » tant que l'ensemble de données n'est pas chargé » permettront d'éviter que cette situation ne se reproduise.
2) Créer notre propre « plan de contrôle » pour Redis
C'est une tâche plus complexe, mais elle s'avère très utile dans une situation où un pod redémarré est « bloqué » sur une ancienne adresse IP maître.
3) Réduire la dépendance à l'égard de ce cluster Redis
À l'heure actuelle, il semble que le cluster Redis soit le SPOF (point de défaillance unique) de l'ensemble du service. Nous prévoyons donc de migrer certaines informations importantes vers d'autres sources de données, comme un cluster Redis distinct pour chaque serveur. Nous prévoyons également de commencer à utiliser Kafka pour certains composants du backend.