Mise à jour majeure par réplication logique¶
Abstract
Ce document décrit le principe d’une migration majeure en utilisant la réplication logique.
Une réplication inversée est prévue pour revenir en arrière sur l’ancien serveur au besoin.
Le but de cette procédure est d’être utilisée comme base de travail pour une mise à jour majeure éligible à cette technique. Il est vivement conseillé de l’adapter et la tester dans un environnement de test avant de l’exécuter en production.
Limitations¶
Info
La réplication logique ne réplique que les tables (ou plutôt leurs modifications).
Ne sont pas répliqués : les vues, les vues matérialisées, les séquences, les index, etc.
Les Large Objects non plus ne sont pas répliqués.
Migration logique depuis ou vers PostgreSQL 10
Cette procédure n’est pas viable si votre application utilise l’ordre
TRUNCATE
sur PostgreSQL 10, qui n’est pas répliqué sur cette version,
à moins de renoncer à tout TRUNCATE
le temps de la migration.
De même, un ordre TRUNCATE
répliqué d’une version supérieure vers la version 10 (suite à un switchover par exemple) lèvera une erreur et bloquera la réplication.
Versions antérieures à PostgreSQL 10
Les versions antérieures à la 10 ne disposent pas de réplication logique, dans un sens ou l’autre. Nous conseillons d’utiliser Slony au besoin.
Pas d’ordres DDL
Il est recommandé d’interdire tout ordre DDL du début à la fin de la mise à jour majeure. Ceux-ci ne sont pas supportés par la réplication logique et demandent une maintenance non négligeable pour les intégrer correctement.
Définition de l’environnement de démonstration¶
Pour information, l’environnement utilisé dans cet exemple de constitué de 2 machines virtuelles Debian 9:
- srv10 (adresse
192.168.122.88/24
) - srv11 (
192.168.122.89/24
).
Création de deux alias :
root@srv10:~# cat >>/etc/hosts<<EOF
192.168.122.88 srv10
192.168.122.89 srv11
EOF
root@srv11:~# cat >>/etc/hosts<<EOF
192.168.122.88 srv10
192.168.122.89 srv11
EOF
Installation de PostgreSQL¶
Cette partie est classique.
- Sur srv10 : PostgreSQL 10 via les paquets PGDG :
- Sur srv11 : PostgreSQL 11 via les paquets PGDG :
Utilisateurs¶
srv10 est défini comme origine au début de la procédure.
Nous allons créer une base pgbench
qui sera ensuite répliquée vers l’instance v 11 :
root@srv10:~$ su - postgres
postgres@srv10:~$ createuser -P bench
Enter password for new role:
Enter it again:
postgres@srv10:~$ cat >>~postgres/.pgpass<<EOF
*:*:*:bench:pass
EOF
postgres@srv10:~$ chmod 600 ~postgres/.pgpass
postgres@srv10:~$ createdb -O bench bench
postgres@srv10:~$ pgbench -s 50 -i -U bench -h localhost bench
Configuration système¶
Pour éviter toute incohérence, s’assurer que les horloges des deux serveurs sont bien synchronisées par NTP :
Configuration PostgreSQL¶
Les actions ci-après sont à réaliser sur les deux instances.
- Créer un rôle de réplication (ici
repli
) - renseigner son mot de passe dans le
~postgres/.pgpass
- autoriser ses connexions dans
pg_hba.conf
root@srv10:~$ su - postgres
postgres@srv10:~$ createuser --replication -P repli
Enter password for new role:
Enter it again:
postgres@srv10:~$ cat >>~postgres/.pgpass<<EOF
*:*:*:repli:repass
EOF
postgres@srv10:~$ chmod 600 ~postgres/.pgpass
postgres@srv10:~$ cat >> /etc/postgresql/10/main/pg_hba.conf <<EOF
host replication repli 192.168.122.88/32 md5
host replication repli 192.168.122.89/32 md5
EOF
postgres@srv10:~$ psql -c "alter system set wal_level to logical"
postgres@srv10:~$ exit
root@srv10:~$ systemctl restart postgresql@10-main
postgres@srv11:~$ cat >>~postgres/.pgpass<<EOF
*:*:*:repli:repass
EOF
postgres@srv11:~$ chmod 600 ~postgres/.pgpass
postgres@srv11:~$ cat >> /etc/postgresql/11/main/pg_hba.conf <<EOF
host replication repli 192.168.122.88/32 md5
host replication repli 192.168.122.89/32 md5
EOF
postgres@srv11:~$ psql -c "alter system set wal_level to logical"
postgres@srv11:~$ exit
root@srv11:~$ systemctl restart postgresql@11-main
Clés primaires¶
Clés primaires
Il est très fortement recommandé d’avoir une clé primaire sur chaque table à répliquer
Rechercher les tables sans clé primaire et les ajouter, quitte à créer une colonne si nécessaire donc :
bench=# SELECT n.nspname, c.relname FROM pg_class c
JOIN pg_namespace n ON n.oid=c.relnamespace
WHERE c.relkind='r' AND n.nspname !~ '^(pg_.*|information_schema)$'
AND NOT EXISTS (select 1 from pg_index i where i.indrelid = c.oid and i.indisprimary);
nspname | relname
---------+-----------------
public | pgbench_history
(1 ligne)
bench=# ALTER TABLE public.pgbench_history ADD COLUMN id integer
PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY;
ALTER TABLE
Certaines tables peuvent avoir une contrainte unique sur une colonne NOT NULL
(fonctionnellement équivalente à une clé primaire).
Il suffit alors de déclarer l’index associé comme identité :
Réplication du schéma¶
Il est nécessaire de préparer les bases vides côté destination avant la mise en réplication.
- Répliquer les objets globaux vers l’instance de destination, c’est-à-dire les rôles, les mots de passe, et les éventuels tablespaces nécessaires :
Il peut y avoir des erreurs si certains objets globaux existent déjà. Il est nécessaire de
vérifier les erreurs renvoyées par psql
.
- Créer une base vide avec le même schéma que la base d’origine dans l’instance de
destination.
pg_dump
est idéal pour cela :
postgres@srv11:~$ createdb -e -O bench bench
postgres@srv11:~$ pg_dump -h srv10 -U bench -v --schema-only bench | psql bench
Mise en œuvre de la réplication¶
Nous pouvons désormais configurer la réplication logique entre les deux instances.
- Commençons par l’initialisation de la publication sur srv10 :
root@srv10:~$ su - postgres
postgres@srv10:~$ psql -c 'CREATE PUBLICATION pub_bench_10 FOR ALL TABLES' bench
CREATE PUBLICATION
- Nous devons ensuite nous assurer que l’utilisateur de réplication
a le droit de lire les données de la base
bench
:
- Nous pouvons désormais créer l’abonnement sur srv11:
root@srv11:~$ su - postgres
postgres@srv11:~$ cat <<'EOQ' | psql bench
CREATE SUBSCRIPTION sub_bench_11
CONNECTION 'host=srv10 user=repli dbname=bench'
PUBLICATION pub_bench_10
EOQ
NOTICE: created replication slot "sub_bench_11" on publisher
CREATE SUBSCRIPTION
- La phase d’initialisation débute. Les données sont répliquées de l’origine (publisher) srv10 vers l’abonné (subscriber) srv11.
Durant la phase d’initialisation, vous trouverez les messages suivants dans les traces de l’origine :
[6355] LOG: logical decoding found consistent point at 0/47ED6268
[6368] LOG: starting logical decoding for slot "sub_bench_11"
[6368] DETAIL: streaming transactions committing after 0/47ED62A0, reading WAL from 0/47ECE438
[6368] LOG: logical decoding found initial starting point at 0/47ECE438
[6368] DETAIL: Waiting for transactions (approximately 1) older than 8584 to end.
[6368] LOG: logical decoding found consistent point at 0/47ED6268
[6369] LOG: logical decoding found consistent point at 0/47EE1FA8
[6370] LOG: logical decoding found consistent point at 0/47EF3DC0
[6383] LOG: logical decoding found initial starting point at 0/47F36FC0
[6383] DETAIL: Waiting for transactions (approximately 1) older than 8611 to end.
[6383] LOG: logical decoding found consistent point at 0/47F39170
[6411] LOG: logical decoding found consistent point at 0/47F64BA8
[6369] LOG: starting logical decoding for slot "sub_bench_11_16526_sync_16505"
Ces messages concernent l’activité du processus principal de la publication,
ainsi que l’activité des différents processus de synchronisation.
Noter que le processus 6369
utilise un slot de réplication temporaire
(ici sub_bench_11_16526_sync_16505
) pour resynchroniser sa table
(pgbench_accounts
) après le premier import de donnée.
postgres@srv11:~$ oid2name -d bench -o 16505
From database "bench":
Filenode Table Name
----------------------------
16505 pgbench_accounts
Coté processus, la phase d’initialisation peut synchroniser plusieurs tables
en même temps en fonction du paramètre max_sync_workers_per_subscription
.
Il peut donc y avoir de processus de synchronisation dans la vue
pg_stat_activity
. Voici un exemple avec deux processus de synchronisation actifs :
postgres@srv10:~$ psql -c 'SELECT application_name,state,query,backend_type FROM pg_stat_activity WHERE pid<>pg_backend_pid()'
application_name | state | query | backend_type
-------------------------------+--------+-------------------------------------------------+--------------
sub_bench_11 | active | | walsender
sub_bench_11_16526_sync_16505 | active | COPY public.pgbench_accounts TO STDOUT | walsender
sub_bench_11_16526_sync_16516 | active | BEGIN READ ONLY ISOLATION LEVEL REPEATABLE READ | walsender
-
Coté vue
pg_stat_replication
, il existe autant de lignes que de walsender. Ceux-ci passent de l’étatstartup
lors de la synchronisation, àcatchup
une fois le premier snapshot importé, et pendant le rattrapage, et enfinstreaming
une fois que la table est synchronisée. -
Une fois toutes les tables initialisées, tous ces processus d’initialisation (et les slot temporaires associés) disparaissent.
Coté abonné, vous trouverez dans les log les messages suivants :
LOG: logical replication apply worker for subscription "sub_bench_11" has started
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_accounts" has started
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_branches" has started
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_branches" has finished
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_history" has started
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_history" has finished
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_tellers" has started
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_tellers" has finished
LOG: logical replication table synchronization worker for subscription "sub_bench_11", table "pgbench_accounts" has finished
Bascule¶
Arrêt des connexions¶
Avant de basculer vers la nouvelle machine, il est nécessaire de positionner
les applications clientes en mode maintenance.
Afin de sécuriser l’ensemble, nous ajoutons les lignes suivantes en début de fichier
pg_hba.conf
afin d’empêcher toute connexion autre que celle concernant la réplication:
host bench repli 192.168.122.89/0 md5
local bench postgres peer
local bench all reject
host bench all 0.0.0.0/0 reject
postgres@srv10:~$ pg_ctlcluster 10 main reload
Tuer toutes les connexions restantes à la base d’origine :
postgres@srv10:~$ cat<<EOQ|psql
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND pid <> pg_backend_pid()
AND datname='bench';
EOQ
- Attendre que le lag entre les deux serveurs soit entièrement résorbé.
Depuis srv10, observer l’évolution des compteurs de lag présentés par
pg_stat_replication
. Noter que même si les autres compteurs évoluent encore,
ces écritures ne concernent plus les données de la base pgbench
(et ces écritures
ne sont pas envoyées vers srv11). Il faut attendre quelque secondes
que les dalais affichés soient résorbés.
Alternativement, il est possible de simplement comparer des données fonctionnellement
importantes de la réplication. Dans le cas d’une base pgbench
:
postgres@srv11:~$ psql -Atc "SELECT max(id) FROM pgbench_history" bench
32595
postgres@srv11:~$ psql -Atc "SELECT max(id) FROM pgbench_history" -h srv10 -U repli bench
32595
- La réplication logique ne réplique pas les séquences. Aussi, il faut mettre à jour les séquences sur srv11 avant d’effectuer la bascule.
postgres@srv11:~$ cat <<'EOQ' | psql -At -h srv10 -U repli bench | psql bench
SELECT format( 'SELECT setval(%L, %s)',
schemaname||'.'||sequencename, last_value
)
FROM pg_sequences
EOQ
- S’il y a des vues matérialisées, prévoir de les rafraîchir si nécessaire avant la bascule. Voici une requête pour les lister avec leur DDL :
SELECT 'REFRESH MATERIALIZED VIEW ' || nspname || '.' || relname || ';'
FROM pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE relkind = 'm';
Bascule¶
Nous pouvons désormais effectuer la bascule, qui consiste à échanger les rôles au sein de la réplication logique.
- Suppression de l’abonnement et création d’une publication inverse :
postgres@srv11:~$ psql -c 'DROP SUBSCRIPTION sub_bench_11 bench'
postgres@srv11:~$ psql -c 'GRANT bench TO repli'
postgres@srv11:~$ psql -c 'CREATE PUBLICATION pub_bench_11 FOR ALL TABLES' bench
- Abonnement de l’ancien serveur auprès du nouveau :
Tip
Puisque les données sont déjà présentes sur srv10, nous demandons explicitement de ne pas resynchroniser les données lors de la création de l’abonnement :
postgres@srv10:~$ cat <<'EOQ' | psql bench
CREATE SUBSCRIPTION sub_bench_10
CONNECTION 'host=srv11 user=repli dbname=bench'
PUBLICATION pub_bench_11
WITH (copy_data = false)
EOQ
NOTICE: created replication slot "sub_bench_10" on publisher
CREATE SUBSCRIPTION
- Les applications peuvent recommencer à travailler sur le nouveau serveur. Les données seront répliquées vers l’ancien, permettant un éventuel retour en arrière.
Si un retour en arrière n’est pas prévu
Si l’on est sûr de ne jamais vouloir rebasculer sur l’ancien serveur,
le DROP SUBSCRIPTION
plus haut suffit.
Suppression de l’ancien serveur¶
Une fois la bascule validée, il suffit de supprimer la réplication logique entre les deux serveurs :
postgres@srv10:~$ psql -c 'DROP subscription sub_bench_10 cascade' bench
NOTICE: dropped replication slot "sub_bench_10" on publisher
DROP SUBSCRIPTION
postgres@srv11:~$ psql -c 'DROP publication pub_bench_11 cascade' bench
DROP PUBLICATION
Références¶
- Documentation officielle :
- La réplication logique
- Restrictions de la réplication logique : dernière version de PostgreSQL, PostgreSQL 10
- Formations Dalibo :
- Réplication logique : module W5
- Workshops Dalibo :