Publié le: 2024-07-29

Upgrade majeure de PostgrESQL à chaud sans downtime

Si vous cherchez à faire une upgrade d’engine PostgreSQL d’une version majeure à une autre, il n’est pas possible de le faire en utilisant une réplication physique standard, et vous pouvez le faire à froid avec la commande pg_upgrade. Cela fonctionera, mais nécessite de couper complètement l’engine, et peut prendre des heures suivant la volumétrie, ce qui en terme de service rendu n’est clairement pas optimal.

Note: je vous conseille d’utiliser Patroni si vous faites tourner PostgreSQL en réplication, c’est le meilleur outil opensource du marché à l’heure actuelle pour gérer des clusters PostgreSQL.

Nous allons voir ici comment pouvoir faire des upgrades majeures PostgreSQL à chaud, sans downtime (mais avec quelques petites subtilités).

Prérequis

  • Un cluster/instance PostgreSQL source
  • Un cluster/instance PostgreSQL à la version cible de destination
  • Ne pas modifier le schéma pendant la migration
  • la configuration max_logical_replication_workers adaptée, si vous avez énormément de bases de données (il faut compter 2 workers par base)
  • la configuration wal_level mise à logical sur les 2 clusters (attention il faut rédémarrer et pas juste reload les instances)

Vous pouvez vérifier la configuration de celui-ci à chaud de la manière suivante:

sudo su - postgres -c 'psql postgres -c "show wal_level;"'
 wal_level
-----------
 logical
(1 row)

Si vous utilisez patroni il faudra éditer la configuration (patronictl -c /etc/patroni/config.yml edit-config) et redémarrer chaque instance:

postgresql:
  parameters:
    ...
    wal_level: logical
    max_logical_replication_workers: 8

Migration

Mise en place de la réplication logique

Afin de mettre en place la réplication logique, nous avons besoin de reproduire notre schéma (sans les données) dans le cluster de destination:

export PGPASSWORD="<redacted>"
pg_dumpall -h <legacy_pg_primary> -U postgres --schema-only | psql -h <new_pg> -U postgres
unset PGPASSWORD

Si vous profitez de cette migration pour changer de topologie, vous pouvez ne répliquer qu’une base source par exemple:

export PGPASSWORD="<redacted>"
pg_dump -h <legacy_pg_primary> -U postgres --schema-only <database_name> | psql -h <new_pg> -U postgres <new_database_name>
unset PGPASSWORD

Maintenant il ne nous reste plus qu’à répliquer. Cela va utiliser le modèle de pub/sub de PostgreSQL.

Sur l’ancien cluster, il faut créer une publication pour toutes les tables:

CREATE PUBLICATION zero_downtime_upgrade FOR ALL TABLES;

Maintenant sur le nouveau cluster on va y souscrire:

CREATE SUBSCRIPTION my_subscription CONNECTION 'dbname=mydb host=myhost user=myuser password=mypass' PUBLICATION zero_downtime_upgrade;

A partir de maintenant le PostgreSQL cible va récupérer les données de toutes les tables depuis la publication. Si ce nouveau PostgreSQL est d’une version >= 16 vous allez pouvoir suivre table par table l’avancée de la réplication initiale dans les logs.

La réplication logique prend énormément d’espace disque au niveau des WALs. Si vous archivez ces WALs, il se peut que l’archive soit trop long par rapport à la vitesse de réplication, ce qui risque de remplir tout l’espace. Ce que vous pouvez faire c’est les nettoyer nettoyer une partie toutes les minutes pendant cette synchronisation initiale, l’archivage n’est pas très important à ce moment du process, il le sera uniquement une fois la synchronisation initiale terminée.

Vous pouvez faire tourner dans un tmux le script suivant qui va garder les 50 derniers WALs sur le système de fichiers:

cd /var/lib/postgresql/16/main/data
while true; do
	pg_archivecleanup pg_wal $(ls  pg_wal/|tail -50|head -1) && sleep 60
done

Si votre cible est un cluster et pas une instance isolée, validez avant que ses replicas suivent bien la réplication au fil de l’eau et ne sont pas perdus, par ex si leurs performances CPU/disques sont moindres que le primaire.

Vous pouvez vérifier l’avancement de la réplication en lisant les offsets sur les 2 clusters:

  • Sur l’ancien cluster, dans les publications et slots de réplication
select slot_name,confirmed_flush_lsn from pg_replication_slots;
select application_name,replay_lsn from pg_stat_replication;
  • Sur le nouveau cluster, dans les souscriptions:
select subname,received_lsn from pg_catalog.pg_stat_subscription;

Bascule sur le nouveau cluster

Une fois la synchronisation initiale terminée, cela va synchroniser au fur et à mesure les données de l’ancien vers le nouveau cluster. Vous pouvez basculer sur le nouveau cluster en changeant vos URL/identifiants dès que vous le souhaitez, néanmoins il faut prendre en considération quelques points:

  • Si la base écrit en insertion, les séquences vont évoluer dans le temps, si votre schéma contient des champs de type SERIAL ou BIGSERIAL. Ceux-ci ne sont pas répliqués dans la mécanique de pub/sub. Cela va déclencher des soucis à l’insertion sur votre cluster cible, du fait que vos séquences ont été réinitialisées

Sur Stackoverflow (source) quelqu’un propose astucieusement le script suivant pour générer les requêtes à jouer sur le nouveau cluster afin de remettre les séquences au bon offset:

#!/bin/bash
port=$1
db=$2

query="select schemaname as schema,
       sequencename as sequence,
       start_value,
       last_value
from pg_sequences order by sequencename asc;
"

while read schema sequence start_value last_value
do
        if [ -z "$last_value" ]
        then
        echo "SELECT pg_catalog.SETVAL('${schema}.\"${sequence}\"', $start_value, true);"
        else
                echo "SELECT pg_catalog.SETVAL('${schema}.\"${sequence}\"', $last_value, true);"
        fi

done < <(psql -t -A -F" " -p ${port} ${db} -c "${query}")
  • Si vous utilisez les réplicas en pool de lecture et pas uniquement le primaire, il faut migrer d’abord les URI PostgreSQL en lecture seule puis le primaire, sinon vous lirez un état plus ancien que celui réel, dans le sens contraire.

Considérations finales

Cet article donne une idée générale de la migration “à chaud” d’une version majeure de PostgreSQL, cela va couvrir énormément de cas d’usage courants néanmoins il faut prendre en considération la topologie de l’application concernée, est-ce qu’elle sépare lectures et écritures ? Est ce que la latence est importante ? Est ce que je peux me permettre une micro coupure pour m’assurer que tout a été répliqué ? Ce sont des points à traîter au cas par cas et qu’un simple article sur le net ne peut traîter à votre place :).