PostgreSQL 10 kom med den velkomne tilføjelse af den logiske replikering funktion. Dette giver en mere fleksibel og lettere måde at replikere dine tabeller på end den almindelige streaming-replikeringsmekanisme. Det har dog nogle begrænsninger, som måske eller måske ikke forhindrer dig i at bruge det til replikering. Læs videre for at lære mere.
Hvad er logisk replikering alligevel?
Streamende replikering
Før v10 var den eneste måde at replikere data på en server på at replikere ændringerne på WAL-niveau. Under driften vil en PostgreSQL-server (den primære ) genererer en sekvens af WAL-filer. Den grundlæggende idé er at få disse filer over til en anden PostgreSQL-server (standby ) som tager i disse filer og "genafspiller" dem for at genskabe de samme ændringer, der sker på den primære server. Standby-serveren forbliver i en skrivebeskyttet tilstand kaldetgendannelsestilstand , og eventuelle ændringer af standby-serveren er ikke tilladt (dvs. kun skrivebeskyttede transaktioner er tilladt).
Processen med at sende WAL-filerne fra den primære til standby kaldes logshipping , og kan udføres manuelt (scripts til at rsync ændringer fra den primære$PGDATA/pg_wal
mappe til sekundære) eller gennem streaming replikering .Forskellige funktioner såsom replikeringspladser , standby-feedback og failover blev tilføjet over tid for at forbedre pålideligheden og anvendeligheden af streamingreplikering.
En stor "funktion" ved streamingreplikering er, at det er alt eller intet. Alle ændringer til alle objekter fra alle databaser på den primære skal sendes til standby, og standby skal importere hver ændring. Det er ikke muligt at selektivt replikere en del af din database.
Logisk replikering
Logisk replikering , tilføjet i v10, gør det muligt at gøre netop det – repliker kun et sæt tabeller til andre servere. Det er bedst forklaret med et eksempel. Lad os tage en database kaldet src
i en server, og opret en tabel 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 vil også oprette en publikation i denne database (bemærk, at du skal have superbrugerrettigheder for at gøre dette):
src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION
Lad os nu gå til en database dst
på en anden server og opret en lignende tabel:
dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE
Og vi opretter nu et abonnement her, der vil forbinde til publikationen på kilden og begynde at trække ændringerne ind. (Bemærk, at du skal have en brugerrepuser
på kildeserveren med replikeringsrettigheder og læseadgang til tabellerne.)
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
Ændringerne synkroniseres, og du kan se rækkerne på destinationssiden:
dst=# SELECT * FROM t;
col1 | col2 | col3
------+------+------
1 | 10 | foo
2 | 20 | foo
3 | 30 | foo
(3 rows)
Destinationstabellen har en ekstra kolonne "col3", som ikke berøres af dens applikation. Ændringerne replikeres "logisk" - så så længe det er muligt at indsætte en række med t.col1 og t.col2 alene, vil replikeringsprocessen gøre det.
Sammenlignet med streamingreplikering er den logiske replikeringsfunktion perfekt til at replikere f.eks. et enkelt skema eller et sæt tabeller i en specifik database til en anden server.
Replikering af skemaændringer
Antag, at du har en Django-applikation med dets sæt tabeller i kildedatabasen. Det er nemt og effektivt at opsætte logisk replikering for at overføre alle disse tabeller til en anden server, hvor du kan køre rapportering, analyse, batchjobs, udvikler-/kundesupport-apps og lignende uden at røre ved de "rigtige" data og uden at påvirke produktionsappen.
Muligvis den største begrænsning ved logisk replikering i øjeblikket er, at den ikke replikerer skemaændringer - enhver DDL-kommando, der udføres i kildedatabasen, forårsager ikke en lignende ændring i destinationsdatabasen, i modsætning til i streamingreplikering. For eksempel, hvis vi gør dette i kildedatabasen:
src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1
dette bliver logget i destinationslogfilen:
ERROR: logical replication target relation "public.t" is missing some replicated columns
og replikationen stopper. Kolonnen skal tilføjes "manuelt" ved destinationen, hvorefter replikeringen genoptages:
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)
Dette betyder, at hvis din Django-applikation har tilføjet en ny funktion, der kræver nye kolonner eller tabeller, og du skal køre django-admin migrate
på kildedatabasen går replikeringsopsætningen i stykker.
Løsning
Dit bedste bud på at løse dette problem ville være at sætte abonnementet på pause på destinationen, migrere destinationen først, derefter kilden og derefter genoptage abonnementet. Du kan pause og genoptage abonnementer som dette:
-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;
-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;
Hvis nye tabeller tilføjes, og din publikation ikke er "FOR ALLE TABELLER", skal du tilføje dem til publikationen manuelt:
ALTER PUBLICATION mypub ADD TABLE newly_added_table;
Du skal også "opdatere" abonnementet på destinationssiden for at fortælle Postgres at begynde at synkronisere de nye tabeller:
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION
Sekvenser
Overvej denne tabel ved kilden 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
blev oprettet for at bakke a
kolonne af serial
type. Dette genererer de autoinkrementerende værdier for s.a
. Lad os nu replikere dette til dst
, og indsæt endnu en række:
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)
Ups, hvad skete der lige? Destinationen forsøgte at starte sekvensen fra bunden og genererede en værdi på 1 for a
. Dette skyldes, at logisk replikering ikke replikerer værdierne for sekvenser, da den næste værdi af disse sekvenser ikke er gemt i selve tabellen.
Løsning
Hvis du tænker logisk over det, kan du ikke ændre den samme "autoincrement"-værdi fra to steder uden tovejssynkronisering. Hvis du virkelig har brug for et stigende antal i hver række i en tabel og har brug for at indsætte i den tabel fra flere servere, kan du:
- brug en ekstern kilde til nummeret, såsom ZooKeeper eller etcd,
- brug ikke-overlappende intervaller – for eksempel genererer og indsætter den første server tal i intervallet 1 til 1 million, den anden i intervallet 1 million til 2 millioner, og så videre.
Tabeller uden unikke rækker
Lad os prøve at oprette en tabel uden en primær nøgle og replikere 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
Og rækkerne er nu også på destinationen:
dst=# SELECT * FROM nopk;
foo
----------
new york
boston
(2 rows)
Lad os nu prøve at slette den anden række ved kilden:
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.
Dette sker, fordi destinationen ikke entydigt vil kunne identificere den række, der skal slettes (eller opdateres) uden en primær nøgle.
Løsning
Du kan selvfølgelig ændre skemaet til at inkludere en primær nøgle. Hvis du ikke ønsker at gøre det, skal du ALTER TABLE
og indstil "replika-identifikation" til hele rækken eller et unikt indeks. For eksempel:
src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1
Sletningen lykkes nu, og replikeringen også:
dst=# SELECT * FROM nopk;
foo
----------
new york
(1 row)
Hvis din tabel virkelig ikke har nogen mulighed for entydigt at identificere rækker, så er du lidt fast. Se afsnittet REPLICA IDENTITY i ALTERTABLE for mere information.
Differently partitioned-destinations
Ville det ikke være rart at have en kilde, der er opdelt på én måde og destination på en anden måde? For eksempel kan vi ved kilden holde paritioner for hver måned og ved destinationen for hvert år. Formodentlig er destinationen en større maskine, og vi skal beholde historiske data, men har sjældent brug for disse data.
Lad os oprette en månedlig opdelt tabel ved kilden:
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
Og prøv at oprette en årligt opdelt tabel 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 klager over, at den har brug for partitionstabellen til januar 2019, som vi ikke har til hensigt at oprette på destinationen.
Dette sker, fordi logisk replikering ikke fungerer på basistabelniveau, men på undertabelniveau. Der er ingen reel løsning på dette – hvis du genbruger partitioner, skal partitionshierarkiet være det samme på begge sider af den alogiske replikeringsopsætning.
Store objekter
Store objekter kan ikke replikeres ved hjælp af logisk replikering. Dette er sandsynligvis ikke en stor sag i dag, da opbevaring af store genstande ikke er en almindelig moderne praksis. Det er også nemmere at gemme en reference til et stort objekt på et eksternt, redudant lager (såsom NFS, S3 osv.) og replikere denne reference i stedet for at gemme og kopiere selve objektet.