Guide de Failover PostgreSQL sur OpenShift
Table des matières
- Vue d'ensemble
- Failover manuel
- Failover automatique avec Patroni
- Tests de failover
- Retour à la normale
- Troubleshooting
Vue d'ensemble
Le failover est le processus de promotion d'un serveur slave en master lorsque le master actuel devient indisponible.
Types de failover
Failover manuel :
- Contrôle total de l'opération
- Intervention humaine requise
- Temps d'indisponibilité plus long (5-15 minutes)
- Adapté aux environnements avec maintenance planifiée
Failover automatique :
- Promotion automatique du slave
- Temps d'indisponibilité minimal (30-90 secondes)
- Nécessite un outil d'orchestration (Patroni, Stolon, repmgr)
- Adapté aux environnements production haute disponibilité
Architecture après failover
AVANT FAILOVER :
┌──────────┐
│ Master │ ← Écritures
└────┬─────┘
│ Réplication
▼
┌──────────┐
│ Slave │ ← Lectures
└──────────┘
APRÈS FAILOVER :
┌──────────┐
│ Ex-Master│ ← ARRÊTÉ
└──────────┘
┌──────────┐
│ Ex-Slave │ ← Écritures + Lectures
│ (promu) │ NOUVEAU MASTER
└──────────┘
Failover manuel
Prérequis
- Accès au pod slave
- Accès aux services OpenShift
- Backup récent de la base de données
Scénario : Le master est indisponible
Signes d'indisponibilité :
- Pod master en
CrashLoopBackOffouError - Applications ne peuvent plus écrire
- Réplication arrêtée sur le slave
Étape 1 : Vérification de l'état
# Vérifier l'état des pods
oc get pods -l app=postgresql
# Vérifier l'état de la réplication (si le master répond)
MASTER_POD=$(oc get pods -l name=postgresql -o jsonpath='{.items[0].metadata.name}')
oc exec $MASTER_POD -- psql -U postgres -c "SELECT * FROM pg_stat_replication;" 2>/dev/null || echo "Master inaccessible"
# Vérifier le lag sur le slave
SLAVE_POD=$(oc get pods -l role=slave -o jsonpath='{.items[0].metadata.name}')
oc exec $SLAVE_POD -- psql -U postgres -c "SELECT pg_is_in_recovery();"
Résultats attendus :
pg_is_in_recovery() = true: le slave est encore en mode standby- Master inaccessible ou ne répond plus
Étape 2 : Backup de sécurité du slave
⚠️ CRITIQUE : Toujours faire un backup avant promotion
# Snapshot du PVC (si votre storage class le supporte)
oc get pvc postgresql-slave -o yaml > pvc-slave-before-promotion.yaml
# Ou backup SQL
oc exec $SLAVE_POD -- pg_dumpall -U postgres > backup-slave-before-promotion.sql
Étape 3 : Promotion du slave en master
Option A : Promotion avec pg_ctl (PostgreSQL 12+)
# Se connecter au pod slave
oc exec -it $SLAVE_POD -- bash
# À l'intérieur du conteneur
pg_ctl promote -D $PGDATA
# Vérifier la promotion
psql -U postgres -c "SELECT pg_is_in_recovery();"
# Devrait retourner "f" (false)
# Sortir du conteneur
exit
Option B : Promotion avec fonction SQL (PostgreSQL 12+)
oc exec $SLAVE_POD -- psql -U postgres -c "SELECT pg_promote();"
Option C : Promotion manuelle (toutes versions)
# Supprimer le fichier standby.signal
oc exec $SLAVE_POD -- rm -f /var/lib/postgresql/data/standby.signal
# Redémarrer PostgreSQL
oc exec $SLAVE_POD -- pg_ctl restart -D /var/lib/postgresql/data
Étape 4 : Vérification de la promotion
# Vérifier que le slave n'est plus en recovery
oc exec $SLAVE_POD -- psql -U postgres -c "SELECT pg_is_in_recovery();"
# Doit retourner "f"
# Tester une écriture
oc exec $SLAVE_POD -- psql -U postgres -c "CREATE TABLE test_failover (id INT);"
oc exec $SLAVE_POD -- psql -U postgres -c "INSERT INTO test_failover VALUES (1);"
oc exec $SLAVE_POD -- psql -U postgres -c "SELECT * FROM test_failover;"
Étape 5 : Redirection du service master
Modifier le service pour pointer vers l'ex-slave (nouveau master) :
Resource : Modification du service postgresql
apiVersion: v1
kind: Service
metadata:
name: postgresql
labels:
app: postgresql
spec:
ports:
- name: postgresql
port: 5432
targetPort: 5432
selector:
app: postgresql
role: slave # ← Pointer vers l'ex-slave
type: ClusterIP
Appliquer :
oc apply -f postgresql-service-updated.yaml
# Ou directement avec oc patch
oc patch service postgresql -p '{"spec":{"selector":{"role":"slave"}}}'
Étape 6 : Mise à jour des labels du nouveau master
Mettre à jour les labels du pod promu :
# Modifier le deployment slave
oc edit deployment postgresql-slave
# Changer les labels
# labels:
# app: postgresql
# role: master # ← Changer de "slave" à "master"
Ou créer un nouveau service temporaire :
# Créer un service pointant vers l'ex-slave
oc expose deployment postgresql-slave --name=postgresql-temp --port=5432
# Supprimer l'ancien service master
oc delete service postgresql
# Renommer le service temporaire
oc patch service postgresql-temp -p '{"metadata":{"name":"postgresql"}}'
Étape 7 : Configuration du nouveau master pour la réplication
Configurer le nouveau master pour accepter de futurs slaves :
# Créer l'utilisateur de réplication si nécessaire
oc exec $SLAVE_POD -- psql -U postgres <<EOF
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='replicator') THEN
CREATE USER replicator WITH REPLICATION ENCRYPTED PASSWORD 'replicator_password';
END IF;
END
\$\$;
EOF
# Créer un nouveau slot de réplication
oc exec $SLAVE_POD -- psql -U postgres -c "SELECT pg_create_physical_replication_slot('replication_slot_new_slave');"
Appliquer la configuration master (réutiliser les ConfigMaps existantes) :
# Le pod utilise déjà slave.conf, il faut le passer en master.conf
oc exec $SLAVE_POD -- bash -c "cp /etc/postgresql/slave.conf /var/lib/postgresql/data/postgresql.auto.conf"
# Ou recréer le pod avec la configuration master
oc set volumes deployment/postgresql-slave \
--add --name=master-config \
--type=configmap \
--configmap-name=postgresql-master-config \
--mount-path=/etc/postgresql/master.conf \
--sub-path=master.conf \
--overwrite
# Redémarrer le pod
oc rollout restart deployment/postgresql-slave
Étape 8 : Validation finale
# Applications peuvent écrire
oc exec $SLAVE_POD -- psql -U postgres -c "INSERT INTO test_failover VALUES (2);"
# Vérifier les connexions
oc exec $SLAVE_POD -- psql -U postgres -c "SELECT count(*) FROM pg_stat_activity;"
# Tester depuis une application
oc run test-pg --image=postgres:15 --rm -it --restart=Never -- \
psql -h postgresql -U postgres -d postgres -c "SELECT now();"
Failover automatique avec Patroni
Patroni est la solution recommandée pour le failover automatique PostgreSQL sur Kubernetes/OpenShift.
Architecture avec Patroni
┌─────────────────────────────────────────────┐
│ etcd / Consul │
│ (Distributed Config Store) │
└────────────────┬────────────────────────────┘
│
┌───────┴────────┐
│ │
┌────▼────┐ ┌────▼────┐
│ Patroni │ │ Patroni │
│ Master │─────▶│ Replica │
│ Pod │ │ Pod │
└─────────┘ └─────────┘
│ │
┌────▼────┐ ┌────▼────┐
│ PG │ │ PG │
│ Master │ │ Slave │
└─────────┘ └─────────┘
Composants nécessaires
- etcd : Store de configuration distribuée (consensus)
- Patroni : Agent qui gère PostgreSQL et le failover
- PostgreSQL : Base de données
Étape 1 : Déploiement d'etcd
Resource : etcd-statefulset.yaml
apiVersion: v1
kind: Service
metadata:
name: etcd
labels:
app: etcd
spec:
ports:
- port: 2379
name: client
- port: 2380
name: peer
clusterIP: None
selector:
app: etcd
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: etcd
spec:
serviceName: etcd
replicas: 3
selector:
matchLabels:
app: etcd
template:
metadata:
labels:
app: etcd
spec:
containers:
- name: etcd
image:
[]
Appliquer :
oc apply -f etcd-statefulset.yaml
# Attendre que etcd soit prêt
oc wait --for=condition=ready pod -l app=etcd --timeout=300s
# Vérifier l'état du cluster
oc exec etcd-0 -- etcdctl member list
Étape 2 : ConfigMap Patroni
Resource : patroni-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: patroni-config
data:
patroni.yml: |
scope: postgres-cluster
namespace: /service/
name: ${HOSTNAME}
restapi:
listen: 0.0.0.0:8008
connect_address: ${HOSTNAME}.postgresql-patroni:8008
etcd:
hosts: etcd-0.etcd:2379,etcd-1.etcd:2379,etcd-2.etcd:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
parameters:
max_connections: 100
shared_buffers: 256MB
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_keep_size: 1GB
Paramètres clés :
ttl: 30: Temps avant de considérer un nœud mortloop_wait: 10: Intervalle de vérificationmaximum_lag_on_failover: Lag max accepté pour être élu masteruse_pg_rewind: true: Permet de resynchroniser un ancien master
Appliquer :
oc apply -f patroni-config.yaml
Étape 3 : StatefulSet PostgreSQL avec Patroni
Resource : postgresql-patroni-statefulset.yaml
apiVersion: v1
kind: Service
metadata:
name: postgresql-patroni
labels:
app: postgresql-patroni
spec:
ports:
- port: 5432
name: postgresql
- port: 8008
name: patroni
clusterIP: None
selector:
app: postgresql-patroni
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-master
labels:
app: postgresql-patroni
spec:
ports:
- port: 5432
targetPort: 5432
selector:
app: postgresql-patroni
role: master
type: ClusterIP
---
apiVersion: v1
[]
Étape 4 : Secret Patroni
Resource : postgresql-patroni-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: postgresql-patroni-secret
type: Opaque
stringData:
superuser-password: <GÉNÉRER_MOT_DE_PASSE_FORT>
replication-password: <GÉNÉRER_MOT_DE_PASSE_FORT>
Étape 5 : ServiceAccount et RBAC
Resource : postgresql-patroni-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: postgresql-patroni
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: postgresql-patroni
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "get", "list", "patch", "update", "watch"]
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["create", "get", "list", "patch", "update", "watch", "delete"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "patch", "update", "watch"]
---
apiVersion:
Étape 6 : Déploiement
# Appliquer les secrets
oc apply -f postgresql-patroni-secret.yaml
# Appliquer les RBAC
oc apply -f postgresql-patroni-rbac.yaml
# Déployer le StatefulSet
oc apply -f postgresql-patroni-statefulset.yaml
# Surveiller le déploiement
oc get pods -l app=postgresql-patroni -w
# Vérifier les logs
oc logs -f postgresql-patroni-0
Étape 7 : Vérification du cluster Patroni
# Voir l'état du cluster
oc exec postgresql-patroni-0 -- patronictl -c /etc/patroni/patroni.yml list
# Résultat attendu :
# + Cluster: postgres-cluster --+---------+---------+----+-----------+
# | Member | Host | Role | State | TL | Lag in MB |
# +---------------------+-------+---------+---------+----+-----------+
# | postgresql-patroni-0| 10... | Leader | running | 1 | |
# | postgresql-patroni-1| 10... | Replica | running | 1 | 0 |
# | postgresql-patroni-2| 10... | Replica | running | 1 | 0 |
# +---------------------+-------+---------+---------+----+-----------+
# Vérifier le master actuel
oc exec postgresql-patroni-0 -- patronictl -c /etc/patroni/patroni.yml show-config
Étape 8 : Test du failover automatique
Simuler une panne du master :
# Identifier le pod master
MASTER_POD=$(oc get pods -l app=postgresql-patroni,role=master -o jsonpath='{.items[0].metadata.name}')
# Supprimer le pod master
oc delete pod $MASTER_POD
# Observer le failover automatique
watch -n 1 "oc exec postgresql-patroni-0 -- patronictl -c /etc/patroni/patroni.yml list"
Comportement attendu :
- Patroni détecte que le master ne répond plus (~30 secondes)
- Un replica est automatiquement promu en master (~10 secondes)
- L'ancien master redémarre et devient replica automatiquement
- Temps total d'indisponibilité : ~40-60 secondes
Configuration du service pour Patroni
Les applications doivent utiliser les services appropriés :
Pour les écritures (master uniquement) :
postgresql-master:5432
Pour les lectures (replicas uniquement) :
postgresql-replicas:5432
Pour lectures + écritures (tout le cluster) :
postgresql-patroni:5432
Tests de failover
Test 1 : Failover manuel planifié
Avec Patroni :
# Effectuer un switchover planifié
oc exec postgresql-patroni-0 -- patronictl -c /etc/patroni/patroni.yml switchover --master postgresql-patroni-0 --candidate postgresql-patroni-1 --force
Sans Patroni (manuel) : Suivre la procédure de failover manuel (section précédente)
Test 2 : Failover automatique sur panne
# Simuler un crash du pod master
MASTER_POD=$(oc get pods -l app=postgresql-patroni,role=master -o jsonpath='{.items[0].metadata.name}')
oc delete pod $MASTER_POD --grace-period=0 --force
# Surveiller
watch -n 1 "oc get pods -l app=postgresql-patroni"
watch -n 1 "oc exec postgresql-patroni-0 -- patronictl -c /etc/patroni/patroni.yml list"
Test 3 : Split-brain prevention
# Isoler un pod du réseau (nécessite Network Policies)
# Créer une NetworkPolicy qui bloque le master actuel
# Patroni devrait :
# 1. Détecter la perte de quorum
# 2. Promouvoir un nouveau master parmi les nœuds connectés
# 3. Empêcher l'ancien master d'accepter des écritures
Test 4 : Performance pendant failover
# Script de test de charge
cat > test-failover-performance.sh <<'EOF'
#!/bin/bash
# Connexion à la base
CONNECTION="postgresql://postgres:password@postgresql-master:5432/postgres"
# Boucle d'écriture
while true; do
result=$(psql "$CONNECTION" -c "INSERT INTO test_failover VALUES (NOW()) RETURNING *;" 2>&1)
if [[ $? -eq 0 ]]; then
echo "[$(date)] SUCCESS: $result"
else
echo "[$(date)] FAILED: $result"
fi
sleep 1
done
EOF
# Lancer le test en arrière-plan
oc run test-failover --image=postgres:15 --rm -it --restart=Never -- bash -c "$(cat test-failover-performance.sh)"
# Dans un autre terminal, déclencher le failover
oc delete pod $MASTER_POD
Retour à la normale
Après failover manuel : Ajouter un nouveau slave
Une fois le failover effectué, l'ancien master peut être reconfiguré comme slave.
Étape 1 : Nettoyer l'ancien master
# Si l'ancien master est récupérable
OLD_MASTER_POD=$(oc get pods -l name=postgresql -o jsonpath='{.items[0].metadata.name}')
# Supprimer le pod et le PVC
oc delete pod $OLD_MASTER_POD
oc delete pvc postgresql # PVC de l'ancien master
Étape 2 : Créer un nouveau PVC pour le nouveau slave
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-new-slave
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: standard
Étape 3 : Déployer le nouveau slave
Utiliser la même configuration que dans le guide principal (OPENSHIFT_REPLICATION_GUIDE.md), en changeant :
- Service master : pointer vers le nouveau master (ex-slave promu)
- Nom du PVC :
postgresql-new-slave
Après failover Patroni : Automatique
Avec Patroni, pas d'action nécessaire :
- L'ancien master redémarre automatiquement comme replica
- Patroni synchronise automatiquement les données
- Le cluster retrouve son état nominal (1 master + N replicas)
Troubleshooting
Problème : Promotion du slave échoue
Symptômes :
ERROR: cannot promote server because it is still in recovery mode
Solutions :
- Vérifier si le slave a fini de rattraper le WAL :
oc exec $SLAVE_POD -- psql -U postgres -c "
SELECT pg_is_in_recovery(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
"
- Attendre la fin du replay :
# Le slave doit terminer d'appliquer tous les WAL
# Attendre quelques minutes puis réessayer
- Forcer la promotion (dernier recours) :
oc exec $SLAVE_POD -- pg_ctl promote -D /var/lib/postgresql/data
Problème : Split-brain (deux masters actifs)
Symptômes :
- Deux pods acceptent des écritures
- Données divergentes entre les deux serveurs
⚠️ CRITIQUE : Éviter à tout prix
Solutions :
- Identifier les deux masters :
oc exec <pod1> -- psql -U postgres -c "SELECT pg_is_in_recovery();"
oc exec <pod2> -- psql -U postgres -c "SELECT pg_is_in_recovery();"
- Choisir le master à conserver :
- Généralement : le plus récent (LSN le plus élevé)
- Ou celui avec le plus de données
# Vérifier les LSN
oc exec <pod1> -- psql -U postgres -c "SELECT pg_current_wal_lsn();"
oc exec <pod2> -- psql -U postgres -c "SELECT pg_current_wal_lsn();"
- Arrêter l'autre master :
oc scale deployment/<old-master> --replicas=0
- Reconfigurer en slave :
- Supprimer le PVC de l'ancien master
- Recréer comme slave depuis le nouveau master
Prévention :
- Utiliser Patroni (gestion automatique du quorum)
- Toujours vérifier qu'un seul master est actif avant failover manuel
Problème : Patroni n'élit pas de nouveau master
Symptômes :
patronictl list
# Tous les membres en état "no master"
Solutions :
- Vérifier etcd :
oc exec etcd-0 -- etcdctl endpoint health --cluster
- Vérifier le quorum :
# Patroni nécessite la majorité (2/3, 3/5, etc.)
# Si trop de nœuds down, pas de quorum
- Réinitialiser le cluster (dernier recours) :
oc exec postgresql-patroni-0 -- patronictl -c /etc/patroni/patroni.yml reinit postgres-cluster postgresql-patroni-0
Problème : Applications continuent d'écrire sur l'ancien master
Symptômes :
- Applications reçoivent des erreurs après failover
- Connexions vers l'ancien master
Solutions :
- Vérifier les services :
oc get endpoints postgresql-master
# Doit pointer vers le nouveau master
- Forcer la reconnexion :
# Redémarrer les applications
oc rollout restart deployment/<app-name>
- Utiliser un proxy (pgBouncer, HAProxy) :
- Le proxy détecte automatiquement le master actif
- Les applications se connectent au proxy, pas directement à PostgreSQL
Checklist de failover
Avant le failover
- Backup complet de toutes les bases de données
- Vérification du lag de réplication (< 10MB recommandé)
- Communication aux équipes
- Plan de rollback préparé
- Vérification que le slave est prêt à être promu
Pendant le failover manuel
- Backup du slave avant promotion
- Promotion du slave exécutée
- Vérification pg_is_in_recovery() = false
- Test d'écriture réussi
- Service redirigé vers le nouveau master
- Applications reconnectées
Après le failover
- Monitoring actif du nouveau master
- Plan de reconstruction d'un slave
- Documentation de l'incident
- Post-mortem (si failover non planifié)
Comparaison solutions de failover
| Critère | Failover Manuel | Patroni |
|---|---|---|
| Temps d'indisponibilité | 5-15 min | 30-90 sec |
| Intervention humaine | Requise | Optionnelle |
| Complexité | Faible | Moyenne |
| Risque d'erreur | Élevé | Faible |
| Split-brain | Possible | Prévenu |
| Coût opérationnel | Élevé | Faible |
| Use case | Dev/Test | Production |
Ressources supplémentaires
Documentation
Commandes utiles
# État du cluster Patroni
oc exec <patroni-pod> -- patronictl list
# Switchover planifié
oc exec <patroni-pod> -- patronictl switchover
# Failover manuel (sans master actuel)
oc exec <patroni-pod> -- patronictl failover
# Redémarrer un membre
oc exec <patroni-pod> -- patronictl restart <member-name>
# Réinitialiser un membre
oc exec <patroni-pod> -- patronictl reinit <cluster-name> <member-name>