PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Problèmes de réplication logique PostgreSQL

PostgreSQL 10 est venu avec l'ajout bienvenu de la réplication logique fonctionnalité. Cela fournit un moyen plus flexible et plus facile de répliquer vos tables que le mécanisme de réplication en continu classique. Cependant, il présente certaines limitations qui peuvent ou non vous empêcher de l'utiliser pour la réplication. Lisez la suite pour en savoir plus.

Qu'est-ce que la réplication logique ?

Réplication en continu

Avant la v10, la seule façon de répliquer les données résidant sur un serveur était de répliquer les modifications au niveau WAL. Pendant son fonctionnement, un serveur PostgreSQL (le serveur principal ) génère une séquence de fichiers WAL. L'idée de base est de transférer ces fichiers sur un autre serveur PostgreSQL (le serveur de secours ) qui prend ces fichiers et les « rejoue » pour recréer les mêmes modifications qui se produisent sur le serveur principal. Le serveur de secours reste en mode lecture seule appelé mode de récupération , et toutes les modifications apportées au serveur de secours ne sont pas autorisé (c'est-à-dire que seules les transactions en lecture seule sont autorisées).

Le processus d'envoi des fichiers WAL du primaire au standby s'appelle logshipping , et peut être fait manuellement (scripts pour rsynchroniser les changements de $PGDATA/pg_wal du primaire répertoire secondaire) ou via la réplication en continu .Diverses fonctionnalités telles que les emplacements de réplication , commentaires en attente et basculement ont été ajoutés au fil du temps pour améliorer la fiabilité et l'utilité de la réplication en continu.

Une grande "fonctionnalité" de la réplication en continu est que c'est tout ou rien. Toutes les modifications apportées à tous les objets de toutes les bases de données sur le primaire doivent être envoyées au standby, et le standby doit importer chaque changement. Il n'est pas possible de répliquer sélectivement une partie de votre base de données.

Réplication Logique

Réplication logique , ajouté dans la v10, permet de faire exactement cela - ne répliquer qu'un ensemble de tables sur d'autres serveurs. Il est mieux expliqué avec un exemple. Prenons une base de données appelée src dans un serveur, et créez-y une table :

src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3

Nous allons également créer une publication dans cette base de données (notez que vous devez disposer des privilèges de superutilisateur pour le faire) :

src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION

Passons maintenant à une base de données dst sur un autre serveur et créez une table similaire :

dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE

Et nous configurons maintenant un abonnement ici qui se connectera à la publication sur la source et commencera à intégrer les modifications. (Notez que vous devez avoir un utilisateurrepuser sur le serveur source avec des privilèges de réplication et un accès en lecture aux tables.)

dst=# CREATE SUBSCRIPTION mysub CONNECTION 'user=repuser password=reppass host=127.0.0.1 port=5432 dbname=src' PUBLICATION mypub;
NOTICE:  created replication slot "mysub" on publisher
CREATE SUBSCRIPTION

Les modifications sont synchronisées et vous pouvez voir les lignes côté destination :

dst=# SELECT * FROM t;
 col1 | col2 | col3
------+------+------
    1 |   10 | foo
    2 |   20 | foo
    3 |   30 | foo
(3 rows)

La table de destination a une colonne supplémentaire "col3", qui n'est pas touchée par la réplication. Les modifications sont répliquées "logiquement" - donc, tant qu'il est possible d'insérer une ligne avec t.col1 et t.col2 seuls, le processus de réplication continuera.

Par rapport à la réplication en continu, la fonctionnalité de réplication logique est parfaite pour répliquer, par exemple, un seul schéma ou un ensemble de tables dans une base de données spécifique vers un autre serveur.

Réplication des modifications de schéma

Supposons que vous ayez une application Django avec son ensemble de tables vivant dans la base de données source. Il est facile et efficace de configurer la réplication logique pour transférer toutes ces tables sur un autre serveur, où vous pouvez exécuter des rapports, des analyses, des travaux par lots, des applications de support développeur/client, etc., sans toucher aux données "réelles" et sans affecter l'application de production.

