Cette version de PostgreSQL introduit une fonctionnalité qui manquait
à la réplication logique depuis longtemps : la possibilité de maintenir
la réplication logique malgré un switchover de la réplication physique
sur la grappe de serveurs provider . Nous allons détailler ici
les différents éléments qui constituent cette fonctionnalité.
Note : pour pouvoir exécuter les commandes ci-dessous, il faut
configurer le paramètre wal_level
à
logical
.
Propriété failover sur les slots de réplications
La fonction pg_create_logical_replication_slot()
s’est
vu ajoutée un nouveau paramètre failover
:
\df pg_create_logical_replication_slot()
List of functions
Name | Argument data types
------------------------------------+--------------------------------------------------------------
pg_create_logical_replication_slot | slot_name name, plugin name, temporary boolean DEFAULT false,
| twophase boolean DEFAULT false, failover DEFAULT false,
| OUT slot_name name, OUT lsn pg_lsn
(1 row)
Cette propriété indique que le slot de réplication logique sera
synchronisé sur l’instance secondaire d’un dispositif de réplication
physique, ce qui permettra de reprendre la réplication logique en cas de
switchover.
La vue pg_replication_slots
a également été mise à jour
afin de refléter la présence de cette propriété.
SELECT slots.*
FROM (VALUES ('rl_test_f' , true ),('rl_test_nof' , false )) AS slot(name, has_failover)
CROSS JOIN LATERAL pg_create_logical_replication_slot(name, 'test_decoding' , failover=> has_failover) AS slots;
slot_name | lsn
--------------+-----------
rl_test_f | 0/1AFD380
rl_test_nof | 0/1AFD3B8
(2 rows)
SELECT slot_name, slot_type, failover FROM pg_replication_slots ;
slot_name | slot_type | failover
--------------+-----------+----------
rl_test_f | logical | t
rl_test_nof | logical | f
(2 rows)
SELECT pg_drop_replication_slot(name)
FROM (VALUES ('rl_test_f' ),('rl_test_nof' )) AS slot(name);
L’option a également été ajoutée au protocole de réplication.
psql "dbname=postgres replication=database" <<_EOF_
CREATE_REPLICATION_SLOT test LOGICAL test_decoding (FAILOVER true);
SELECT slot_name, slot_type, failover FROM pg_replication_slots ;
_EOF_
slot_name | consistent_point | snapshot_name | output_plugin
-----------+------------------+---------------------+---------------
test | 0/1AFD460 | 00000074-00000002-1 | test_decoding
(1 row)
slot_name | slot_type | failover
-----------+-----------+----------
test | logical | t
(1 row)
Une nouvelle commande a été ajoutée au protocole de réplication pour
mettre à jour un slot :
psql "dbname=postgres replication=database" <<_EOF_
ALTER_REPLICATION_SLOT test (FAILOVER false);
SELECT slot_name, slot_type, failover FROM pg_replication_slots ;
_EOF_
ALTER_REPLICATION_SLOT
slot_name | slot_type | failover
-----------+-----------+----------
test | logical | f
(1 row)
psql "dbname=postgres replication=database" <<_EOF_
DROP_REPLICATION_SLOT test;
_EOF_
Ces mises à jour du protocole permettent à une standby de demander au
walsender de créer / mettre à jour un slot et définir sa
propriété failover
.
Capacité de synchroniser des slots depuis une standby
L’étape suivante dans le développement de cette fonctionnalité a été
de mettre en place la synchronisation des slots de réplications. Cette
fonctionnalité ajoute des contraintes à la mise en place de la
réplication physique :
la réplication physique doit utiliser un slot de réplication. Les
efforts réalisés pour invalider les slots si les standbys ne consomment
pas les slots et améliorer les vues de supervision, font que ce n’est
pas un risque pour la disponibilité du service sur l’instance
primaire ;
le hot_standby_feedback
doit être activé. Cela peut
avoir un impact sur le nettoyage des lignes mortes sur la primaire si
des requêtes longues sont lancées sur la standby.
la chaine de connexion spécifiée dans
primary_conninfo
doit contenir un nom de base de données
valide.
Pour utiliser la synchronisation des slots, il faut :
Il faudra ensuite exécuter la fonction
pg_sync_replication_slots()
pour forcer la synchronisation
du slot.
Une nouvelle colonne synched
a été ajoutée à la vue
pg_replication_slots
pour indiquer que le slot est un slot
synchronisé avec la primaire. En situation normale, il ne devrait donc
pas y avoir de slots synched
sur une instance primaire.
Un slot synchronisé ne peut pas être supprimé et son contenu ne peut
pas être consommé.
Si le slot de réplication logique de l’instance primaire est
invalidé, le slot synchronisé sur la secondaire sera également
invalidé.
Si le slot de réplication logique de l’instance secondaire est
invalidé sur la standby, il sera supprimé. Il faudra relancer la
commande pg_sync_replication_slots()
pour le recréer. Cette
invalidation peut être due au fait que le
max_slot_wal_keep_size
de la standby est insuffisant pour
retenir les enregistrements de WAL nécessaire pour satisfaire le
restart_lsn
du slot ou que le
primary_slot_name
a été supprimé.
Voici un exemple réalisé sous FEDORA avec
PGDATAS=~/tmp/failover
et PGUSER=postgres
, le
logging collector est activé, les sockets sont créés dans ‘/tmp’, les
traces et la configuration sont dans le répertoire de données. Toutes
les connexions sont faites via les sockets, pour qui la méthode
d’authentification est trust
(donc pas
d’authentification).
mise en place d’une instance primaire :
initdb --pgdata=$PGDATAS/primaire \
--set port=5495 \
--set wal_level=logical \
--set cluster_name=primaire \
--username=postgres
pg_ctl start --pgdata $PGDATAS/primaire
createuser replicator --replication --port=5495
mise en place d’une instance secondaire :
pg_basebackup --pgdata $PGDATAS/secondaire \
--progress --checkpoint=fast \
--create-slot \
--slot=secondaire \
--dbname="port=5495 user=replicator"
rm - f $PGDATAS/ secondaire/ log /*
touch $PGDATAS/secondaire/standby.signal
cat >> $PGDATAS/secondaire/postgresql.conf <<_EOF_
port = 5496
cluster_name = secondaire
primary_conninfo = 'port=5495 user=replicator dbname=postgres application_name=secondaire'
primary_slot_name = 'secondaire'
hot_standby_feedback = on
_EOF_
pg_ctl start --pgdata $PGDATAS/secondaire
vérification de la réplication : les walsenders et
walreceiver sont présents et la primaire est au statut
streaming.
ps f - ee | grep - E "(primaire|secondaire)"
300207 ? Ss 0:00 \_ /usr/pgsql-17/bin/postgres -D /home/benoit/tmp/failover/primaire
300208 ? Ss 0:00 | \_ postgres: primaire: logger
300209 ? Ss 0:00 | \_ postgres: primaire: checkpointer
300210 ? Ss 0:00 | \_ postgres: primaire: background writer
300212 ? Ss 0:00 | \_ postgres: primaire: walwriter
300213 ? Ss 0:00 | \_ postgres: primaire: autovacuum launcher
300214 ? Ss 0:00 | \_ postgres: primaire: logical replication launcher
303235 ? Ss 0:00 | \_ postgres: primaire: walsender replicator [local] streaming 0/B000168
303229 ? Ss 0:00 \_ /usr/pgsql-17/bin/postgres -D /home/benoit/tmp/failover/secondaire
303230 ? Ss 0:00 \_ postgres: secondaire: logger
303231 ? Ss 0:00 \_ postgres: secondaire: checkpointer
303232 ? Ss 0:00 \_ postgres: secondaire: background writer
303233 ? Ss 0:00 \_ postgres: secondaire: startup recovering 00000001000000000000000B
303234 ? Ss 0:00 \_ postgres: secondaire: walreceiver streaming 0/B000168
création d’un slot de réplication synchronisé et
synchronisation :
psql -p 5495 -c "SELECT pg_create_logical_replication_slot('test_sync', 'pgoutput', failover=>'true');"
psql -p 5496 -c "SELECT pg_sync_replication_slots();"
Vérifications :
psql <<_EOF_
\c postgres postgres /tmp 5495
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
\c postgres postgres /tmp 5496
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
_EOF_
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5495".
slot_name | slot_type | failover | synced
------------+-----------+----------+--------
secondaire | physical | f | f
test_sync | logical | t | f
(2 rows)
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
slot_name | slot_type | failover | synced
-----------+-----------+----------+--------
test_sync | logical | t | t
(1 row)
On voit que le slot est synchronisé sur la standby et que l’option
failover
est activée.
Comme promis, il n’est pas possible de supprimer le slot directement
sur la standby.
psql <<_EOF_
-- serveur de secondaire
\c postgres postgres /tmp 5496
SELECT pg_drop_replication_slot('test_sync');
-- serveur de primaire
\c postgres postgres /tmp 5495
SELECT pg_drop_replication_slot('test_sync');
_EOF_
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
ERROR: cannot drop replication slot "test_sync"
DETAIL: This replication slot is being synchronized from the primary server.
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5495".
pg_drop_replication_slot
--------------------------
Une fois le slot supprimé sur la primaire, le slot est encore visible
sur la standby. Il faut utiliser la fonction
pg_sync_replication_slots()
sur la standby pour le faire
disparaitre.
psql <<_EOF_
-- serveur de primaire
\c postgres postgres /tmp 5495
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
-- serveur de secondaire
\c postgres postgres /tmp 5496
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
_EOF_
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5495".
slot_name | slot_type | failover | synced
------------+-----------+----------+--------
secondaire | physical | f | f
(1 row)
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
slot_name | slot_type | failover | synced
-----------+-----------+----------+--------
test_sync | logical | t | t
(1 row)
psql -p 5496 <<_EOF_
SELECT pg_sync_replication_slots();
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
_EOF_
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
slot_name | slot_type | failover | synced
-----------+-----------+----------+--------
(0 rows)
mise en place de la publication :
psql -p 5495 -c "CREATE PUBLICATION pub FOR TABLES IN SCHEMA public;"
création d’une souscription et de l’instance associée :
initdb --pgdata=$PGDATAS/souscription \
--set port=5497 \
--set cluster_name=souscription \
--username=postgres
pg_ctl start --pgdata $PGDATAS/souscription
psql << _EOF_
-- serveur de souscription
\c postgres postgres / tmp 5497
CREATE SUBSCRIPTION sub
CONNECTION 'port=5495 dbname=postgres'
PUBLICATION pub
WITH (failover);
-- serveur de secondaire
\c postgres postgres / tmp 5496
SELECT pg_sync_replication_slots();
_EOF_
psql <<_EOF_
\c postgres postgres /tmp 5497
\x
\d Rs+
\x
\c postgres postgres /tmp 5495
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
\c postgres postgres /tmp 5496
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
_EOF_
List of subscriptions
-[ RECORD 1 ]------+------------------------------------------
Name | sub
Owner | postgres
Enabled | t
Publication | {pub}
Binary | f
Streaming | off
Two-phase commit | d
Disable on error | f
Origin | any
Password required | t
Run as owner? | f
Failover | t
Synchronous commit | off
Conninfo | port=5495 user=replicator dbname=postgres
Skip LSN | 0/0
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5495".
slot_name | slot_type | failover | synced
------------+-----------+----------+--------
secondaire | physical | f | f
sub | logical | t | f
(2 rows)
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
slot_name | slot_type | failover | synced
-----------+-----------+----------+--------
sub | logical | t | t
(1 row)
Si on supprime le slot, il faut aussi relancer
pg_sync_replication_slots()
sur la standby.
psql <<_EOF_
-- serveur de souscription
\c postgres postgres /tmp 5497
DROP SUBSCRIPTION sub;
-- serveur de secondaire
\c postgres postgres /tmp 5496
SELECT pg_sync_replication_slots();
_EOF_
Synchronisation automatique des slots
La synchronisation des slots peut être automatiquement réalisée en
activant le paramètre sync_replication_slots
sur l’instance
secondaire.
Nous allons l’activer sur les deux instances puisque l’objectif est
de réaliser un switchover.
echo "sync_replication_slots = true" >> $PGDATAS /primaire/postgresql.conf
pg_ctl restart -D $PGDATAS /primaire
echo "sync_replication_slots = true" >> $PGDATAS /secondaire/postgresql.conf
pg_ctl restart -D $PGDATAS /secondaire
On peut observer qu’un processus slotsync worker
est
démarré sur la standby :
ps f -e | grep -E "(primaire|secondaire)"
316406 ? Ss 0:00 \_ /usr/pgsql-17/bin/postgres -D /home/benoit/tmp/failover/primaire
316407 ? Ss 0:00 | \_ postgres: primaire: logger
316409 ? Ss 0:00 | \_ postgres: primaire: checkpointer
316410 ? Ss 0:00 | \_ postgres: primaire: background writer
316412 ? Ss 0:00 | \_ postgres: primaire: walwriter
316413 ? Ss 0:00 | \_ postgres: primaire: autovacuum launcher
316414 ? Ss 0:00 | \_ postgres: primaire: logical replication launcher
316441 ? Ss 0:00 | \_ postgres: primaire: walsender replicator [local] streaming 0/B0063C0
316442 ? Ss 0:00 | \_ postgres: primaire: replicator postgres [local] idle
316434 ? Ss 0:00 \_ /usr/pgsql-17/bin/postgres -D /home/benoit/tmp/failover/secondaire
316435 ? Ss 0:00 \_ postgres: secondaire: logger
316436 ? Ss 0:00 \_ postgres: secondaire: checkpointer
316437 ? Ss 0:00 \_ postgres: secondaire: background writer
316438 ? Ss 0:00 \_ postgres: secondaire: startup recovering 00000001000000000000000B
316439 ? Ss 0:00 \_ postgres: secondaire: walreceiver streaming 0/B0063C0
316440 ? Ss 0:00 \_ postgres: secondaire: slotsync worker
Recréons un slot de réplication :
psql -p 5497 <<_EOF_
CREATE SUBSCRIPTION sub
CONNECTION 'port=5495 dbname=postgres'
PUBLICATION pub
WITH (failover);
_EOF_
La synchronisation des slots de réplication logique n’est pas
instantanée, le slotsync worker
se réveille à intervalle
régulier et récupère les informations concernant les slots de
réplication pour les créer ou les mettre à jour. La durée de repos du
processus est adaptée en fonction de l’activité sur la primaire.
Après une période d’attente, le slot apparaît sur la secondaire :
psql <<_EOF_
\c postgres postgres /tmp 5497
\x
\d Rs+
\x
\c postgres postgres /tmp 5495
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
\c postgres postgres /tmp 5496
SELECT slot_name, slot_type, failover, synced FROM pg_replication_slots;
_EOF_
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5497".
Expanded display is on.
List of subscriptions
-[ RECORD 1 ]------+------------------------------------------
Name | sub
Owner | postgres
Enabled | t
Publication | {pub}
Binary | f
Streaming | off
Two-phase commit | d
Disable on error | f
Origin | any
Password required | t
Run as owner? | f
Failover | t
Synchronous commit | off
Conninfo | port=5495 user=replicator dbname=postgres
Skip LSN | 0/0
Expanded display is off.
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5495".
slot_name | slot_type | failover | synced
------------+-----------+----------+--------
secondaire | physical | f | f
sub | logical | t | f
(2 rows)
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
slot_name | slot_type | failover | synced
-----------+-----------+----------+--------
sub | logical | t | t
(1 row)
Insertions de données
psql <<_EOF_
-- primaire / publication
\c postgres postgres /tmp 5495
CREATE TABLE junk(i int, t text);
INSERT INTO junk SELECT x, 'texte '||x FROM generate_series(1,10000) AS F(x);
-- souscription
\c postgres postgres /tmp 5497
CREATE TABLE junk(i int, t text);
ALTER SUBSCRIPTION sub REFRESH PUBLICATION;
SELECT pg_sleep_for(INTERVAL '2s');
SELECT count(*) FROM junk;
_EOF_
psql <<_EOF_
-- primaire / publication
\c postgres postgres /tmp 5495
SELECT slot_name, slot_type, failover, synced, restart_lsn FROM pg_replication_slots;
-- secondaire
\c postgres postgres /tmp 5496
SELECT slot_name, slot_type, failover, synced, restart_lsn FROM pg_replication_slots;
_EOF_
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5495".
slot_name | slot_type | failover | synced | restart_lsn
------------+-----------+----------+--------+-------------
secondaire | physical | f | f | 0/3129A40
sub | logical | t | f | 0/3129A08
(2 rows)
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
slot_name | slot_type | failover | synced | restart_lsn
-----------+-----------+----------+--------+-------------
sub | logical | t | t | 0/3129A08
(1 row)
Bascule controlée de la réplication (switchover)
Arrêter la primaire :
# primaire
pg_ctl stop -D $PGDATAS /primaire -m fast -w
pg_controldata -D $PGDATAS /primaire | \
grep -E "(Database cluster state|Latest checkpoint's REDO location)"
Vérifier que la standby a tout reçu et la promouvoir :
# secondaire > nouvelle primaire
psql -p 5496 -c "CHECKPOINT;"
pg_controldata -D $PGDATAS /secondaire | \
grep -E "(Database cluster state|Latest checkpoint's REDO location)"
pg_ctl promote -D $PGDATAS /secondaire
echo "cluster_name = 'nouvelle primaire'" >> $PGDATAS /secondaire/postgresql.conf
psql -p 5496 -c "SELECT pg_create_physical_replication_slot('ancienne_primaire');"
pg_ctl restart -D $PGDATAS /secondaire # pour màj les titres de processus pour la démo
psql -p 5496 -c "INSERT INTO junk SELECT x, 'texte '||x FROM generate_series(1,10000) AS F(x);"
Reconnecter l’ancienne primaire :
# ancienne primaire
touch $PGDATAS /primaire/standby.signal
cat >> $PGDATAS /primaire/postgresql.conf <<_EOF_
primary_conninfo = 'port=5496 user=replicator dbname=postgres application_name=ancienne_primaire'
primary_slot_name = 'ancienne_primaire'
hot_standby_feedback = on
cluster_name = 'ancienne primaire'
_EOF_
pg_ctl start -D $PGDATAS /primaire
Il faut ensuite mettre à jour la souscription :
psql -p 5497 -c "ALTER SUBSCRIPTION sub CONNECTION 'port=5496 dbname=postgres';"
… et supprimer les slot sur l’ancienne primaire :
psql -p 5495 -c "SELECT pg_drop_replication_slot('sub');"
psql -p 5495 -c "SELECT pg_drop_replication_slot('secondaire');"
La réplication logique a bien basculé :
ps f -e | grep -E "(primaire|secondaire|souscription)"
330608 ? Ss 0:00 \_ /usr/pgsql-17/bin/postgres -D /home/benoit/tmp/failover/souscription
330609 ? Ss 0:00 | \_ postgres: souscription: logger
330610 ? Ss 0:00 | \_ postgres: souscription: checkpointer
330611 ? Ss 0:00 | \_ postgres: souscription: background writer
330613 ? Ss 0:00 | \_ postgres: souscription: walwriter
330614 ? Ss 0:00 | \_ postgres: souscription: autovacuum launcher
330615 ? Ss 0:00 | \_ postgres: souscription: logical replication launcher
335150 ? Ss 0:00 | \_ postgres: souscription: logical replication apply worker for subscription 16384
333983 ? Ss 0:00 \_ /usr/pgsql-17/bin/postgres -D /home/benoit/tmp/failover/primaire
333984 ? Ss 0:00 | \_ postgres: ancienne primaire: logger
333985 ? Ss 0:00 | \_ postgres: ancienne primaire: checkpointer
333986 ? Ss 0:00 | \_ postgres: ancienne primaire: background writer
333987 ? Ss 0:00 | \_ postgres: ancienne primaire: startup recovering 000000020000000000000003
334439 ? Ss 0:00 | \_ postgres: ancienne primaire: walreceiver streaming 0/31E1B50
336165 ? Ss 0:00 | \_ postgres: ancienne primaire: slotsync worker
334371 ? Ss 0:00 \_ /usr/pgsql-17/bin/postgres -D /home/benoit/tmp/failover/secondaire
334372 ? Ss 0:00 \_ postgres: nouvelle primaire: logger
334373 ? Ss 0:00 \_ postgres: nouvelle primaire: checkpointer
334374 ? Ss 0:00 \_ postgres: nouvelle primaire: background writer
334376 ? Ss 0:00 \_ postgres: nouvelle primaire: walwriter
334377 ? Ss 0:00 \_ postgres: nouvelle primaire: autovacuum launcher
334378 ? Ss 0:00 \_ postgres: nouvelle primaire: logical replication launcher
334440 ? Ss 0:00 \_ postgres: nouvelle primaire: walsender replicator [local] streaming 0/31E1B50
335156 ? Ss 0:00 \_ postgres: nouvelle primaire: walsender postgres postgres [local] START_REPLICATION
336169 ? Ss 0:00 \_ postgres: nouvelle primaire: replicator postgres [local] idle
Les données ont bien été synchronisées :
psql <<_EOF_
-- nouvelle primaire / publication
\c postgres postgres /tmp 5496
INSERT INTO junk SELECT x, 'texte '||x FROM generate_series(1,10000) AS F(x);
SELECT count(*) FROM junk;
-- souscription
\c postgres postgres /tmp 5497
SELECT pg_sleep_for(INTERVAL '2s');
SELECT count(*) FROM junk;
_EOF_
_EOF_
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5496".
INSERT 0 10000
count
-------
30000
(1 row)
You are now connected to database "postgres" as user "postgres" via socket in "/tmp" at port "5497".
pg_sleep_for
--------------
(1 row)
count
-------
30000
(1 row)
Bascule non programmée (failover)
Si une bascule non programmée est réalisée et que pour une raison ou
pour une autre les deux instances divergent, il est possible que la
souscription reçoive des informations qui ne sont pas arrivées sur la
nouvelle instance primaire. Cela pourrait causer des conflits de
réplication logique par la suite et/ou provoquer des bugs au niveau
applicatif.