PostgreSQL 10 został wprowadzony z mile widzianym dodatkiem replikacji logicznej funkcja. Zapewnia to bardziej elastyczny i łatwiejszy sposób replikacji tabel niż zwykły mechanizm replikacji strumieniowej. Jednak ma pewne ograniczenia, które mogą, ale nie muszą, uniemożliwiać wykorzystanie go do replikacji. Czytaj dalej, aby dowiedzieć się więcej.
Czym w ogóle jest replikacja logiczna?
Replikacja strumieniowa
Przed wersją 10 jedynym sposobem replikacji danych znajdujących się na serwerze była replikacja zmian na poziomie WAL. Podczas działania serwer PostgreSQL (podstawowy ) generuje sekwencję plików WAL. Podstawowym pomysłem jest przeniesienie tych plików na inny serwer PostgreSQL (stan gotowości ), który pobiera te pliki i „odtwarza” je, aby odtworzyć te same zmiany zachodzące na serwerze podstawowym. Serwer rezerwowy pozostaje w trybie tylko do odczytu, zwanym trybem odzyskiwania , a wszelkie zmiany na serwerze rezerwowym nie dozwolone (oznacza to, że dozwolone są tylko transakcje tylko do odczytu).
Proces przesyłania plików WAL z systemu podstawowego do rezerwowego nazywa się wysyłką dziennika i można to zrobić ręcznie (skrypty do rsync zmiany z $PGDATA/pg_wal
podstawowego do katalogu wtórnego) lub przez replikację strumieniową .Różne funkcje, takie jak miejsca replikacji , opinia o gotowości i awaryjne zostały dodane z czasem, aby poprawić niezawodność i użyteczność replikacji strumieniowej.
Jedną z wielkich „cech” replikacji strumieniowej jest to, że to wszystko albo nic. Wszystkie zmiany we wszystkich obiektach ze wszystkich baz danych w systemie podstawowym muszą zostać wysłane do rezerwy, a każda zmiana musi zostać zaimportowana do rezerwy. Nie ma możliwości selektywnej replikacji części bazy danych.
Replikacja logiczna
Replikacja logiczna , dodany w v10, umożliwia właśnie to – replikację tylko zestawu tabel na inne serwery. Najlepiej wyjaśnić to na przykładzie. Weźmy bazę danych o nazwie src
na serwerze i utwórz na nim tabelę:
src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3
Zamierzamy również stworzyć publikację w tej bazie danych (pamiętaj, że musisz mieć uprawnienia superużytkownika, aby to zrobić):
src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION
Przejdźmy teraz do bazy danych dst
na innym serwerze i utwórz podobną tabelę:
dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE
A teraz ustawiamy subskrypcję tutaj, który połączy się z publikacją w źródle i zacznie wprowadzać zmiany. (Pamiętaj, że musisz mieć użytkownikarepuser
na serwerze źródłowym z uprawnieniami do replikacji i dostępem do odczytu tabel).
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
Zmiany są zsynchronizowane i widać wiersze po stronie docelowej:
dst=# SELECT * FROM t;
col1 | col2 | col3
------+------+------
1 | 10 | foo
2 | 20 | foo
3 | 30 | foo
(3 rows)
W tabeli docelowej znajduje się dodatkowa kolumna „col3”, której nie dotyka replikacja. Zmiany są replikowane „logicznie” – tak długo, jak będzie możliwe wstawienie wiersza z samymi t.col1 i t.col2, proces replikacji będzie działał.
W porównaniu z replikacją strumieniową, funkcja replikacji logicznej jest idealna do replikacji, powiedzmy, pojedynczego schematu lub zestawu tabel w określonej bazie danych na inny serwer.
Replikacja zmian schematu
Załóżmy, że masz aplikację Django z jej zestawem tabel żyjących w źródłowej bazie danych. Konfiguracja replikacji logicznej w celu przeniesienia wszystkich tych tabel na inny serwer, na którym można uruchamiać raportowanie, analizy, zadania wsadowe, aplikacje wsparcia programistów/klientów i tym podobne, jest łatwa i wydajna bez dotykania „prawdziwych” danych i bez wpływu na aplikację produkcyjną.
Prawdopodobnie największym ograniczeniem obecnie replikacji logicznej jest to, że nie replikuje ona zmian schematu — żadne polecenie DDL wykonywane w źródłowej bazie danych nie powoduje podobnej zmiany w docelowej bazie danych, w przeciwieństwie do replikacji strumieniowej. Na przykład, jeśli zrobimy to w źródłowej bazie danych:
src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1
zostanie to zarejestrowane w docelowym pliku dziennika:
ERROR: logical replication target relation "public.t" is missing some replicated columns
i replikacja się zatrzymuje. Kolumna musi zostać dodana „ręcznie” w miejscu docelowym, po czym replikacja zostanie wznowiona:
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)
Oznacza to, że jeśli Twoja aplikacja Django dodała nową funkcję, która wymaga nowych kolumn lub tabel i musisz uruchomić django-admin migrate
w źródłowej bazie danych konfiguracja replikacji ulega awarii.
Obejście
Najlepszym rozwiązaniem tego problemu byłoby wstrzymanie subskrypcji w miejscu docelowym, najpierw migracja miejsca docelowego, następnie źródła, a następnie wznowienie subskrypcji. Możesz wstrzymywać i wznawiać subskrypcje w ten sposób:
-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;
-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;
Jeśli dodano nowe tabele, a Twoja publikacja nie jest „DLA WSZYSTKICH TABEL”, musisz dodać je do publikacji ręcznie:
ALTER PUBLICATION mypub ADD TABLE newly_added_table;
Musisz także „odświeżyć” subskrypcję po stronie docelowej, aby poinformować Postgres o rozpoczęciu synchronizacji nowych tabel:
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION
Sekwencje
Rozważ tę tabelę u źródła, mając sekwencję:
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)
Sekwencja s_a_seq
został utworzony w celu wsparcia a
kolumna serial
type.To generuje wartości autoinkrementacji dla s.a
. Teraz zreplikujmy to do dst
i wstaw kolejny wiersz:
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, co się właśnie stało? Miejsce docelowe próbowało rozpocząć sekwencję od podstaw i wygenerowało wartość 1 dla a
. Dzieje się tak, ponieważ replikacja logiczna nie powiela wartości sekwencji, ponieważ kolejna wartość tych sekwencji nie jest przechowywana w samej tabeli.
Obejście
Jeśli myślisz o tym logicznie, nie możesz zmodyfikować tej samej wartości „autoinkrementacji” z dwóch miejsc bez synchronizacji dwukierunkowej. Jeśli naprawdę potrzebujesz rosnącej liczby w każdym wierszu tabeli i musisz wstawić do tej tabeli z wielu serwerów, możesz:
- użyj zewnętrznego źródła numeru, takiego jak ZooKeeper lub etcd,
- użyj nienakładających się zakresów – na przykład pierwszy serwer generuje i wstawia liczby z zakresu od 1 do 1 miliona, drugi z zakresu od 1 do 2 milionów i tak dalej.
Tabele bez unikalnych wierszy
Spróbujmy utworzyć tabelę bez klucza podstawowego i ją zreplikować:
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
A wiersze są teraz również w miejscu docelowym:
dst=# SELECT * FROM nopk;
foo
----------
new york
boston
(2 rows)
Teraz spróbujmy usunąć drugi wiersz u źródła:
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.
Dzieje się tak, ponieważ miejsce docelowe nie będzie w stanie jednoznacznie zidentyfikować wiersza, który należy usunąć (lub zaktualizować) bez klucza podstawowego.
Obejście
Możesz oczywiście zmienić schemat, aby zawierał klucz podstawowy. Jeśli nie chcesz tego robić, ALTER TABLE
i ustaw „identyfikator repliki” na pełny wiersz lub unikalny indeks. Na przykład:
src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1
Usunięcie teraz się powiodło, podobnie jak replikacja:
dst=# SELECT * FROM nopk;
foo
----------
new york
(1 row)
Jeśli twoja tabela naprawdę nie ma sposobu na jednoznaczną identyfikację wierszy, to jesteś w błędzie. Aby uzyskać więcej informacji, zobacz sekcję IDENTYFIKACJA REPLIK w ALTERTABLE.
Miejsca docelowe podzielone na różne partycje
Czy nie byłoby miło mieć źródło podzielone w jeden sposób, a przeznaczenie w inny sposób? Na przykład u źródła możemy przechowywać partycje za każdy miesiąc, a u celu za każdy rok. Przypuszczalnie miejscem docelowym jest większa maszyna i musimy przechowywać dane historyczne, ale rzadko potrzebujemy tych danych.
Utwórzmy u źródła tabelę podzieloną co miesiąc:
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
I spróbuj utworzyć roczną tabelę partycjonowaną w miejscu docelowym:
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 skarży się, że potrzebuje tabeli partycji na styczeń 2019 r., której nie mamy zamiaru tworzyć w miejscu docelowym.
Dzieje się tak, ponieważ replikacja logiczna działa nie na poziomie tabeli podstawowej, ale na poziomie tabeli podrzędnej. Nie ma prawdziwego obejścia tego problemu — jeśli używasz partycji, hierarchia partycji musi być taka sama po obu stronach konfiguracji replikacji alogicznej.
Duże obiekty
Nie można replikować dużych obiektów przy użyciu replikacji logicznej. W dzisiejszych czasach prawdopodobnie nie jest to wielka sprawa, ponieważ przechowywanie dużych przedmiotów nie jest powszechną współczesną praktyką. Łatwiej jest również przechowywać referencję do dużego obiektu w jakiejś zewnętrznej, nadmiarowej pamięci (takiej jak NFS, S3 itp.) i replikować tę referencję zamiast przechowywać i replikować sam obiekt.