03. Résilience
Trois mécanismes indépendants, trois questions différentes — et un scénario de drain qui révèle une conséquence non évidente.
Pending — c'est attendu.
Trois mécanismes, trois questions#
| Mécanisme | Question | Agit | Protège de |
|---|---|---|---|
| Anti-affinité | Où placer les pods ? | au scheduling | la perte d'un nœud |
| HPA | Combien de pods ? | en continu | la montée de charge |
| PodDisruptionBudget | Combien peut-on en retirer ? | aux évictions via l'API Eviction | nos propres opérations |
La confusion la plus fréquente porte sur le dernier : un PDB ne protège
pas d'un crash. Si un nœud brûle, les pods meurent, PDB ou pas. Il ne
contraint que les évictions qui passent par l'API Eviction
— un kubectl drain, un vidage de nœud déclenché par
l'autoscaler. C'est déjà énorme : ce sont les perturbations qu'on déclenche
soi-même, donc celles qu'on peut rendre sûres.
C'est l'erreur qui suit immédiatement. Un rolling update de Deployment et un
kubectl rollout restart sont pilotés par le contrôleur du Deployment
— maxUnavailable, maxSurge, sondes de readiness
— et ne consultent pas le PDB. Une suppression directe de
pod ou de Deployment le contourne également. Deux mécanismes distincts, deux
réglages distincts : « j'ai un PDB, donc mon déploiement est protégé » est
faux.
Anti-affinité : requise pour les données, préférée pour le reste#
| Workload | Type | Effet si le placement est impossible |
|---|---|---|
| PostgreSQL (CNPG ×3) | required | Le pod reste Pending |
api ×2 | preferred (poids 100) | Le pod se schedule quand même, colocalisé |
mail-worker ×2 | preferred | idem |
frontend ×2 | preferred | idem |
| RabbitMQ ×3 | preferred | idem |
Le choix required protège la séparation des instances ; preferred privilégie la facilité de placement. Cette décision dépend du stockage, de la réplication, du nombre de nœuds disponibles et des opérations de maintenance, pas seulement du caractère stateful du workload. PostgreSQL asynchrone n’exige pas le quorum de réplicas décrit pour RabbitMQ.
preferred. C'est un compromis : sur un cluster de développement, un
cluster RabbitMQ bloqué en Pending gêne plus qu'il ne protège. Sur un
vrai cluster, required se défendrait — la question mérite
d'être reposée le jour où l'infrastructure sort du portable.
La formule « 3 nœuds » induit en erreur. Les nœuds répliquent les métadonnées du cluster ; le contenu d'une queue classique vit sur un seul nœud, et RabbitMQ 4 a supprimé le mécanisme historique des mirrored classic queues. Ici, rien ne demande autre chose : la perte permanente du nœud et de son volume perd les messages ; une interruption temporaire les rend indisponibles jusqu’au retour du stockage.
Une quorum queue réplique ses messages et requiert une majorité de ses membres. Le dépôt utilise des queues classiques durables et des messages persistants. Il a déjà les ACK consumers via incoming.process() et les publisher confirms activés par défaut par aio-pika.channel(). Il manque surtout la réplication des queues et une stratégie d’idempotence. Voir aio-pika et les quorum queues.
HPA : deux métriques, et le plus exigeant l'emporte#
# k8s/hpa.yaml
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: api }
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
- type: Resource
resource:
name: memory
target: { type: Utilization, averageUtilization: 80 }| Cible | Min → Max | CPU | Mémoire |
|---|---|---|---|
api | 2 → 6 | 70 % | 80 % |
frontend | 2 → 4 | 70 % | 80 % |
mail-worker | bloc présent mais commenté — voir plus bas | ||
Trois points à comprendre pour que ces chiffres veuillent dire quelque chose :
- Le pourcentage se calcule sur les
requests, pas sur leslimitsni sur la capacité du nœud. L'API demande 50m de CPU : 70 % signifie 35m consommés en moyenne. Une request mal choisie donne un autoscaling absurde — c'est le lien direct avec le chapitre 02. - Deux métriques, pas une condition ET. Le HPA calcule une recommandation par métrique et retient la plus élevée. Un pic mémoire seul suffit donc à déclencher la montée.
minReplicas: 2, jamais 1. Ce n'est pas une question de charge mais de disponibilité : descendre à 1 rendrait le PDBminAvailable: 1impossible à satisfaire, et bloquerait tout drain. Les deux réglages se tiennent.
<unknown> et n'ajuste jamais. Sur Kind, il faut en
plus le patcher avec --kubelet-insecure-tls, sinon il refuse les
certificats auto-signés des kubelets. deploy.sh fait les deux, en
vérifiant d'abord si le drapeau est déjà présent — en sachant que ce drapeau
désactive la vérification du certificat kubelet et ne se transpose
pas hors d'un cluster local (voir
l'encadré du sommaire).
Pourquoi le worker n'est pas autoscalé#
Le bloc existe dans hpa.yaml, entièrement commenté. Le fichier ne
dit pas pourquoi ; voici l'argument qui justifie de le laisser ainsi.
Un consommateur de file passe l'essentiel de son temps à attendre — un message, une réponse SMTP. Son CPU reste bas même quand 10 000 messages s'accumulent : autoscaler dessus revient à mesurer la mauvaise chose. Le signal pertinent est la profondeur de la file ou l'âge du plus vieux message, que Kubernetes ne connaît pas nativement — il faut KEDA, ou un adaptateur de métriques externes.
Laisser le bloc commenté plutôt que de le supprimer est un choix défendable :
il documente une intention, et rend explicite le fait que replicas: 2
est un dimensionnement manuel assumé. Deux consommateurs concurrents sur la même
file, c'est déjà du parallélisme — simplement pas élastique.
PodDisruptionBudget : rendre nos propres opérations sûres#
# k8s/pdb.yaml — un budget par workload applicatif
spec:
minAvailable: 1
selector:
matchLabels: { app: api } # puis frontend, puis mail-workerAvec 2 replicas et minAvailable: 1, Kubernetes accepte d'évincer
un pod à la fois. Un drain qui voudrait en retirer
deux attend. C'est ce qui transforme une mise à jour de nœud d'un « ça devrait
passer » en une garantie.
Aucun PDB supplémentaire n’est déclaré ici pour les ressources d’opérateurs. Cela ne signifie pas qu’elles n’en ont pas : CloudNativePG gère des PDB selon sa configuration. Inspecter kubectl get pdb -n task-manager et la procédure de maintenance de la version installée. Un drain peut être bloqué ; l’opérateur n’est pas une garantie que toute éviction est autorisée.
Ce qui se passe vraiment quand on draine un nœud#
Scénario à observer, si les PDB effectifs autorisent les évictions. Un drain peut attendre et ne garantit pas que toutes les étapes suivantes se produisent. Ne pas désactiver les protections pour obtenir artificiellement le résultat attendu.
- Le nœud est cordonné : plus aucun nouveau pod n'y est planifié.
- Les pods sans état sont évincés un par un, dans le respect des PDB. L'API passe momentanément à 1 replica servant, puis remonte à 2 dès que le remplaçant est prêt sur un autre nœud.
- Si l'instance PostgreSQL évincée était le primaire, CNPG
promeut un réplica. Le Service
-rwsuit automatiquement et le pooler rétablit ses connexions ;pool_pre_ping(ch. 01) réduit les erreurs dues aux connexions devenues obsolètes. Il ne rend pas la bascule transparente pour autant : les transactions en vol au moment du failover échouent, et en réplication asynchrone les écritures non encore répliquées sont perdues. C'est à l'application de tolérer ces erreurs et de rejouer — le pooler ne fait que réduire le bruit. - L'instance évincée reste en
Pending. Pour deux raisons indépendantes, et il faut lever les deux (ch. 06) : son volume est un répertoire du nœud cordonné, et l'anti-affinitérequiredexige un nœud sans instance PostgreSQL : avec 3 workers dont un cordonné, il n'y en a plus. Elle reviendra auuncordon.
required se paie en capacité de manœuvre, et il faut
le savoir avant de lancer la commande, pas en lisant
kubectl get pods.
Ce qui n'est pas couvert#
- La redondance matérielle. Les nœuds sont des conteneurs sur une seule machine : Kind reproduit les comportements de Kubernetes, pas la tolérance à la panne d'un serveur.
- La sauvegarde continue. CNPG sait faire du PITR vers un stockage objet ; ce dépôt s'en tient à des dumps logiques (ch. 05). On restaure à l'instant d'un dump, pas à la seconde près.
- Les budgets et procédures de maintenance des opérateurs doivent être inspectés sur le cluster ; l’absence de PDB dans ce dépôt ne signifie pas absence de PDB généré.
- La reprise du worker après crash. Elle repose sur l'accusé de réception RabbitMQ, pas sur Kubernetes : un message non acquitté est redélivré (cours ch. 11).
À retenir#
- Trois mécanismes, trois questions : où (anti-affinité), combien (HPA), combien on peut retirer (PDB).
- Un PDB ne protège pas d'un crash : uniquement des perturbations volontaires.
- L’anti-affinité requise est un compromis de disponibilité et de placement ; elle n’est pas obligatoire pour tout workload avec état.
- Le HPA calcule sur les
requestset retient la métrique la plus exigeante ; sans Metrics Server il ne fait rien. minReplicas: 2etminAvailable: 1se tiennent : à 1 replica, plus aucun drain n'est possible.- Une contrainte
requiredse paie en capacité de manœuvre — un drain laisse une instance enPending, et c'est attendu.