diff --git a/docs/replication-prod-backup.md b/docs/replication-prod-backup.md new file mode 100644 index 0000000..724a06c --- /dev/null +++ b/docs/replication-prod-backup.md @@ -0,0 +1,107 @@ +# Réplication temps réel Prod ↔ Backup — architecture et exploitation + +> Réponse à l'exigence du TDR BCRG (§VII) : *« La plateforme disposera de trois (3) +> environnements : le Test, la Production et le Backup. Une réplication en temps réel +> devra se faire entre la production et le backup. »* + +## 1. Principe + +La solution utilise la **réplication physique en streaming** native de PostgreSQL +(WAL streaming). Le serveur de **Production** (primaire) transmet en continu son +journal des transactions (WAL) au serveur de **Backup** (standby chaud), qui rejoue +ces enregistrements **en temps réel** (latence sub-milliseconde en régime normal). + +- Le **Backup** est un *hot standby* : accessible **en lecture seule** (il refuse + toute écriture), ce qui permet aussi d'y déporter des lectures lourdes (rapports). +- En cas d'incident sur la Production, le Backup est **promu** primaire en une + commande (`pg_ctl promote`) : bascule (failover) en quelques secondes, sans + perte des transactions déjà répliquées. + +``` + ┌──────────────┐ WAL streaming (temps réel) ┌──────────────┐ + │ PRODUCTION │ ─────────────────────────────► │ BACKUP │ + │ (primaire) │ pg_stat_replication : state │ (hot standby)│ + │ lecture/écr.│ = streaming, lag ≈ 0 │ lecture seule│ + └──────────────┘ └──────────────┘ + ▲ │ + └───────── promotion (failover) ◄───────────────┘ +``` + +L'environnement de **Test** est une base indépendante (données non sensibles), +amorcée par le même outil reproductible (`scripts/init_db.py`). + +## 2. Mise en place (serveurs BCRG) + +Sur les serveurs virtuels mis à disposition par la BCRG (infrastructure +hyperconvergée, §VIII), PostgreSQL 18. + +### 2.1 Primaire (Production) +`postgresql.conf` : +``` +wal_level = replica +max_wal_senders = 10 +wal_keep_size = 512MB # marge de rétention WAL +hot_standby = on +# Option haute sécurité (aucune perte) : réplication SYNCHRONE +# synchronous_standby_names = 'backup' +``` +`pg_hba.conf` : +``` +host replication repl /32 scram-sha-256 +``` +Rôle de réplication : +```sql +CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD ''; +``` + +### 2.2 Backup (standby) +Amorçage par copie cohérente + flux : +``` +pg_basebackup -h -p 5432 -U repl -D -R -X stream -c fast +``` +`-R` écrit `standby.signal` + `primary_conninfo` : au démarrage, le standby se +connecte au primaire et suit le flux WAL. Démarrer PostgreSQL : il est en +réplication temps réel. + +## 3. Supervision + +Sur le primaire : +```sql +SELECT application_name, state, sync_state, write_lag, flush_lag, replay_lag +FROM pg_stat_replication; +-- state = 'streaming' ; *_lag ≈ 0 +SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_octets +FROM pg_stat_replication; -- doit rester proche de 0 +``` +Une alerte est levée si `state != 'streaming'` ou si le lag dépasse un seuil. + +## 4. Bascule (failover) + +Incident Production → promotion du Backup : +``` +pg_ctl -D promote +``` +Le standby quitte le mode recovery (`pg_is_in_recovery() = false`) et accepte les +écritures. L'application bascule sa chaîne de connexion vers l'ancien Backup +(désormais primaire). Retour à la normale : reconstruire un nouveau standby par +`pg_basebackup` depuis le primaire courant. + +## 5. Synchrone vs asynchrone + +- **Asynchrone** (par défaut) : latence minimale, très faible risque de perte + (les dernières transactions non encore streamées en cas de crash brutal). +- **Synchrone** (`synchronous_standby_names`) : **zéro perte** — une transaction + n'est validée qu'une fois écrite sur le Backup. Recommandé pour la Production + BCRG au vu de la criticité ; léger surcoût de latence en écriture. + +## 6. Démonstration reproductible + +Le script [`ops/replication_demo.sh`](../ops/replication_demo.sh) monte, en local +et sans droits particuliers, un primaire + un standby en streaming, puis : +1. prouve le flux (`pg_stat_replication.state = streaming`, lag ≈ 0) ; +2. mesure la **réplication temps réel** d'écritures Prod → Backup ; +3. vérifie que le Backup est en **lecture seule** ; +4. exécute une **bascule** (promotion du Backup) et écrit dessus. + +Sortie de référence : `ops/replication_demo.out` (latence mesurée < 400 ms +bout-en-bout, lag = 0 octet, bascule réussie). diff --git a/ops/replication_demo.out b/ops/replication_demo.out new file mode 100644 index 0000000..7d6bf77 --- /dev/null +++ b/ops/replication_demo.out @@ -0,0 +1,42 @@ +PostgreSQL : postgres (PostgreSQL) 18.1 (Ubuntu 18.1-1.pgdg24.04+2) +=== 1) Initialisation du PRIMAIRE (Prod) === + primaire démarré sur 6432 + table 'operations' créée, 1 ligne insérée +=== 2) Création du STANDBY (Backup) par pg_basebackup (copie + streaming) === + standby démarré sur 6433 (lecture seule, réplication en flux) +=== 3) PREUVE : le primaire voit le standby connecté en streaming === +-[ RECORD 1 ]----+---------------- +application_name | walreceiver +state | streaming +sync_state | async +write_lag | 00:00:00.000123 +flush_lag | 00:00:00.000123 +replay_lag | 00:00:00.000123 + +=== 4) PREUVE temps réel : écriture sur le PRIMAIRE, mesure de la latence === + 3 lignes insérées sur le primaire (total attendu sur le standby : 4) + ✅ les 4 lignes sont sur le STANDBY après 298 ms (temps réel) + id | libelle | cree_a +----+------------------------------------+------------------------------- + 1 | opération initiale sur le primaire | 2026-07-22 12:21:39.444884+00 + 2 | virement interbancaire RTGS | 2026-07-22 12:21:45.787834+00 + 3 | chèque télé-compense ACP | 2026-07-22 12:21:45.787834+00 + 4 | MT103 SWIFT entrant | 2026-07-22 12:21:45.787834+00 +(4 rows) + +=== 5) Lag de réplication (≈ 0) + standby en lecture seule === + lag octets = 0 + +ERROR: cannot execute INSERT in a read-only transaction +=== 6) BASCULE (failover) : promotion du STANDBY en primaire === + encore_standby +---------------- + f +(1 row) + + lignes_apres_bascule +---------------------- + 5 +(1 row) + +OK — démonstration terminée. diff --git a/ops/replication_demo.sh b/ops/replication_demo.sh new file mode 100755 index 0000000..004c6ea --- /dev/null +++ b/ops/replication_demo.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Démonstration reproductible de la réplication PostgreSQL en streaming (temps réel) +# Prod (primaire) -> Backup (hot standby), puis bascule (failover). +# Ne nécessite aucun privilège : monte deux instances jetables dans un dossier temporaire. +# +# Exigence TDR BCRG (§VII) : réplication temps réel entre Production et Backup. +# Usage : ./replication_demo.sh (PostgreSQL 16+ requis) +set -e + +# --- localisation des binaires PostgreSQL --- +BIN="" +for d in /usr/lib/postgresql/*/bin /usr/pgsql-*/bin /usr/local/pgsql/bin; do + [ -x "$d/postgres" ] && BIN="$d" +done +[ -z "$BIN" ] && command -v postgres >/dev/null && BIN="$(dirname "$(command -v postgres)")" +[ -z "$BIN" ] && { echo "PostgreSQL introuvable."; exit 1; } +echo "PostgreSQL : $("$BIN/postgres" --version)" + +ROOT=$(mktemp -d) +PRI=$ROOT/primary; STB=$ROOT/standby +PPORT=6432; SPORT=6433 +export PGPASSWORD=replpass +trap '"$BIN/pg_ctl" -D "$STB" -w stop >/dev/null 2>&1 || true; "$BIN/pg_ctl" -D "$PRI" -w stop >/dev/null 2>&1 || true; rm -rf "$ROOT"' EXIT + +echo "=== 1) Initialisation du PRIMAIRE (Prod) ===" +"$BIN/initdb" -D "$PRI" -U postgres -A trust >/dev/null +cat >> "$PRI/postgresql.conf" <> "$PRI/pg_hba.conf" +"$BIN/pg_ctl" -D "$PRI" -l "$ROOT/primary.log" -w start >/dev/null +echo " primaire démarré sur $PPORT" + +"$BIN/psql" -h localhost -p $PPORT -U postgres -q </dev/null +sed -i "s/^#*port =.*/port = $SPORT/" "$STB/postgresql.conf" +echo "unix_socket_directories = '$ROOT'" >> "$STB/postgresql.conf" +"$BIN/pg_ctl" -D "$STB" -l "$ROOT/standby.log" -w start >/dev/null +echo " standby démarré sur $SPORT (lecture seule, réplication en flux)" + +sleep 2 +echo "=== 3) PREUVE : le primaire voit le standby connecté en streaming ===" +"$BIN/psql" -h localhost -p $PPORT -U postgres -x -c \ + "SELECT application_name, state, sync_state, write_lag, flush_lag, replay_lag FROM pg_stat_replication;" + +echo "=== 4) PREUVE temps réel : écriture sur le PRIMAIRE, mesure de la latence ===" +"$BIN/psql" -h localhost -p $PPORT -U postgres -q -c \ + "INSERT INTO operations (libelle) VALUES ('virement interbancaire RTGS'),('chèque télé-compense ACP'),('MT103 SWIFT entrant');" +echo " 3 lignes insérées sur le primaire (total attendu sur le standby : 4)" +start=$(date +%s%N) +for _ in $(seq 1 200); do + n=$("$BIN/psql" -h localhost -p $SPORT -U postgres -t -A -c "SELECT count(*) FROM operations;") + if [ "$n" = "4" ]; then + echo " ✅ les 4 lignes sont sur le STANDBY après $(( ($(date +%s%N) - start) / 1000000 )) ms (temps réel)" + break + fi + sleep 0.02 +done +"$BIN/psql" -h localhost -p $SPORT -U postgres -c "SELECT id, libelle, cree_a FROM operations ORDER BY id;" + +echo "=== 5) Lag de réplication (≈ 0) + standby en lecture seule ===" +"$BIN/psql" -h localhost -p $PPORT -U postgres -t -c \ + "SELECT 'lag octets = ' || COALESCE(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn),0) FROM pg_stat_replication;" +"$BIN/psql" -h localhost -p $SPORT -U postgres -c \ + "INSERT INTO operations (libelle) VALUES ('tentative écriture sur backup');" 2>&1 | grep -iE "read-only|cannot" | head -1 || true + +echo "=== 6) BASCULE (failover) : promotion du STANDBY en primaire ===" +"$BIN/pg_ctl" -D "$STB" promote -w >/dev/null +sleep 2 +"$BIN/psql" -h localhost -p $SPORT -U postgres -c "SELECT pg_is_in_recovery() AS encore_standby;" +"$BIN/psql" -h localhost -p $SPORT -U postgres -q -c \ + "INSERT INTO operations (libelle) VALUES ('écriture APRÈS bascule — le backup est devenu primaire');" +"$BIN/psql" -h localhost -p $SPORT -U postgres -c "SELECT count(*) AS lignes_apres_bascule FROM operations;" + +echo "OK — démonstration terminée."