Tutoriel

PostgreSQL haute disponibilité avec Patroni sur VPS

Bases de données12 min de lecture8 étapes

Un SaaS multi-tenant ou une application critique ne peut pas se permettre une coupure de base de données. Amazon RDS et Aurora résolvent le problème, mais leur modèle de facturation grandit avec la charge. Patroni 4.1 associé à etcd 3.6 offre le même niveau de disponibilité sur trois VPS avec accès root : failover automatique en moins de 30 secondes, réplication synchrone configurable, et une API REST pour piloter le cluster depuis un terminal. Ce guide couvre l'installation complète, la démonstration du basculement et les opérations quotidiennes.

Sommaire· Pourquoi la haute disponibilité PostgreSQL sur VPS — et pas RDS1/11
  1. 01Pourquoi la haute disponibilité PostgreSQL sur VPS — et pas RDS
  2. 02Ce que ce cluster vous apporte
  3. 03Architecture du cluster : trois nœuds, un quorum
  4. 04Prérequis : ressources et réseau
  5. 05Ressources minimales recommandées par nœud
  6. 06Installation : de zéro au cluster opérationnel
  7. 07Failover et switchover : démonstration
  8. 08Opérations quotidiennes
  9. 09Durcissement : TLS mutual auth entre nœuds
  10. 10Dépannage : les cinq erreurs les plus fréquentes
  11. 11Un cluster qui s'administre, pas qui s'improvise

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: true garantit 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.conf des 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

  1. 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 tracking

    Vé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.

  2. 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/etcdctl

    Cré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: new

    Créez l'unité systemd, activez et démarrez etcd sur les trois nœuds avant de passer à l'étape suivante.

  3. 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=table

    Attendez 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.

  4. 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-binary

    Arrêtez PostgreSQL — Patroni prend en charge l'initialisation du cluster :

    systemctl stop postgresql
    systemctl disable postgresql
  5. Configurer 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.

  6. 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.target

    Dé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 patroni

    Suivez l'initialisation :

    patronictl -c /etc/patroni/patroni.yml list
  7. Vé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.

  8. 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-config

    Ajoutez 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 Standby

Arretez Patroni sur le leader :

systemctl stop patroni   # sur pg-node1

Suivez 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 : stopped

Patroni 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-node2

Patroni 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, lag

HAProxy 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 backup

Activez 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: true

Pour 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.

Trois VPS pour un cluster Patroni

Un cluster PostgreSQL Patroni 3 nœuds demande trois VPS avec accès root, IPv4 dédiée et réseau privé. Tous les templates VPS ServOrbit livrent un accès root complet et un réseau privé entre instances.

Besoin d'aide ?

Parcourez notre centre d'aide et notre FAQ, ou contactez notre équipe — rappel, WhatsApp ou e-mail. Support en français, anglais et arabe.

Écrire sur WhatsApps'ouvre dans un nouvel onglet