La plus grande limitation de la réplication logique actuellement est peut-être qu'elle ne réplique pas les modifications de schéma - toute commande DDL exécutée sur la base de données source ne provoque pas de modification similaire dans la base de données de destination, contrairement à la réplication en continu. Par exemple, si nous faisons cela au niveau de la base de données source :

src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1

ceci est enregistré dans le fichier journal de destination :

ERROR:  logical replication target relation "public.t" is missing some replicated columns

et la réplication s'arrête. La colonne doit être ajoutée "manuellement" à la destination, à quel point la réplication reprend :

dst=# SELECT * FROM t;
 col1 | col2 | col3
------+------+------
    1 |   10 | foo
    2 |   20 | foo
    3 |   30 | foo
(3 rows)

dst=# ALTER TABLE t ADD newcol int;
ALTER TABLE
dst=# SELECT * FROM t;
 col1 | col2 | col3 | newcol
------+------+------+--------
    1 |   10 | foo  |
    2 |   20 | foo  |
    3 |   30 | foo  |
   -1 |  -10 | foo  |   -100
(4 rows)

Cela signifie que si votre application Django a ajouté une nouvelle fonctionnalité nécessitant de nouvelles colonnes ou tables, et que vous devez exécuter django-admin migrate sur la base de données source, la configuration de la réplication s'interrompt.

Solution de contournement

Votre meilleur pari pour résoudre ce problème serait de suspendre l'abonnement sur la destination, de migrer d'abord la destination, puis la source, puis de reprendre l'abonnement. Vous pouvez mettre en pause et reprendre des abonnements comme ceci :

-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;

-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;

Si de nouvelles tables sont ajoutées et que votre publication n'est pas "POUR TOUTES LES TABLES", vous devrez les ajouter manuellement à la publication :

ALTER PUBLICATION mypub ADD TABLE newly_added_table;

Vous devrez également "actualiser" l'abonnement côté destination pour dire à Postgres de commencer à synchroniser les nouvelles tables :

dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION

Séquences

Considérez ce tableau à la source, ayant une séquence :

src=# CREATE TABLE s (a serial PRIMARY KEY, b text);
CREATE TABLE
src=# INSERT INTO s (b) VALUES ('foo'), ('bar'), ('baz');
INSERT 0 3
src=# SELECT * FROM s;
 a |  b
---+-----
 1 | foo
 2 | bar
 3 | baz
(3 rows)

src=# SELECT currval('s_a_seq'), nextval('s_a_seq');
 currval | nextval
---------+---------
       3 |       4
(1 row)

La séquence s_a_seq a été créé pour sauvegarder le a colonne, de serial type. Cela génère les valeurs d'auto-incrémentation pour s.a . Maintenant, répliquons ceci dans dst , et insérez une autre ligne :

dst=# SELECT * FROM s;
 a |  b
---+-----
 1 | foo
 2 | bar
 3 | baz
(3 rows)

dst=# INSERT INTO s (b) VALUES ('foobaz');
ERROR:  duplicate key value violates unique constraint "s_pkey"
DETAIL:  Key (a)=(1) already exists.
dst=#  SELECT currval('s_a_seq'), nextval('s_a_seq');
 currval | nextval
---------+---------
       1 |       2
(1 row)

Oups, que vient-il de se passer ? La destination a essayé de démarrer la séquence à partir de zéro et a généré une valeur de 1 pour a . En effet, la réplication logique ne réplique pas les valeurs des séquences puisque la valeur suivante de la séquence n'est pas stockée dans la table elle-même.

Solution

Si vous y réfléchissez logiquement, vous ne pouvez pas modifier la même valeur "d'auto-incrémentation" à partir de deux endroits sans synchronisation bidirectionnelle. Si vous avez vraiment besoin d'un nombre incrémenté dans chaque ligne d'un tableau et que vous devez insérer dans ce tableau à partir de plusieurs serveurs, vous pouvez :

  • utiliser une source externe pour le numéro, comme ZooKeeper ou etcd,
  • utilisez des plages qui ne se chevauchent pas :par exemple, le premier serveur génère et insère des nombres compris entre 1 et 1 million, le second entre 1 million et 2 millions, et ainsi de suite.

Tableaux sans lignes uniques

