Quand le failover n’est pas sûr : construire une haute disponibilité PostgreSQL sur Kubernetes
(datadoghq.com)- Comment résoudre une faiblesse structurelle dans des clusters PostgreSQL sur k8s où, en cas de panne réseau, le retard de réplication (replication lag) s’accumule jusqu’à rendre impossible un failover sûr
- L’architecture existante privilégiait la disponibilité (availability) à la durabilité (durability) : tant que le primaire continuait à accepter des écritures, les réplicas prenaient du retard, jusqu’à ce qu’il ne reste plus aucun candidat promouvable sans perte de données
- La solution a consisté à appliquer une réplication synchrone (synchronous replication) aux candidats au failover, orchestrée par le gestionnaire open source de haute disponibilité Patroni
- Avec un modèle de réplication hybride, seuls les standby du leader pool participent à la réplication synchrone, tandis que les read replicas restent en asynchrone, ce qui permet d’équilibrer durabilité et latence
- Malgré un coût en performances — par exemple une hausse de 53 % de la latence d’écriture avec le mode
remote_apply— la validation sur cinq scénarios de panne a permis d’obtenir un failover automatique et sûr
Le problème révélé pendant un game day
- Datadog organise régulièrement des game days afin d’identifier de manière proactive les failles dans les systèmes et les processus, en appliquant volontairement de la charge sur la plateforme pour observer sa réaction en conditions réelles
- Lors de l’un de ces game days, une panne de zone de disponibilité (AZ) a été simulée dans l’environnement de staging, provoquant une latence réseau qui a mis en lumière une faiblesse de l’architecture PostgreSQL
- Les nœuds primaire/writer de plusieurs clusters PostgreSQL sur Kubernetes fonctionnaient dans l’AZ affectée
- La forte hausse de latence réseau a empêché le primaire de communiquer de façon fiable avec les réplicas, augmentant le retard de réplication → blocage progressif des écritures → applications servant des données obsolètes
- Aucun réplica n’étant suffisamment à jour, un failover sûr n’était pas possible, et le cluster s’est retrouvé pratiquement à l’arrêt
- Cette architecture fonctionnait bien dans des conditions normales, mais dans certains scénarios de panne réseau elle privilégiait la disponibilité à la durabilité, sans chemin de reprise sûr
- Le primaire continuait à accepter des écritures pendant que la réplication prenait du retard, ce qui accentuait encore l’écart des réplicas
- Au final, il devenait impossible de promouvoir un candidat au failover sans risque de perte de données, et la seule option était d’attendre que la latence baisse et que les réplicas rattrapent leur retard
- L’objectif était de rendre le failover automatique et sûr sans dégrader inutilement les caractéristiques de performance de PostgreSQL
Architecture de référence : PostgreSQL sur Kubernetes
- Le cluster PostgreSQL sur Kubernetes se compose de deux pools, le leader pool et le read replica pool, PostgreSQL étant un système single-writer
- La séparation lecture/écriture permet d’étendre la capacité de lecture sans surcharger le leader, tout en gardant une latence d’écriture prévisible et stable
- Le leader pool se compose d’un unique nœud writer actif qui traite toutes les écritures, et de deux nœuds standby
- Les standby ne servent pas de trafic applicatif, mais peuvent être promus en cas de panne du leader
- Le read replica pool regroupe plusieurs nœuds qui traitent uniquement le trafic en lecture, optimisés pour l’extensibilité de lecture et l’isolation des requêtes, et volontairement exclus des cibles de failover
Rôle de Patroni et de ZooKeeper
- Patroni gère la réplication, le failover et l’élection du leader, en s’appuyant sur ZooKeeper comme distributed configuration store (DCS)
- ZooKeeper stocke les métadonnées de la clé/verrou du leader actuel, la configuration du cluster, ainsi que l’état de réplication de chaque membre, comme le LSN le plus récent
- Patroni utilise ces informations pour décider de manière prudente des promotions et rétrogradations, en privilégiant la cohérence des données à un failover agressif
- Lorsqu’un nouveau nœud rejoint le cluster, il commence par vérifier dans ZooKeeper si un leader existe déjà
- S’il n’y a pas de leader, il tente d’obtenir la clé de leader via la création d’un znode temporaire ; ZooKeeper garantit qu’un seul nœud peut obtenir cette clé, ce qui empêche la formation de plusieurs primaires
- S’il existe déjà un leader, le nœud se configure comme réplica et démarre la réplication en flux continu
- En cas de partition réseau (network partition), un réplica qui perd la connexion avec le leader ou avec ZooKeeper ne peut plus vérifier l’état du cluster ; Patroni met alors ce nœud en pause ou le rétrograde temporairement
- Si le leader perd sa connexion, Patroni coopère avec ZooKeeper pour qu’un seul standby éligible obtienne le verrou de leader, garantissant un failover contrôlé même lors de pannes réseau partielles
- Une fois la connectivité rétablie, l’ancien leader, s’il ne parvient pas à récupérer le verrou, se rétrograde lui-même en standby, évitant ainsi le split brain
Pourquoi un failover sûr était impossible
- Dans un modèle single-writer, en cas de panne, Patroni élit un nouveau leader parmi les standby sains
- Pour éviter toute perte de données, Patroni effectue des vérifications de sécurité avant la promotion, la plus importante étant que le retard de réplication reste sous le seuil
maximum_lag_on_failover- Si un standby est en retard sur le leader, sa promotion peut entraîner des données manquantes ou incohérentes
- Pendant le game day, lorsque le primaire a perdu sa connectivité, le retard de réplication de tous les standby a dépassé ce seuil, et Patroni a donc refusé le failover à juste titre
- Si le cluster s’est retrouvé sans primaire sûr capable d’accepter des écritures, ce n’était pas à cause de Patroni, mais parce qu’il n’existait aucun candidat sûr à promouvoir
Les deux modes de la réplication en flux continu
- Dans la réplication en flux continu, le leader envoie en permanence aux réplicas les write-ahead logs (WAL) contenant toutes les modifications, et les réplicas les appliquent localement pour rester synchronisés
-
Réplication asynchrone (par défaut)
- Le leader n’attend pas l’accusé de réception des réplicas avant de valider une transaction
- Latence d’écriture minimale et haut débit
- En revanche, si le leader tombe, des transactions validées sur le primaire mais pas encore répliquées peuvent être perdues lors de la promotion
-
Réplication synchrone
- Le leader attend l’accusé de réception d’au moins un réplica avant de répondre au client
- Cela réduit fortement le risque qu’au moins un réplica prenne trop de retard, et offre une durabilité plus forte en confirmant qu’une transaction validée existe déjà sur un autre nœud avant de répondre
- Un candidat au failover a ainsi bien plus de chances d’être à jour et d’être promu sans risque de divergence de données
Configuration de réplication hybride
- Pour équilibrer durabilité, latence et débit, un modèle de réplication hybride a été adopté
- Les nœuds standby du leader pool participent à la réplication synchrone, et le leader ne valide l’écriture qu’après accusé de réception du standby synchrone désigné
- Les read replicas restent en réplication asynchrone, car ils ne traitent que du trafic en lecture et ne sont pas des cibles de failover, ce qui limite la charge de réplication sur le leader pool
- Cette approche permet d’imposer de fortes garanties de durabilité uniquement aux candidats au failover, sans faire subir le même coût en latence aux réplicas de lecture
Ajuster PostgreSQL et Patroni pour un failover sûr
- L’activation de la réplication synchrone a nécessité l’ajustement de paramètres à la fois dans PostgreSQL et dans Patroni
-
Principaux paramètres modifiés
synchronous_mode: active la réplication synchrone côté Patroni ; sitrue, le commit attend la confirmation des standby synchrones selonsynchronous_node_count(valeur par défaut false → true, géré par Patroni, requis)synchronous_node_count: nombre de nœuds standby synchrones, utilisé pour générer la listesynchronous_standby_names(valeur par défaut 1 → 1, géré par Patroni, optionnel)synchronous_mode_strict: impose le mode synchrone strict ; sitrueet qu’aucun réplica n’est disponible, les écritures sont bloquées au lieu de repasser en asynchrone (valeur par défaut false → true, géré par Patroni, optionnel)synchronous_commit: réglage PostgreSQL de la durabilité du commit (valeur par défaut on → remote_apply, géré par PostgreSQL, optionnel)
- Après application, le leader n’envoie la réponse de transaction au client qu’une fois que le standby synchrone a confirmé la réception et l’application des données
Équilibrer durabilité et latence
- La réplication synchrone améliore la durabilité, mais augmente la latence d’écriture puisque le leader attend l’accusé de réception du standby synchrone ; elle peut aussi affecter le débit sous charge soutenue
- L’impact sur les performances dépend du nombre de standby synchrones (
synchronous_node_count) et du niveau de durabilité configuré viasynchronous_commit -
Arbitrages selon les niveaux de durabilité de
synchronous_commitremote_apply: attend que le réplica écrive, flush et rejoue le WAL ; cohérence maximale mais latence la plus élevéeon(en interneremote_flush) : attend que le réplica flush le WAL sur disque ; durabilité forte, mais données pas encore lisibles sur le standbyremote_write: attend que le WAL atteigne le cache OS du réplica, pas le disque ; latence plus faible mais vulnérable à un crash de l’OSlocal: commit après flush sur disque local sans attendre de standby ; aucune garantie de durabilité entre nœudsoff: commit avant le flush local du WAL ; latence minimale mais risque maximal de perte de données
Benchmark des performances de la réplication synchrone
- Chaque commit devant attendre la confirmation d’au moins un standby, la réplication synchrone ajoute de la latence ; pour quantifier cet impact, des benchmarks ont été réalisés avec pgbench, l’outil standard de test de charge de PostgreSQL (Patroni version 3.2.1)
- La suite de transactions TPC-B, qui simule un mélange simple de lectures et d’écritures, a été utilisée, avec deux métriques mesurées
- Latence moyenne : temps moyen de traitement par transaction (ms)
- Transactions par seconde (tps) : nombre de transactions terminées, hors temps d’établissement des connexions
-
Paramètres de test
- Le facteur d’échelle (taille de base), le nombre de clients (trafic utilisateur concurrent), le nombre de threads (CPU et parallélisme) et le nombre de transactions (intensité de charge) ont été variés pour simuler des conditions proches de la production
- Le mode de commit synchrone par quorum n’a pas été testé dans ce benchmark
-
Résultats du benchmark
- Hausse de la latence d’écriture :
remote_apply53 %,on46 %,remote_write38 %,local32 % - Baisse du débit (tps) :
remote_apply34 %,on31 %,remote_write27 %,local23 % remote_applya systématiquement présenté la latence la plus élevée et le débit le plus faible, car il attend le rejeu et l’application du WAL sur le réplica ; il reste toutefois le mieux adapté à un failover sûr grâce à sa cohérence maximale
- Hausse de la latence d’écriture :
-
Déploiement en production
- Après benchmarking,
remote_applya été déployé sur plusieurs clusters à forte charge d’écriture, sans impact significatif observé au niveau applicatif sur la latence d’écriture ni le débit, même sous charge continue en production - Pour réduire le risque de performance, le déploiement a été progressif par datacenter et par couche de workload, avec périodes de rodage et surveillance continue entre chaque étape
- Exemple : une charge de traitement de ressources à haut débit a continué à fonctionner sans retard de traitement ni backlog en aval malgré l’augmentation de la latence d’écriture en base après activation de la réplication synchrone
synchronous_commitpeut être ajusté immédiatement et sans interruption viapatronictl edit-config, offrant la flexibilité de réduire rapidement la durabilité des commits pour des workloads à très haut débit
- Après benchmarking,
Validation du failover à travers des scénarios de panne
- Il fallait vérifier que la réplication synchrone et les contrôles stricts de failover protègent bien l’intégrité des données, empêchent le split brain et garantissent une reprise automatique
-
Scénario 1 : perte d’un standby synchrone
- En cas de perte d’un standby synchrone, Patroni essaie d’assigner un autre standby éligible afin de maintenir la réplication synchrone
- Patroni sur le nœud leader détecte les connexions de streaming rompues, bloquées ou retardées via
pg_stat_replication, et suit l’appartenance des réplicas via ZooKeeper - Il recalcule la liste des réplicas de streaming sains et éligibles, puis met à jour
synchronous_standby_namesselonsynchronous_node_count, de façon à maintenir la réplication synchrone active
-
Scénario 2 : perte de tous les standby synchrones
- Le comportement dépend de la valeur de
synchronous_mode_strict -
Mode non strict : priorité à la disponibilité des écritures
- Patroni vide
synchronous_standby_nameset désactive temporairement la réplication synchrone, permettant au leader de repasser en asynchrone et de continuer à accepter des écritures jusqu’au retour d’un réplica sain
- Patroni vide
-
Mode strict : blocage des écritures pour la sécurité
- Patroni règle
synchronous_standby_namessur*; même sans standby synchrone explicitement désigné, PostgreSQL accepte et valide localement les transactions d’écriture mais bloque la réponse au client jusqu’à ce qu’un réplica accuse réception du WAL - Lorsqu’un réplica apte à rejoindre la réplication synchrone se reconnecte, Patroni lui attribue le rôle de standby synchrone
- Patroni règle
- Le comportement dépend de la valeur de
-
Scénario 3 : indisponibilité de tous les standby et réplicas
- Si tous les réplicas sont indisponibles et que
synchronous_mode_strict = true, PostgreSQL suspend la confirmation des transactions jusqu’au retour d’au moins un réplica éligible - La cohérence des données est préservée, mais l’application subit une indisponibilité temporaire en écriture
- Si tous les réplicas sont indisponibles et que
-
Scénario 4 : panne du leader pendant un commit synchrone
- Cas limite où le leader tombe alors qu’il attend encore l’accusé de réception du standby synchrone
- Causes fréquentes : annulation de transaction par le client pendant le commit, crash ou arrêt du processus PostgreSQL leader, partition réseau pendant l’étape de commit
- Si PostgreSQL a flush le WAL localement mais n’a pas réussi à le répliquer vers le standby, faute d’accusé de réception la transaction n’apparaît pas sur le réplica
- Si le leader plante avant de répliquer le WAL vers le standby synchrone et que ce standby est promu, la transaction peut être perdue, avec divergence d’historique entre l’ancien leader et le nouveau primaire
- L’ancien leader utilise alors pg_rewind pour identifier le point de divergence de timeline et réaligner son répertoire de données sur la timeline du nouveau primaire, en abandonnant les changements locaux non répliqués avant de rejoindre de nouveau le cluster comme standby
- Ce comportement ne vient pas de Patroni mais du fonctionnement interne du commit synchrone dans PostgreSQL, ce qui souligne la nécessité d’un réglage et d’une supervision attentifs des mécanismes de quorum et d’accusé de réception
-
Scénario 5 : indisponibilité de ZooKeeper
- Si ZooKeeper devient indisponible, Patroni ne peut plus confirmer le leadership ni organiser une nouvelle élection ; il bascule donc sur un comportement conservateur pour éviter toute incohérence de données
-
Avec le mode failsafe désactivé
- Même sans accès à ZooKeeper, si le leader reste joignable et que tous les nœuds sont sains, les écritures peuvent continuer, mais seulement jusqu’à l’expiration du TTL du verrou leader
- Une fois le délai de rafraîchissement du verrou dépassé et le renouvellement impossible, Patroni rétrograde le leader et bascule le cluster en lecture seule
-
Avec le mode failsafe activé
- Si le leader perd sa connexion à ZooKeeper, Patroni vérifie en continu via l’API REST que tous les membres du cluster restent joignables
- Les écritures ne continuent que si tous les membres sont accessibles ; sinon, le leader est rétrogradé en lecture seule
Failover et switchover manuels sous réplication synchrone
- En plus du failover et du switchover automatiques basés sur les health checks et la coordination via ZooKeeper, Patroni permet aussi des opérations manuelles via la commande
patronictl; avec la réplication synchrone active, tous les standby ne sont pas des candidats valides, d’où l’application de garde-fous pour protéger l’intégrité des données -
Failover vers un standby asynchrone
patronictl failover: échec si le nœud choisi est asynchronepatronictl switchover: échec si le nœud choisi est asynchrone- Forcer un failover vers un nœud asynchrone alors que la réplication synchrone est active contournerait les garanties de durabilité et pourrait provoquer une perte de données
-
Lorsqu’on cible un standby synchrone
patronictl failover: succès, le leader bascule vers le standby synchronepatronictl switchover: succès, avec transmission propre du rôle entre le leader et le standby synchrone
- Après validation du comportement des différents modes
synchronous_commitet des garde-fous Patroni, la réplication synchrone a été activée sur des clusters de production à forte écriture, forte lecture et charge mixte, sans impact mesurable sur la latence ni le débit - En cas de problème, un retour sûr à la réplication asynchrone reste possible sans interruption via le paramètre
synchronous_mode: false
Pourquoi DRBD n’a pas été retenu
- Lors de l’évaluation de la haute disponibilité, le système de réplication au niveau bloc DRBD (Distributed Replicated Block Device) a également été étudié ; il crée un réplica standby quasi temps réel en miroir de volumes entiers entre serveurs, y compris le répertoire de données PostgreSQL et les fichiers WAL
- DRBD peut offrir une latence plus faible que la réplication en flux continu intégrée à PostgreSQL, mais exigerait un changement d’architecture important, avec nouvelle infrastructure, supervision et playbooks opérationnels
- Compte tenu de la maturité de l’environnement existant sur Kubernetes et du contrôle fin offert par la réplication synchrone de PostgreSQL, le choix s’est porté sur une réplication au niveau base de données, jugée meilleure en visibilité, flexibilité et fiabilité opérationnelle
Supervision de la réplication synchrone
- Après activation de la réplication synchrone, l’état de réplication et la capacité de failover ont été suivis de près, deux signaux en particulier contribuant à maintenir la stabilité à grande échelle
-
Événement d’attente SyncRep
- Il se produit lorsque le primaire attend l’accusé de réception du standby synchrone avant de terminer un commit et de retourner l’état ; une certaine attente est normale, mais des attentes longues ou fréquentes peuvent indiquer des problèmes de performance côté réplica ou de latence réseau entre nœuds
- Pourquoi c’est important : une attente prolongée augmente la latence d’écriture et réduit le débit
- À suivre : durée et fréquence des événements d’attente
SyncRepetWalSenderWaitForReply, collectés dans la métrique Datadogpostgresql.activity.waitsfiltrée avec le tagwait_event:SyncRep(interrogation interne de la tablepg_stat_activity)
-
Absence de standby synchrone détecté
- Si Patroni ne détecte aucun standby synchrone sain pendant une longue période, le cluster perd sa capacité de failover sûr
- Pourquoi c’est important : sans standby synchrone, le failover redevient vulnérable à la perte de données
- Critère d’alerte : si
patroni_sync_standbyreste vide durablement, une alerte de santé haute disponibilité (HA) est déclenchée ; la donnée est collectée via OpenMetrics, sans intégration Datadog native
- La réplication synchrone améliore la durabilité, mais peut dégrader disponibilité et performances lorsque les réplicas sont anormaux ou injoignables ; la surveillance des temps d’attente et de la disponibilité des standby est donc essentielle pour préserver disponibilité et performances sous charge
Un failover sûr dès la conception
- La panne AZ simulée a révélé une faiblesse critique de l’architecture PostgreSQL : les réplicas prenaient trop de retard sur le leader, imposant un choix intenable entre attendre la fin de la panne réseau ou accepter une divergence de données, un compromis inacceptable en production
- En adoptant la réplication synchrone avec Patroni et en ajustant finement durabilité et latence, l’équipe a rendu le failover possible et sûr même dans des conditions réseau dégradées ; les benchmarks et simulations répétées de panne ont confirmé une reprise prévisible sans dégradation notable des performances à grande échelle
- En bloquant les écritures lors des défaillances de réplication synchrone, le système expose explicitement l’échec aux services amont, au lieu de perdre silencieusement des écritures comme avec la réplication asynchrone, ce qui permet d’y répondre par des retries, de la mise en file d’attente, etc., pour un mode de panne plus visible et plus récupérable
- À l’avenir, exploration prévue de modes de commit basés sur le quorum et d’une observabilité plus poussée de l’état de réplication
Aucun commentaire pour le moment.