Montée de version majeure de PostgreSQL sans arrêt
Comment passer d'une version majeure à la suivante par réplication logique, ce que la bascule coûte vraiment, comment garder un retour possible, et à partir de quand un simple pg_upgrade est le meilleur choix.
Par Elias Varen7 min de lecture
« Sans arrêt » est un abus de langage que j'assume dans le titre et que je corrige ici. Une montée de version majeure par réplication logique ne supprime pas la coupure, elle la réduit à quelques secondes d'écritures suspendues. En échange, vous achetez deux à trois semaines de préparation, un doublement temporaire du stockage et un gel des migrations de schéma. Si votre plateforme supporte dix minutes de maintenance un dimanche matin, tout cela est superflu, et la dernière section explique pourquoi.
Pourquoi pg_upgrade impose une coupure
pg_upgrade réécrit le catalogue système de l'ancienne version vers la nouvelle et réutilise les fichiers de données. Avec --link, il crée des liens physiques au lieu de copier, et l'opération dure quelques minutes quelle que soit la taille de la base. C'est rapide, mais l'instance est arrêtée pendant tout le traitement, et d'autres facteurs allongent la coupure réelle au-delà du chiffre que donne l'outil.
Les statistiques du planificateur ne sont pas toujours conservées selon la version de départ. Tant que l'ANALYZE n'a pas repassé, les plans sont choisis à l'aveugle, et rouvrir le trafic à ce moment revient à rouvrir sur une base lente. Les extensions doivent exister dans la nouvelle version avec une bibliothèque compatible ; PostGIS, en particulier, demande d'avoir validé le couple de versions avant le jour J. Enfin, avec --link, le rollback disparaît dès que la nouvelle instance a démarré, puisque les deux répertoires partagent les mêmes fichiers et que l'ancien n'est plus utilisable.
C'est ce dernier point, plus que la durée de coupure, qui me fait choisir la réplication logique sur une base qui compte. Je veux deux instances indépendantes, et un chemin de retour qui ne soit pas « restaurer la sauvegarde ».
Le montage : deux instances, un flux de changements
La réplication physique copie des blocs, donc elle exige la même version majeure des deux côtés. La réplication logique décode le WAL en changements de lignes (INSERT, UPDATE, DELETE) et les rejoue en SQL sur une autre instance, qui peut être d'une version différente. C'est cette propriété qu'on exploite.
ancienne (v N) nouvelle (v N+1)
publication ── slot logique ──▶ subscription
trafic applicatif schéma restauré, aucune écriture applicativeLa nouvelle instance reçoit d'abord le schéma seul (pg_dump --schema-only, rôles compris), puis s'abonne :
-- sur l'ancienne instance
CREATE PUBLICATION upgrade_pub FOR ALL TABLES;
-- sur la nouvelle
CREATE SUBSCRIPTION upgrade_sub
CONNECTION 'host=pg-old dbname=app user=replicator'
PUBLICATION upgrade_pub;L'abonnement lance une copie initiale table par table, puis applique le flux. Pendant la copie, le slot retient sur l'ancienne instance tout le WAL produit depuis sa création. Sur une base de quelques centaines de gigaoctets avec un trafic d'écriture soutenu, cette rétention se compte en dizaines de gigaoctets, et l'espace libre du volume WAL se vérifie avant de commencer.
Ce que le flux ne transporte pas
La réplication logique transporte des lignes. Tout le reste est à votre charge, et chaque oubli de cette liste devient un incident de bascule.
Les tables sans clé primaire. Pour rejouer un UPDATE ou un DELETE, l'abonné doit identifier la ligne. Sans clé primaire ni REPLICA IDENTITY, la publication fait échouer ces écritures sur la source, donc en production. Il faut les chercher avant de publier :
SELECT c.oid::regclass AS table_sans_pk
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.oid AND i.indisprimary
);Le DDL. Une colonne ajoutée sur la source n'apparaît pas sur la cible, et le premier changement qui la contient arrête l'application du flux. Chez nous, aucune migration de schéma ne passe entre la création de l'abonnement et la bascule. C'est le coût le plus visible pour l'équipe, et la raison pour laquelle la fenêtre doit rester courte.
Les séquences. Dans les versions que vous avez probablement en production, les valeurs de séquences ne sont pas répliquées. La cible a les lignes avec leurs identifiants, mais ses séquences sont restées à leur valeur initiale, et le premier INSERT après la bascule viole la clé primaire. Il faut les recaler au moment de basculer, avec une marge.
-- à exécuter sur l'ancienne, le résultat se rejoue sur la nouvelle
SELECT format('SELECT setval(%L, %s);',
schemaname || '.' || sequencename,
last_value + 1000)
FROM pg_sequences
WHERE last_value IS NOT NULL;Le reste. Les large objects ne passent pas. Les vues matérialisées sont à rafraîchir sur la cible. Les statistiques sont à construire, par un ANALYZE complet sur la nouvelle instance une fois la copie terminée, suivi d'une comparaison des plans de vos dix requêtes les plus fréquentes avant d'y envoyer du trafic.
La bascule, seconde par seconde
La bascule tient en une idée : arrêter les écritures, attendre que la cible ait tout reçu, puis rediriger. Si vos applications passent par PgBouncer, vous disposez de l'outil exact. PAUSE laisse les transactions en cours se terminer et met les nouvelles en attente, sans couper les connexions clientes ; les requêtes patientent au lieu d'échouer.
1. PAUSE sur PgBouncer (les clients attendent)
2. attendre retard du flux = 0
3. recaler les séquences sur la cible
4. monter le flux inverse (section suivante)
5. pointer PgBouncer vers la nouvelle instance, RELOAD
6. RESUME (les clients repartent)Le retard se lit sur l'ancienne instance :
SELECT slot_name,
pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn) AS octets_en_retard
FROM pg_replication_slots
WHERE slot_name = 'upgrade_sub';Écritures suspendues, ce nombre tombe à zéro en une à deux secondes si le flux était à jour avant la pause. Comptez cinq à dix secondes pour l'ensemble de la séquence quand elle est scriptée. Elle doit l'être, parce qu'une bascule tapée à la main sous stress oublie une étape, et l'étape oubliée est toujours celle des séquences.
Une transaction longue en cours au moment du PAUSE bloque tout : la pause attend sa fin, et les clients attendent la pause. Avant de lancer, je regarde pg_stat_activity et je fixe un délai maximal au-delà duquel le script annule et fait RESUME. Un abandon propre vaut mieux qu'une coupure d'une minute.
Garder un retour : le flux inverse
Une fois le trafic ouvert sur la nouvelle version, l'ancienne instance commence à dater. Dix minutes plus tard, y revenir signifie perdre dix minutes d'écritures. Le retour n'existe que si l'ancienne instance continue à recevoir les changements, donc si la réplication est montée dans l'autre sens pendant la pause, avant le RESUME.
-- sur la nouvelle instance (pendant la pause)
CREATE PUBLICATION rollback_pub FOR ALL TABLES;
-- sur l'ancienne, après avoir supprimé son rôle de source
CREATE SUBSCRIPTION rollback_sub
CONNECTION 'host=pg-new dbname=app user=replicator'
PUBLICATION rollback_pub
WITH (copy_data = false);Tout repose sur copy_data = false : les deux instances sont identiques à cet instant, il ne faut pas recopier, seulement suivre. En cas de régression de performance dans l'heure qui suit, on refait la même séquence en sens inverse sans perdre une seule écriture.
Ce filet a une durée de vie. Tant qu'il existe, le gel du DDL continue, et un slot ouvert sur votre nouvelle production retient du WAL si l'ancienne instance tombe. Je fixe l'échéance avant de commencer : quarante-huit heures de production normale, un cycle complet de traitements nocturnes, puis je supprime l'abonnement, le slot et l'ancienne instance. Un flux inverse oublié remplit le disque en trois semaines.
Quand prendre la coupure à la place
La réplication logique n'est pas le choix par défaut. Elle déplace le risque d'une coupure courte et connue vers une préparation longue où chaque oubli se paie le jour de la bascule. Les questions se posent dans cet ordre :
- Dix minutes de maintenance annoncée sont-elles acceptables ? Si oui,
pg_upgrade --linksur un réplica promu, l'ancien primaire gardé intact comme retour, suffit. Cela représente une demi-journée de travail au lieu de trois semaines. - La base tient-elle sous quelques dizaines de gigaoctets ? Un
pg_dumpet unpg_restoreparallèles se terminent dans la fenêtre, et l'on repart avec des tables et des index sans bloat. - Pouvez-vous geler le DDL pendant toute la fenêtre ? Si l'équipe livre des migrations chaque jour et ne peut pas s'arrêter, la réplication logique cassera avant la bascule.
- Toutes vos tables ont-elles une clé primaire, et utilisez-vous des large objects ? Chaque exception est un chantier préalable.
- Avez-vous de quoi répéter sous charge ? Sans répétition réaliste, on remplace une coupure prévisible par une bascule jamais testée.
Je réserve la réplication logique aux cas où la coupure a un coût contractuel, ou à ceux où je veux le flux inverse comme assurance. Partout ailleurs, la coupure annoncée, courte et répétée la veille reste la procédure la plus sûre.
À lire ensuite
Toute la rubrique DonnéesDonnées
Files d'attente dans PostgreSQL : jusqu'où
Une table et SKIP LOCKED font une file d'attente sérieuse, jusqu'au jour où une transaction longue ailleurs dans la base la met à genoux. Le mécanisme, les deux patrons de consommation, et les signaux qui disent qu'il est temps d'en sortir.
6 minMembres
Données
Partitionner : quand la table unique ne suffit plus
Le partitionnement règle un problème de cycle de vie des données bien plus qu'un problème de requêtes lentes. Ce qu'il résout, ce qu'il aggrave, et le critère pour décider avant de migrer une table de production.
7 minLecture libre
Données
Sauvegardes : la seule métrique est la restauration
Un job de sauvegarde au vert ne prouve rien. Comment calculer un RPO et un RTO honnêtes, les défaillances qui rendent une sauvegarde inutilisable, et l'exercice de restauration automatisé qui remplace la confiance par une mesure.
7 minMembres