Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/replication-prod-backup.md
Original file line number Diff line number Diff line change
@@ -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 <IP_backup>/32 scram-sha-256
```
Rôle de réplication :
```sql
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD '<secret>';
```

### 2.2 Backup (standby)
Amorçage par copie cohérente + flux :
```
pg_basebackup -h <IP_prod> -p 5432 -U repl -D <datadir> -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 <datadir_backup> 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).
42 changes: 42 additions & 0 deletions ops/replication_demo.out
Original file line number Diff line number Diff line change
@@ -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.
88 changes: 88 additions & 0 deletions ops/replication_demo.sh
Original file line number Diff line number Diff line change
@@ -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" <<CONF
port = $PPORT
listen_addresses = 'localhost'
unix_socket_directories = '$ROOT'
wal_level = replica
max_wal_senders = 10
wal_keep_size = 128MB
hot_standby = on
CONF
echo "host replication repl 127.0.0.1/32 md5" >> "$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 <<SQL
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD 'replpass';
CREATE TABLE operations (id serial PRIMARY KEY, libelle text, cree_a timestamptz DEFAULT now());
INSERT INTO operations (libelle) VALUES ('opération initiale sur le primaire');
SQL
echo " table 'operations' créée, 1 ligne insérée"

echo "=== 2) Création du STANDBY (Backup) par pg_basebackup (copie + streaming) ==="
"$BIN/pg_basebackup" -h localhost -p $PPORT -U repl -D "$STB" -R -X stream -c fast >/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."
Loading