Aller au contenu

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 :
apt install postgresql-10 postgresql-contrib-10 postgresql-client-10
  • Sur srv11 : PostgreSQL 11 via les paquets PGDG :
apt install postgresql-11 postgresql-client-11 postgresql-contrib-11

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 :

root@srv10:~$ timedatectl |grep ^NTP
NTP synchronized: yes

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é :

ALTER TABLE <schema>.<relname> REPLICA IDENTITY USING INDEX <idxname>

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 :
postgres@srv11:~$ pg_dumpall -h srv10 -U postgres --globals|psql

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 :
postgres@srv10:~$ psql -c "GRANT bench to repli"
  • 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’état startup lors de la synchronisation, à catchup une fois le premier snapshot importé, et pendant le rattrapage, et enfin streaming 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