Essayons de créer une table sans clé primaire et de la répliquer :

src=# CREATE TABLE nopk (foo text);
CREATE TABLE
src=# INSERT INTO nopk VALUES ('new york');
INSERT 0 1
src=# INSERT INTO nopk VALUES ('boston');
INSERT 0 1

Et les lignes sont maintenant également sur la destination :

dst=# SELECT * FROM nopk;
   foo
----------
 new york
 boston
(2 rows)

Essayons maintenant de supprimer la deuxième ligne à la source :

src=# DELETE FROM nopk WHERE foo='boston';
ERROR:  cannot delete from table "nopk" because it does not have a replica identity and publishes deletes
HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.

Cela se produit car la destination ne pourra pas identifier de manière unique la ligne qui doit être supprimée (ou mise à jour) sans clé primaire.

Solution

Vous pouvez bien sûr modifier le schéma pour inclure une clé primaire. Si vous ne voulez pas le faire, vous ALTER TABLE et définissez "l'identifiant de réplique" sur la ligne complète ou un index unique. Par exemple :

src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1

La suppression réussit maintenant, et la réplication aussi :

dst=# SELECT * FROM nopk;
   foo
----------
 new york
(1 row)

Si votre table n'a vraiment aucun moyen d'identifier de manière unique les lignes, alors vous êtes un peu coincé. Consultez la section IDENTITÉ DU RÉPLIQUE de ALTERTABLE pour plus d'informations.

Destinations partitionnées différemment

Ne serait-il pas agréable d'avoir une source partitionnée d'une manière et une destination d'une manière différente ? Par exemple, à la source on peut garder les partitions pour chaque mois, et à la destination pour chaque année. Vraisemblablement, la destination est une machine plus grosse, et nous devons conserver des données historiques, mais nous avons rarement besoin de ces données.

Créons une table partitionnée mensuellement à la source :

src=# CREATE TABLE measurement (
src(#     logdate         date not null,
src(#     peaktemp        int
src(# ) PARTITION BY RANGE (logdate);
CREATE TABLE
src=#
src=# CREATE TABLE measurement_y2019m01 PARTITION OF measurement
src-# FOR VALUES FROM ('2019-01-01') TO ('2019-02-01');
CREATE TABLE
src=#
src=# CREATE TABLE measurement_y2019m02 PARTITION OF measurement
src-# FOR VALUES FROM ('2019-02-01') TO ('2019-03-01');
CREATE TABLE
src=#
src=# GRANT SELECT ON measurement, measurement_y2019m01, measurement_y2019m02 TO repuser;
GRANT

Et essayez de créer une table partitionnée annuellement à la destination :

dst=# CREATE TABLE measurement (
dst(#     logdate         date not null,
dst(#     peaktemp        int
dst(# ) PARTITION BY RANGE (logdate);
CREATE TABLE
dst=#
dst=# CREATE TABLE measurement_y2018 PARTITION OF measurement
dst-# FOR VALUES FROM ('2018-01-01') TO ('2019-01-01');
CREATE TABLE
dst=#
dst=# CREATE TABLE measurement_y2019 PARTITION OF measurement
dst-# FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');
CREATE TABLE
dst=#
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ERROR:  relation "public.measurement_y2019m01" does not exist
dst=#

Postgres se plaint d'avoir besoin de la table de partition pour janvier 2019, que nous n'avons pas l'intention de créer sur la destination.

Cela se produit parce que la réplication logique ne fonctionne pas au niveau de la table de base, mais au niveau de la table enfant. Il n'y a pas vraiment de solution de contournement pour cela - si vous réutilisez des partitions, la hiérarchie des partitions doit être la même des deux côtés d'une configuration de réplication logique.

Gros Objets

Les objets volumineux ne peuvent pas être répliqués à l'aide de la réplication logique. Ce n'est probablement pas un gros problème de nos jours, car le stockage de gros objets n'est pas une pratique courante de nos jours. Il est également plus facile de stocker une référence à un objet volumineux sur un stockage externe redondant (comme NFS, S3, etc.) et de répliquer cette référence plutôt que de stocker et de répliquer l'objet lui-même.