PostgreSQL 10 kom med det välkomnande tillägget av den logiska replikeringen funktion. Detta ger ett mer flexibelt och enklare sätt att replikera dina tabeller än den vanliga strömmande replikeringsmekanismen. Det har dock vissa begränsningar som kan eller kanske inte hindrar dig från att använda det för replikering. Läs vidare för att lära dig mer.
Vad är logisk replikering egentligen?
Streamande replikering
Före v10 var det enda sättet att replikera data som finns på en server att replikera ändringarna på WAL-nivå. Under driften, en PostgreSQL-server (den primära ) genererar en sekvens av WAL-filer. Grundidén är att överföra dessa filer till en annan PostgreSQL-server (standby ) som tar in dessa filer och "spelar upp" dem för att återskapa samma ändringar som sker på den primära servern. Standbyservern förblir i ett skrivskyddat läge som kallasåterställningsläget , och eventuella ändringar av standbyservern är inte tillåtet (det vill säga endast skrivskyddade transaktioner är tillåtna).
Processen att skicka WAL-filerna från den primära till standby-läge kallas logshipping , och kan göras manuellt (skript för att rsynka ändringar från primära$PGDATA/pg_wal
katalog till sekundära) eller genom strömmande replikering . Olika funktioner som replikeringsplatser , beredskapsfeedback och failover lades till med tiden för att förbättra tillförlitligheten och användbarheten av streamingreplikering.
En stor "funktion" med strömmande replikering är att det är allt eller inget. Alla ändringar av alla objekt från alla databaser på den primära måste skickas till standby, och standby måste importera varje ändring. Det är inte möjligt att selektivt replikera en del av din databas.
Logisk replikering
Logisk replikering , lagt till i v10, gör det möjligt att göra just det – replikera endast en uppsättning tabeller till andra servrar. Det förklaras bäst med ett exempel. Låt oss ta en databas som heter src
i en server och skapa en tabell i den:
src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3
Vi kommer också att skapa en publikation i den här databasen (observera att du måste ha superanvändarbehörighet för att göra detta):
src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION
Låt oss nu gå till en databas dst
på en annan server och skapa en liknande tabell:
dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE
Och vi skapar nu en prenumeration här som kommer att ansluta till publikationen på källan och börja dra in ändringarna. (Observera att du måste ha en användarrepuser
på källservern med replikeringsbehörighet och läsåtkomst till tabellerna.)
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
Ändringarna synkroniseras och du kan se raderna på destinationssidan:
dst=# SELECT * FROM t;
col1 | col2 | col3
------+------+------
1 | 10 | foo
2 | 20 | foo
3 | 30 | foo
(3 rows)
Destinationstabellen har en extra kolumn "col3", som inte berörs av applikationen. Ändringarna replikeras "logiskt" - så så länge det är möjligt att infoga en rad med enbart t.col1 och t.col2 kommer replikeringsprocessen att fungera.
Jämfört med strömmande replikering är den logiska replikeringsfunktionen perfekt för att replikera till exempel ett enstaka schema eller en uppsättning tabeller i en specifik databas till en annan server.
Replikering av schemaändringar
Anta att du har en Django-applikation med dess uppsättning tabeller som finns i källdatabasen. Det är enkelt och effektivt att ställa in logisk replikering för att överföra alla dessa tabeller till en annan server, där du kan köra rapportering, analys, batchjobb, utvecklare/kundsupportappar och liknande utan att röra den "riktiga" data och utan att påverka produktionsappen.
Den kanske största begränsningen för logisk replikering för närvarande är att den inte replikerar schemaändringar - något DDL-kommando som körs i källdatabasen orsakar inte en liknande förändring i måldatabasen, till skillnad från i streamingreplikering. Till exempel, om vi gör detta i källdatabasen:
src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1
detta loggas i målloggfilen:
ERROR: logical replication target relation "public.t" is missing some replicated columns
och replikeringen stoppar. Kolumnen måste läggas till "manuellt" vid destinationen, varvid replikeringen återupptas:
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)
Det betyder att om din Django-applikation har lagt till en ny funktion som behöver nya kolumner eller tabeller, och du måste köra django-admin migrate
i källdatabasen avbryts replikeringsinställningarna.
Lösning
Det bästa alternativet för att åtgärda det här problemet är att pausa prenumerationen på destinationen, migrera destinationen först, sedan källan och sedan återuppta prenumerationen. Du kan pausa och återuppta prenumerationer så här:
-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;
-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;
Om nya tabeller läggs till och din publikation inte är "FÖR ALLA TABELL", måste du lägga till dem i publikationen manuellt:
ALTER PUBLICATION mypub ADD TABLE newly_added_table;
Du måste också "uppdatera" prenumerationen på destinationssidan för att berätta för Postgres att börja synkronisera de nya tabellerna:
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION
Sekvenser
Betrakta den här tabellen vid källan, med en sekvens:
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)
Sekvensen s_a_seq
skapades för att backa upp a
kolumn, av serial
typ. Detta genererar de autoinkrementerande värdena för s.a
. Låt oss nu replikera detta till dst
, och infoga en annan rad:
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)
Oj, vad hände just? Destinationen försökte starta sekvensen från början och genererade ett värde på 1 för a
. Detta beror på att logisk replikering inte replikerar värdena för sekvenser eftersom nästa värde för dessa sekvenser inte lagras i själva tabellen.
Lösning
Om du tänker på det logiskt kan du inte ändra samma "autoinkrement"-värde från två ställen utan dubbelriktad synkronisering. Om du verkligen behöver ett ökande nummer i varje rad i en tabell och behöver infoga i den tabellen från flera servrar, kan du:
- använd en extern källa för numret, som ZooKeeper eller etcd,
- använd icke-överlappande intervall – till exempel genererar och infogar den första servern nummer i intervallet 1 till 1 miljon, den andra i intervallet 1 miljon till 2 miljoner, och så vidare.
Tabell utan unika rader
Låt oss försöka skapa en tabell utan en primärnyckel och replikera den:
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
Och raderna är nu också på destinationen:
dst=# SELECT * FROM nopk;
foo
----------
new york
boston
(2 rows)
Låt oss nu försöka ta bort den andra raden vid källan:
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.
Detta beror på att destinationen inte unikt kommer att kunna identifiera raden som behöver raderas (eller uppdateras) utan en primärnyckel.
Lösning
Du kan naturligtvis ändra schemat så att det inkluderar en primärnyckel. Om du inte vill göra det, ALTER TABLE
och ställ in "replikidentifieringen" till hela raden eller ett unikt index. Till exempel:
src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1
Borttagningen lyckas nu, och replikeringen också:
dst=# SELECT * FROM nopk;
foo
----------
new york
(1 row)
Om din tabell verkligen inte har något sätt att unikt identifiera rader, då är du lite fast. Se avsnittet REPLICA IDENTITY i ALTERTABLE för mer information.
Annorlunda partitionerade destinationer
Skulle det inte vara trevligt att ha en källa som är uppdelad på ett sätt och destination på ett annat sätt? Till exempel, vid källan kan vi hålla pariteter för varje månad och vid destinationen för varje år. Förmodligen är destinationen en större maskin, och vi behöver behålla historiska data, men de behöver sällan.
Låt oss skapa en månatlig partitionerad tabell vid källan:
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
Och försök skapa en årlig uppdelad tabell på destinationen:
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 klagar över att den behöver partitionstabellen för januari 2019, som vi inte har för avsikt att skapa på destinationen.
Detta händer eftersom logisk replikering inte fungerar på bastabellnivå, utan på undertabellnivå. Det finns ingen verklig lösning för detta – om du återanvänder partitioner måste partitionshierarkin vara densamma på båda sidor av den logiska replikeringsinställningen.
Stora objekt
Stora objekt kan inte replikeras med logisk replikering. Detta är förmodligen inte en stor sak nuförtiden, eftersom att lagra stora föremål inte är en vanlig modern praxis. Det är också lättare att lagra en referens till ett stort objekt på någon extern, redudant lagring (som NFS, S3 etc.) och replikera den referensen snarare än att lagra och replikera själva objektet.