PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Fallstricke bei der logischen Replikation von PostgreSQL

PostgreSQL 10 kam mit der willkommenen Ergänzung der logischen Replikation Merkmal. Dies bietet eine flexiblere und einfachere Methode zum Replizieren Ihrer Tabellen als der reguläre Streaming-Replikationsmechanismus. Es hat jedoch einige Einschränkungen, die Sie möglicherweise daran hindern, es für die Replikation zu verwenden. Lesen Sie weiter, um mehr zu erfahren.

Was ist überhaupt eine logische Replikation?

Streaming-Replikation

Vor v10 bestand die einzige Möglichkeit zum Replizieren von Daten auf einem Server darin, die Änderungen auf WAL-Ebene zu replizieren. Während des Betriebs wird ein PostgreSQL-Server (der primäre ) generiert eine Folge von WAL-Dateien. Die Grundidee besteht darin, diese Dateien auf einen anderen PostgreSQL-Server (den Standby-Server) zu übertragen ), das diese Dateien aufnimmt und „wiedergibt“, um die gleichen Änderungen wiederherzustellen, die auf dem primären Server stattfinden. Der Standby-Server bleibt in einem schreibgeschützten Modus, der als Wiederherstellungsmodus bezeichnet wird , und alle Änderungen am Standby-Server sind nicht zulässig (d. h. nur lesende Transaktionen sind zulässig).

Der Vorgang des Versendens der WAL-Dateien vom primären zum Standby-Server wird als Logshipping bezeichnet , und kann manuell ausgeführt werden (Skripte zum rsync von Änderungen aus $PGDATA/pg_wal des primären Verzeichnisses). Verzeichnis zu sekundären) oder durch Streaming-Replikation .Verschiedene Funktionen wie Replikationsslots , Standby-Feedback und Failover wurden im Laufe der Zeit hinzugefügt, um die Zuverlässigkeit und Nützlichkeit der Streaming-Replikation zu verbessern.

Ein großes „Merkmal“ der Streaming-Replikation ist, dass es um alles oder nichts geht. Alle Änderungen an allen Objekten aus allen Datenbanken auf der Primärdatenbank müssen an die Standby-Datenbank gesendet werden, und die Standby-Datenbank muss jede Änderung importieren. Es ist nicht möglich, einen Teil Ihrer Datenbank selektiv zu replizieren.

Logische Replikation

Logische Replikation , das in v10 hinzugefügt wurde, macht es möglich, genau das zu tun – nur eine Reihe von Tabellen auf andere Server zu replizieren. Es wird am besten mit einem Beispiel erklärt. Nehmen wir eine Datenbank namens src in einem Server und erstellen Sie darin eine Tabelle:

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

Wir werden auch eine Publikation erstellen in dieser Datenbank (beachten Sie, dass Sie dazu Superuser-Rechte benötigen):

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

Gehen wir nun zu einer Datenbank dst auf einem anderen Server und erstellen Sie eine ähnliche Tabelle:

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

Und wir richten jetzt ein Abonnement ein Hier wird eine Verbindung zur Veröffentlichung in der Quelle hergestellt und die Änderungen übernommen. (Beachten Sie, dass Sie einen Benutzerrepuser benötigen auf dem Quellserver mit Replikationsrechten und Lesezugriff auf die Tabellen.)

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

Die Änderungen werden synchronisiert und Sie können die Zeilen auf der Zielseite sehen:

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

Die Zieltabelle hat eine zusätzliche Spalte „col3“, die von der Replikation nicht berührt wird. Die Änderungen werden „logisch“ repliziert – solange es also möglich ist, eine Zeile nur mit t.col1 und t.col2 einzufügen, wird der Replikationsprozess dies tun.

Im Vergleich zur Streaming-Replikation eignet sich die logische Replikationsfunktion perfekt zum Replizieren beispielsweise eines einzelnen Schemas oder eines Satzes von Tabellen in einer bestimmten Datenbank auf einen anderen Server.

Replikation von Schemaänderungen

Angenommen, Sie haben eine Django-Anwendung mit ihrem Tabellensatz in der Quelldatenbank. Es ist einfach und effizient, eine logische Replikation einzurichten, um all diese Tabellen auf einen anderen Server zu bringen, wo Sie Berichte, Analysen, Batch-Jobs, Entwickler-/Kunden-Support-Apps und dergleichen ausführen können, ohne die „echten“ Daten zu berühren und ohne die Produktions-App zu beeinträchtigen.

Die möglicherweise größte Einschränkung der logischen Replikation besteht derzeit darin, dass Schemaänderungen nicht repliziert werden – anders als bei der Streamingreplikation verursacht kein DDL-Befehl, der in der Quelldatenbank ausgeführt wird, eine ähnliche Änderung in der Zieldatenbank. Wenn wir dies zum Beispiel in der Quelldatenbank tun:

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

dies wird in der Zielprotokolldatei protokolliert:

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

und die Replikation stoppt. Die Spalte muss am Ziel „manuell“ hinzugefügt werden, an diesem Punkt wird die Replikation fortgesetzt:

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)

