Le failover est le processus de promotion d'un serveur slave en master lorsque le master actuel devient indisponible.
Failover manuel :
Failover automatique :
AVANT FAILOVER :
┌──────────┐
│ Master │ ← Écritures
└────┬─────┘
│ Réplication
▼
┌──────────┐
│ Slave │ ← Lectures
└──────────┘
APRÈS FAILOVER :
┌──────────┐
│ Ex-Master│ ← ARRÊTÉ
└──────────┘
┌──────────┐
│ Ex-Slave │ ← Écritures + Lectures
│ (promu) │ NOUVEAU MASTER
└──────────┘
Signes d'indisponibilité :
CrashLoopBackOff ou Error# 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⚠️ 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
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
# 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;"
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"}}}'
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"}}'
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
# 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();"
Patroni est la solution recommandée pour le failover automatique PostgreSQL sur Kubernetes/OpenShift.
┌─────────────────────────────────────────────┐
│ etcd / Consul │
│ (Distributed Config Store) │
└────────────────┬────────────────────────────┘
│
┌───────┴────────┐
│ │
┌────▼────┐ ┌────▼────┐
│ Patroni │ │ Patroni │
│ Master │─────▶│ Replica │
│ Pod │ │ Pod │
└─────────┘ └─────────┘
│ │
┌────▼────┐ ┌────▼────┐
│ PG │ │ PG │
│ Master │ │ Slave │
└─────────┘ └─────────┘
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
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 masterAppliquer :
oc apply -f patroni-config.yaml
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
[]
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>
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:
# 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
# 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
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 :
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
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)
# 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"
# 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
# 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
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 :
postgresql-new-slaveAvec Patroni, pas d'action nécessaire :
Symptômes :
ERROR: cannot promote server because it is still in recovery mode
Solutions :
oc exec $SLAVE_POD -- psql -U postgres -c "
SELECT pg_is_in_recovery(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
"
# Le slave doit terminer d'appliquer tous les WAL
# Attendre quelques minutes puis réessayer
oc exec $SLAVE_POD -- pg_ctl promote -D /var/lib/postgresql/data
Symptômes :
⚠️ CRITIQUE : Éviter à tout prix
Solutions :
oc exec <pod1> -- psql -U postgres -c "SELECT pg_is_in_recovery();"
oc exec <pod2> -- psql -U postgres -c "SELECT pg_is_in_recovery();"
# 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();"
oc scale deployment/<old-master> --replicas=0
Prévention :
Symptômes :
patronictl list
# Tous les membres en état "no master"
Solutions :
oc exec etcd-0 -- etcdctl endpoint health --cluster
# Patroni nécessite la majorité (2/3, 3/5, etc.)
# Si trop de nœuds down, pas de quorum
oc exec postgresql-patroni-0 -- patronictl -c /etc/patroni/patroni.yml reinit postgres-cluster postgresql-patroni-0
Symptômes :
Solutions :
oc get endpoints postgresql-master
# Doit pointer vers le nouveau master
# Redémarrer les applications
oc rollout restart deployment/<app-name>
| 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 |
# É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>