Pourquoi la haute disponibilité PostgreSQL sur VPS — et pas RDS
Amazon RDS Multi-AZ résout bien le failover, mais son modèle tarifaire est conçu pour que la facture suive la croissance de façon non linéaire : stockage, IOPS provisionnées, connexions simultanées et la réplication Multi-AZ elle-même se facturent séparément. Pour un SaaS dont la base grossit, le surcoût dépasse vite le coût d'un VPS dédié.
Sur trois VPS avec accès root, Patroni assure le même service : un seul leader accepte les écritures, deux répliques synchrones ou asynchrones le suivent, et etcd tient le quorum. Si le leader tombe, Patroni détecte l'absence via le bail etcd (TTL configurable, typiquement 30 secondes) et promeut la réplique la plus à jour. Aucune intervention humaine, aucun DNS à mettre à jour manuellement si un répartiteur de charge comme HAProxy pointe vers l'endpoint de santé de Patroni.
Ce que ce cluster vous apporte
- Failover automatique sous 30 secondes — Patroni détecte la perte du leader par expiration du bail etcd et promeut sans intervention.
- Réplication synchrone configurable —
synchronous_mode: truegarantit qu'aucune transaction validée n'est perdue en cas de crash du leader. - API REST intégrée —
GET /leader,GET /health,POST /switchover: le cluster s'interroge et se pilote sans client PostgreSQL. - Coût fixe et prévisible — trois VPS à ressources fixes, sans surprise de facturation liée au trafic.
- Extensions libres —
pg_hba.conf,postgresql.conf,pgvector,PostGIS: aucune restriction imposée par un service managé. - Sauvegardes centralisées — pgBackRest 2.59 s'intègre nativement avec Patroni pour des sauvegardes incrémentales depuis la réplique.
Architecture du cluster : trois nœuds, un quorum
Le cluster repose sur trois couches :
etcd tient le registre de configuration distribué (DCS). C'est lui qui détient le bail de leader. Si le leader Patroni ne renouvelle pas ce bail dans le délai TTL, etcd le libère et les standbys se candidatent à l'élection. Avec trois nœuds etcd (un par VPS), le quorum tolère la perte d'un nœud sans perdre la disponibilité.
Patroni tourne sur chaque VPS aux côtés de PostgreSQL. Il s'occupe de l'initialisation du cluster, de la configuration de postgresql.conf et de pg_hba.conf, du suivi du lag de réplication, et du failover. Il expose une API REST sur le port 8008.
PostgreSQL est géré entièrement par Patroni — ne pas éditer postgresql.conf directement, toute modification passe par patronictl edit-config pour rester synchronisée sur les trois nœuds.
Le flux de réplication : le leader reçoit les écritures en WAL, les répliques se connectent via pg_basebackup au premier démarrage puis suivent le flux WAL en continu. En mode synchrone, le leader attend la confirmation d'au moins une réplique avant de retourner COMMIT au client.
Prérequis : ressources et réseau
Ce guide a été écrit avec Patroni 4.1.5, etcd 3.6.6 et PostgreSQL 17 sur Debian 12.
Ressources minimales recommandées par nœud
- 2 vCPU / 4 Go RAM — suffisant pour démarrer ; prévoir 8 Go RAM dès que la base dépasse quelques Go de
shared_buffers. - SSD NVMe — la réplication WAL est sensible à la latence d'écriture ; un disque magnétique dégrade le lag de réplication.
- Réseau privé entre les trois nœuds — la communication etcd-etcd et Patroni-PostgreSQL ne doit pas transiter par Internet.
- IPv4 dédiée — pour l'accès client externe et le
pg_hba.confdes répliques. - NTP synchronisé (chrony) — dérive < 1 s — etcd refuse le quorum si l'horloge d'un nœud dérive de plus d'une seconde. C'est le piège le plus fréquent sur VPS.
Installation : de zéro au cluster opérationnel
Synchroniser l'horloge sur les trois nœuds
Sur chaque nœud, installez et activez chrony avant toute autre opération :
apt install -y chrony
systemctl enable --now chronyd
chronyc trackingVérifiez que System time offset est inférieur à 0,1 seconde. Une dérive supérieure à 1 seconde provoque des timeouts etcd et des élections en boucle.
Installer etcd 3.6 sur les trois nœuds
Définissez les variables d'environnement propres à chaque nœud (remplacez NODE1_IP, NODE2_IP, NODE3_IP par les IP privées) :
ETCD_VER=v3.6.6
curl -L https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz \
| tar xz -C /usr/local/bin --strip-components=1 etcd-${ETCD_VER}-linux-amd64/etcd \
etcd-${ETCD_VER}-linux-amd64/etcdctlCréez /etc/etcd/etcd.conf.yml sur chaque nœud (exemple pour pg-node1) :
name: pg-node1
data-dir: /var/lib/etcd
listen-peer-urls: http://NODE1_IP:2380
listen-client-urls: http://NODE1_IP:2379,http://127.0.0.1:2379
initial-advertise-peer-urls: http://NODE1_IP:2380
advertise-client-urls: http://NODE1_IP:2379
initial-cluster: pg-node1=http://NODE1_IP:2380,pg-node2=http://NODE2_IP:2380,pg-node3=http://NODE3_IP:2380
initial-cluster-token: pg-cluster-token
initial-cluster-state: newCréez l'unité systemd, activez et démarrez etcd sur les trois nœuds avant de passer à l'étape suivante.
Vérifier le quorum etcd
Sur n'importe quel nœud :
etcdctl --endpoints=http://NODE1_IP:2379,http://NODE2_IP:2379,http://NODE3_IP:2379 \
endpoint status --write-out=tableAttendez que les trois lignes affichent IS LEADER pour l'une et false pour les deux autres, et que ERRORS soit vide. Si un nœud n'apparaît pas, vérifiez le pare-feu sur les ports 2379 et 2380.
Installer PostgreSQL et Patroni
Sur les trois nœuds :
# PostgreSQL depuis le dépôt officiel PGDG
apt install -y curl ca-certificates
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg
echo "deb https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list
apt update && apt install -y postgresql-17
# Patroni et le driver etcd
pip3 install patroni[etcd3] psycopg2-binaryArrêtez PostgreSQL — Patroni prend en charge l'initialisation du cluster :
systemctl stop postgresql
systemctl disable postgresqlConfigurer Patroni sur chaque nœud
Créez /etc/patroni/patroni.yml (exemple pour pg-node1) :
scope: pg-cluster
namespace: /service/
name: pg-node1
restapi:
listen: NODE1_IP:8008
connect_address: NODE1_IP:8008
etcd3:
hosts: NODE1_IP:2379,NODE2_IP:2379,NODE3_IP:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
synchronous_mode: true
synchronous_node_count: 1
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
hot_standby: "on"
wal_keep_size: 128MB
max_wal_senders: 10
max_replication_slots: 10
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 0.0.0.0/0 scram-sha-256
- host all all 0.0.0.0/0 scram-sha-256
postgresql:
listen: NODE1_IP:5432
connect_address: NODE1_IP:5432
data_dir: /var/lib/postgresql/17/main
bin_dir: /usr/lib/postgresql/17/bin
authentication:
replication:
username: replicator
password: 'VOTRE_MOT_DE_PASSE_REPLICATION'
superuser:
username: postgres
password: 'VOTRE_MOT_DE_PASSE_POSTGRES'Adaptez NODE1_IP pour chaque nœud.
Démarrer Patroni et initialiser le cluster
Créez l'unité systemd /etc/systemd/system/patroni.service :
[Unit]
Description=Patroni PostgreSQL HA
After=network.target etcd.service
Requires=etcd.service
[Service]
Type=simple
User=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.targetDémarrez d'abord sur pg-node1 (ce nœud effectue initdb et devient leader), puis sur les deux autres avec un délai de quelques secondes :
systemctl daemon-reload
systemctl enable --now patroniSuivez l'initialisation :
patronictl -c /etc/patroni/patroni.yml listVérifier l'état initial du cluster
Sortie attendue après initialisation complète :
+ Cluster: pg-cluster (7234567890123456789) +---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+-----------+------------------+---------+---------+----+-----------+
| pg-node1 | NODE1_IP:5432 | Leader | running | 1 | |
| pg-node2 | NODE2_IP:5432 | Sync Standby | running | 1 | 0 |
| pg-node3 | NODE3_IP:5432 | Replica | running | 1 | 0 |
+-----------+------------------+---------+---------+----+-----------+pg-node2 apparaît comme Sync Standby : toute transaction validée sur le leader est garantie sur ce nœud avant que le COMMIT ne soit retourné au client.
Configurer pg_hba.conf via Patroni
Ne modifiez jamais pg_hba.conf directement. Utilisez patronictl edit-config pour ajouter des règles d'accès dans la section pg_hba — Patroni propage la configuration sur tous les nœuds et recharge PostgreSQL automatiquement :
patronictl -c /etc/patroni/patroni.yml edit-configAjoutez vos règles dans le bloc pg_hba du YAML. Sur VPS, la règle host all all 0.0.0.0/0 scram-sha-256 est un point de départ à affiner selon votre réseau privé.
Failover et switchover : démonstration
Failover simulé — arrêt brutal du leader.
Avant l'arrêt, notez l'état du cluster :
patronictl -c /etc/patroni/patroni.yml list
# → pg-node1 est Leader, pg-node2 est Sync StandbyArretez Patroni sur le leader :
systemctl stop patroni # sur pg-node1Suivez la promotion sur un des standbys :
patronictl -c /etc/patroni/patroni.yml list
# → (après 10 à 30 secondes)
# pg-node2 : Leader | running | TL 2
# pg-node3 : Replica | running | TL 2 | 0 MB
# pg-node1 : stoppedPatroni attend l'expiration du bail etcd (TTL = 30 s), puis pg-node2 (le sync standby) acquiert le bail et se promeut. Le délai effectif est typiquement entre 10 et 30 secondes selon la valeur de loop_wait.
Switchover planifié — bascule sans coupure.
Pour une maintenance programmée, préférez switchover qui attend que la réplique cible soit à jour avant de basculer :
patronictl -c /etc/patroni/patroni.yml switchover pg-cluster \
--master pg-node1 --candidate pg-node2Patroni attend que le lag soit nul, donne le signal de promotion à pg-node2, puis pg-node1 se reconnecte comme réplique. Durée effective : moins de 5 secondes en conditions normales.
API REST de statut.
Sans client PostgreSQL, interrogez l'état depuis un load balancer ou un script de monitoring :
curl -s http://NODE1_IP:8008/leader # 200 = c'est le leader
curl -s http://NODE2_IP:8008/replica # 200 = c'est une réplique saine
curl -s http://NODE1_IP:8008/health # JSON : state, role, lagHAProxy peut pointer ses health checks vers /leader et /replica pour acheminer les écritures et les lectures sur les bons nœuds. Voir le guide HAProxy sur VPS pour le câblage complet.
Opérations quotidiennes
Sauvegardes avec pgBackRest 2.59.
Installez pgBackRest sur les trois nœuds et désignez un dépôt partagé (objet S3, NFS ou répertoire local dédié). La configuration recommandée tire les sauvegardes depuis une réplique pour ne pas charger le leader :
pgbackrest --stanza=pg-cluster --type=full backupActivez la compression et les sauvegardes incrémentales quotidiennes dans pgbackrest.conf (repo1-retention-full=7). Consultez le guide sauvegardes sur VPS pour les stratégies complémentaires.
Monitoring du cluster.
patronictl list donne le lag en Mo par réplique. Alertez dès que le lag dépasse un seuil (exemple : 50 Mo) : cela signale soit une réplique lente, soit un problème réseau. L'endpoint GET /patroni retourne un JSON complet incluant xlog_location et replication_state.
Scaling vertical.
Pour augmenter les ressources d'un nœud : arrêtez Patroni sur ce nœud (il passe en réplique déconnectée), redimensionnez le VPS, redémarrez. Patroni se reconnecte et rattrape le lag automatiquement via pg_rewind ou pg_basebackup selon l'amplitude du décalage.
Trade-off synchronous_commit.
Avec synchronous_mode: true, chaque COMMIT attend la confirmation du sync standby. Sur un réseau privé local, chaque COMMIT attend la confirmation du standby synchrone — le délai dépend de la latence réseau entre nœuds (vérifiez pg_stat_replication.replay_lag). Sur un réseau plus large (nœuds dans des datacenters différents), cette latence peut impacter les applications à écritures intensives. Dans ce cas, basculer en synchronous_mode: false + réplication asynchrone : vous perdez la garantie de zéro perte de données en cas de crash, mais les écritures restent rapides. C'est un arbitrage à documenter explicitement dans votre configuration.
Durcissement : TLS mutual auth entre nœuds
Par défaut, la communication etcd et les connexions de réplication PostgreSQL circulent en clair sur le réseau privé. Sur un réseau partagé ou dans un environnement multi-tenant, activez le TLS mutual auth.
Pour etcd, générez une CA et des certificats par nœud, puis ajoutez dans etcd.conf.yml :
client-transport-security:
cert-file: /etc/etcd/tls/server.crt
key-file: /etc/etcd/tls/server.key
trusted-ca-file: /etc/etcd/tls/ca.crt
client-cert-auth: true
peer-transport-security:
cert-file: /etc/etcd/tls/peer.crt
key-file: /etc/etcd/tls/peer.key
trusted-ca-file: /etc/etcd/tls/ca.crt
peer-client-cert-auth: truePour la réplication PostgreSQL, utilisez sslmode=verify-full dans les paramètres de connexion primary_conninfo de Patroni. Le certificat du leader est ainsi vérifié par chaque réplique.
Dépannage : les cinq erreurs les plus fréquentes
1. Quorum etcd perdu — le cluster refuse d'élire un leader.
Symptôme : patronictl list affiche tous les nœuds en running mais aucun Leader. Cause : un nœud etcd est inaccessible et le quorum (2 sur 3) n'est plus atteint. Vérifiez avec etcdctl endpoint status — le nœud défaillant apparaît sans réponse ou avec une erreur de connexion. Corrigez le nœud ou retirez-le provisoirement du cluster (etcdctl member remove).
2. Dérive NTP — élections en boucle.
Symptôme : le leader change toutes les 30 secondes, les logs Patroni affichent failed to update leader key. Cause : l'horloge d'un nœud dérive de plus d'une seconde. Vérifiez avec chronyc tracking sur chaque nœud et corrigez avant de redémarrer Patroni.
3. Split-brain potentiel — pg_rewind refuse de s'appliquer.
Symptôme : un ancien leader redémarre et Patroni refuse de le réintégrer comme réplique, avec l'erreur could not connect to the target server: pg_rewind target server must be in standby mode. Le serveur a continué à écrire après la perte du bail. Solution : pg_rewind --target-pgdata=/var/lib/postgresql/17/main --source-server='host=NEW_LEADER_IP ...', puis redémarrez Patroni.
4. Connexion peer rejetée — pg_hba.conf manquant.
Symptôme : la réplication s'initialise mais échoue avec FATAL: no pg_hba.conf entry for replication connection. La règle host replication replicator 0.0.0.0/0 scram-sha-256 n'est pas présente dans la section pg_hba du patroni.yml. Ajoutez-la via patronictl edit-config — pas directement dans pg_hba.conf.
5. Lag persistant après failover — max_wal_senders insuffisant.
Symptôme : la réplique affiche un lag qui ne diminue pas après promotion. Cause fréquente : max_wal_senders est trop bas (valeur par défaut de 10 sur certaines versions) et le slot de réplication est saturé. Augmentez à 20 via patronictl edit-config (paramètre max_wal_senders) et rechargez.
Un cluster qui s'administre, pas qui s'improvise
Patroni 4.1 avec etcd 3.6 couvre l'essentiel de ce qu'un service managé apporte sur la disponibilité : élection automatique, réplication synchrone, API de pilotage. La différence, c'est la maîtrise : accès root, extensions libres, coût fixe, et la possibilité de debugguer le nœud qui vacille au lieu d'attendre un support tiers.
Le prérequis opérationnel n'est pas la complexité de Patroni — la procédure ci-dessus le montre. C'est la discipline sur trois points : NTP synchronisé, sauvegardes vérifiées régulièrement, et un runbook de failover testé avant que la panne arrive.
Pour démarrer, un cluster Patroni 3 nœuds demande trois VPS avec accès root, IPv4 dédiée et réseau privé. Voir le guide d'installation de base PostgreSQL sur VPS et le comparatif auto-hébergé vs Amazon RDS pour choisir l'approche qui correspond à votre charge.