PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Gotcha do logicznej replikacji PostgreSQL

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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. django.db.utils.ProgrammingError:relacja app_user nie istnieje podczas testu manage.py

  2. PostgreSQL:serial a tożsamość

  3. Dodaj dni do daty w PostgreSQL

  4. Jak uruchomić serwer PostgreSQL na Mac OS X?

  5. Zainstaluj Postgres.app na komputerze Mac