Das bedeutet, wenn Ihre Django-Anwendung eine neue Funktion hinzugefügt hat, die neue Spalten oder Tabellen benötigt, müssen Sie django-admin migrate ausführen Auf der Quelldatenbank bricht die Replikationseinrichtung zusammen.

Problemumgehung

Sie können dieses Problem am besten beheben, indem Sie das Abonnement auf dem Ziel anhalten, zuerst das Ziel und dann die Quelle migrieren und dann das Abonnement fortsetzen. Sie können Abonnements wie folgt pausieren und fortsetzen:

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

-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;

Wenn neue Tabellen hinzugefügt werden und Ihre Veröffentlichung nicht „FOR ALL TABLES“ ist, müssen Sie sie manuell zur Veröffentlichung hinzufügen:

ALTER PUBLICATION mypub ADD TABLE newly_added_table;

Außerdem müssen Sie das Abonnement auf der Zielseite „aktualisieren“, um Postgres anzuweisen, mit der Synchronisierung der neuen Tabellen zu beginnen:

dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION

Sequenzen

Betrachten Sie diese Tabelle an der Quelle mit einer Sequenz:

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)

Die Sequenz s_a_seq wurde erstellt, um den a zu unterstützen Spalte von serial type. Dadurch werden die automatisch inkrementierenden Werte für s.a generiert . Lassen Sie uns dies nun in dst replizieren , und fügen Sie eine weitere Zeile ein:

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)

Hoppla, was ist gerade passiert? Das Ziel hat versucht, die Sequenz von Grund auf neu zu starten und einen Wert von 1 für a generiert . Dies liegt daran, dass die logische Replikation die Werte für Sequenzen nicht repliziert, da der nächste Wert dieser Sequenz nicht in der Tabelle selbst gespeichert wird.

Problemumgehung

Wenn Sie logisch darüber nachdenken, können Sie ohne bidirektionale Synchronisation nicht denselben „Autoinkrement“-Wert an zwei Stellen ändern. Wenn Sie wirklich eine fortlaufende Zahl in jeder Zeile einer Tabelle benötigen und von mehreren Servern in diese Tabelle einfügen müssen, könnten Sie:

  • Verwenden Sie eine externe Quelle für die Nummer, wie ZooKeeper oder etcd,
  • Verwenden Sie nicht überlappende Bereiche – zum Beispiel generiert der erste Server Zahlen im Bereich von 1 bis 1 Million und fügt sie ein, der zweite im Bereich von 1 Million bis 2 Millionen usw.

Tabellen ohne eindeutige Zeilen

Versuchen wir, eine Tabelle ohne Primärschlüssel zu erstellen und zu replizieren:

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

Und die Zeilen sind jetzt auch am Ziel:

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

Versuchen wir nun, die zweite Zeile an der Quelle zu löschen:

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.

Dies liegt daran, dass das Ziel die zu löschende (oder zu aktualisierende) Zeile ohne Primärschlüssel nicht eindeutig identifizieren kann.

Problemumgehung

Sie können das Schema natürlich so ändern, dass es einen Primärschlüssel enthält. Falls Sie das nicht möchten, ALTER TABLE und setzen Sie die „Replikatidentifikation“ auf die vollständige Zeile oder einen eindeutigen Index. Zum Beispiel:

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

Das Löschen gelingt jetzt und die Replikation auch:

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

Wenn Ihre Tabelle wirklich keine Möglichkeit hat, Zeilen eindeutig zu identifizieren, dann sind Sie ein Bitstuck. Weitere Informationen finden Sie im Abschnitt REPLICA IDENTITY von ALTERTABLE.

Unterschiedlich partitionierte Ziele

Wäre es nicht schön, eine Quelle zu haben, die auf eine Weise und ein Ziel auf eine andere Weise partitioniert ist? Zum Beispiel können wir an der Quelle für jeden Monat und am Ziel für jedes Jahr Aufteilungen beibehalten. Vermutlich ist das Ziel eine größere Maschine, und wir müssen Verlaufsdaten speichern, benötigen diese Daten aber nur selten.

Lassen Sie uns eine monatlich partitionierte Tabelle an der Quelle erstellen:

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

Und versuchen Sie, am Zielort eine jährlich partitionierte Tabelle zu erstellen:

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 beschwert sich, dass es die Partitionstabelle für Januar 2019 benötigt, die wir nicht auf dem Ziel erstellen möchten.

Dies liegt daran, dass die logische Replikation nicht auf Basistabellenebene, sondern auf untergeordneter Tabellenebene funktioniert. Dafür gibt es keine wirkliche Problemumgehung – wenn Sie Partitionen wiederverwenden, muss die Partitionshierarchie auf beiden Seiten der Einrichtung einer logischen Replikation gleich sein.

Große Objekte

Große Objekte können nicht mit der logischen Replikation repliziert werden. Dies ist heutzutage wahrscheinlich keine große Sache, da die Aufbewahrung großer Objekte keine gängige moderne Praxis ist. Es ist auch einfacher, einen Verweis auf ein großes Objekt auf einem externen, redundanten Speicher (wie NFS, S3 usw.) zu speichern und diesen Verweis zu replizieren, anstatt das Objekt selbst zu speichern und zu replizieren.