Montée de version PostgreSQL sans interruption
Cet article explique comment monter de version une base PostgreSQL de production active sans l'arrêter, en utilisant la logical replication pour faire tourner ancienne et nouvelle version en parallèle jusqu'à basculer, plutôt que la fenêtre de maintenance qu'exige normalement un pg_upgrade sur place.
Pourquoi pg_upgrade sur place impose une interruption
pg_upgrade, même en mode rapide --link, pose un verrou exclusif sur l'ensemble du cluster pendant toute la conversion du catalogue : indisponible depuis le lancement jusqu'à la fin de la vérification et du redémarrage. Cette durée dépend de la complexité du catalogue, extensions, nombre de relations, pas du volume de données, ce qui explique pourquoi l'estimation « 20 minutes » n'est pas fiable. Pire : si la montée de version échoue en cours de route, votre plan de repli consiste à restaurer depuis une sauvegarde, à l'heure où ça échoue.
La logical replication contourne ce problème : vous construisez une réplique sur la nouvelle version à partir de zéro, vous répliquez dedans pendant que l'ancien primaire continue de servir le trafic, et vous redirigez l'application une fois la nouvelle version prouvée à jour. L'ancien primaire est mis au repos ensuite, à votre rythme.
Mettre en place la réplique sur la nouvelle version
Provisionnez l'instance de la nouvelle version majeure (disons PostgreSQL 17, en montant depuis la 14) comme un cluster séparé, pas une copie de l'ancien passée par pg_upgrade. Chargez-la via pg_dump/pg_restore (ou un snapshot de base), puis câblez la logical replication depuis l'ancien primaire :
-- Sur l'ANCIEN primaire (PostgreSQL 14)
CREATE PUBLICATION app_upgrade_pub FOR ALL TABLES;
-- Vérifiez que wal_level supporte la logical replication
SHOW wal_level; -- doit valoir 'logical', pas 'replica'-- Sur la NOUVELLE réplique (PostgreSQL 17), après le chargement du schéma + des données initiales
CREATE SUBSCRIPTION app_upgrade_sub
CONNECTION 'host=old-primary.internal dbname=app user=replicator password=...'
PUBLICATION app_upgrade_pub
WITH (copy_data = false, create_slot = true, slot_name = 'app_upgrade_slot');Positionner copy_data = false est délibéré : vous avez déjà chargé les données via pg_dump/pg_restore à un LSN connu, donc la subscription doit seulement streamer les changements à partir de ce point, pas tout recopier. (Sautez le chargement manuel et copy_data = true gère lui-même la synchronisation initiale : plus simple, mais plus lent pour de grosses bases et moins de contrôle sur le snapshot.)
Les séquences ne sont pas répliquées par la logical replication : CREATE PUBLICATION FOR ALL TABLES couvre les tables, pas l'état des séquences. Basculez sans traiter ce point et le nouveau primaire distribuera des clés primaires en collision avec des lignes déjà écrites par l'ancien juste avant le basculement. Interrogez pg_sequences sur l'ancien primaire juste avant le basculement et faites un setval() sur chaque séquence de la nouvelle instance pour la faire correspondre, avec une marge, comme une des dernières étapes avant de reprendre les écritures.
Surveillez aussi le DDL : la logical replication ne réplique pas les changements de schéma. Livrez une migration pendant que la subscription tourne en l'appliquant manuellement des deux côtés, schéma du publisher en sur-ensemble (ou identique) de celui du subscriber. Colonne nullable ajoutée d'abord côté subscriber ; colonne supprimée en ordre inverse.
Surveiller le lag de réplication jusqu'à rattrapage complet
Ne faites pas confiance à une subscription qui affiche active, faites confiance au chiffre de lag. Exécutez ceci sur l'ancien primaire pour voir de combien la nouvelle réplique est en retard, en temps comme en octets :
SELECT
slot_name,
active,
pg_current_wal_lsn() AS publisher_lsn,
confirmed_flush_lsn AS subscriber_confirmed_lsn,
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) AS lag_bytes,
now() - pg_stat_activity.query_start AS wal_sender_age
FROM pg_replication_slots
LEFT JOIN pg_stat_activity ON pg_stat_activity.pid = pg_replication_slots.active_pid
WHERE slot_name = 'app_upgrade_slot';Côté subscriber, pg_stat_subscription donne la vue correspondante : latest_end_lsn et latest_end_time indiquent sa dernière confirmation de flush. Surveillez la convergence de lag_bytes vers zéro (ou un chiffre faible et stable) sous charge normale. Un lag qui oscille sans converger signifie généralement que le matériel de la nouvelle réplique ne suit pas le débit d'écriture.
C'est pour ça que je fais tourner cette requête en boucle pendant au moins 24 à 48 heures, sur un cycle complet de pic de trafic : une subscription à jour un mardi après-midi calme ne dit rien de sa tenue sous charge réelle.
La séquence de basculement
Une fois le lag constamment proche de zéro sur un cycle complet de trafic, le basculement lui-même devrait prendre des secondes, pas des minutes :
#!/usr/bin/env bash
set -euo pipefail
OLD_PRIMARY="old-primary.internal"
NEW_PRIMARY="new-primary.internal"
APP_CONN_STRING_SECRET="projects/prod/secrets/app-db-conn"
echo "[1/6] Mise en pause des écritures applicatives (readiness probe renvoie 503)..."
kubectl -n app scale deployment app-writer --replicas=0
echo "[2/6] Attente de la purge des transactions en cours..."
psql -h "$OLD_PRIMARY" -Atc \
"SELECT count(*) FROM pg_stat_activity WHERE state = 'active' AND pid <> pg_backend_pid();"
sleep 5
echo "[3/6] Vérification que le lag de réplication est nul..."
LAG=$(psql -h "$OLD_PRIMARY" -Atc \
"SELECT pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) FROM pg_replication_slots WHERE slot_name = 'app_upgrade_slot';")
if [ "$LAG" -ne 0 ]; then
echo "ABANDON : le lag est de $LAG octets, pas zéro. Basculement non sûr." >&2
exit 1
fi
echo "[4/6] Réconciliation des séquences sur le nouveau primaire..."
psql -h "$NEW_PRIMARY" -f ./sync-sequences.sql
echo "[5/6] Redirection de la chaîne de connexion applicative vers le nouveau primaire..."
gcloud secrets versions add "$APP_CONN_STRING_SECRET" --data-file=./new-primary-conn.txt
echo "[6/6] Reprise des écritures applicatives contre le nouveau primaire..."
kubectl -n app scale deployment app-writer --replicas=3L'étape 3 est le véritable garde-fou : si elle échoue, vous abandonnez et laissez l'ancien primaire servir le trafic, intact.
La redirection DNS/chaîne de connexion à l'étape 5 est là où les équipes perdent du temps si elles ne l'ont pas répétée : un TTL trop long, un connection pooler (PgBouncer, RDS Proxy) qui garde l'ancien hôte en cache, ou une application qui ne se reconnecte pas proprement au changement. Répétez cette redirection contre une cible jetable en amont, séparément du test de la migration.
Plan de repli
L'avantage de cette approche sur un pg_upgrade sur place : l'ancien primaire existe toujours, intact, juste après le basculement. Gardez-le actif plutôt que de le décommissionner le jour même, conservez aussi le slot de réplication, et conservez-le pendant au moins un cycle métier complet, assez long pour attraper les problèmes de charge de pic ou de traitement batch hebdomadaire.
Si la nouvelle version se comporte mal après le basculement (une régression du planificateur de requêtes, une incompatibilité d'extension, un client qui n'apprécie pas un changement de protocole réseau), le plan de repli est le même script de basculement exécuté à l'envers ; redirection vers l'ancien primaire, reprise des écritures : un véritable retour en arrière, pas un exercice de restauration-depuis-sauvegarde-et-on-espère.
Si vous revenez en arrière, garder ce slot de réplication actif vous permet de rebasculer vers l'avant plus tard sans repartir de zéro sur la synchronisation.
Conclusion
Un basculement de 30 secondes et une interruption de quatre heures peuvent porter sur la même migration, vers la même version. Ce qui les sépare, c'est que l'ancienne et la nouvelle base aient tourné en parallèle assez longtemps pour prouver que la nouvelle fonctionne avant de vous y engager. pg_upgrade sur place vous fait sauter cette étape et vous la refacture plus tard, en interruption de service, à un moment que vous ne choisissez pas.
Envie de faire tourner ça en production ?
Ce tutoriel couvre les concepts et l'architecture. Si vous voulez l'implémenter dans votre propre infrastructure, ou monter en compétence pour posséder ce sujet durablement, je propose du mentorat individuel construit autour de votre environnement réel, pas une formation générique.
Ce tutoriel
- Concepts clés et architecture principale
- Extraits de code illustratifs
- Le raisonnement derrière chaque décision
Mentorat individuel
- Des sessions de travail sur votre propre environnement
- Des réponses directes aux cas particuliers que vous rencontrez
- Un retour sur votre implémentation réelle
- Un accompagnement continu pendant que vous la construisez