PostgreSQL 10 è arrivato con la gradita aggiunta della replica logica caratteristica. Ciò fornisce un mezzo più flessibile e più semplice per replicare le tabelle rispetto al normale meccanismo di replica in streaming. Tuttavia, ha alcune limitazioni che potrebbero o meno impedirti di utilizzarlo per la replica. Continua a leggere per saperne di più.
Cos'è comunque la replica logica?
Replica in streaming
Prima della v10, l'unico modo per replicare i dati che risiedono in un server era replicare le modifiche a livello WAL. Durante il suo funzionamento, un server PostgreSQL (il primario ) genera una sequenza di file WAL. L'idea di base è trasferire questi file su un altro server PostgreSQL (il standby ) che prende questi file e li "riproduce" per ricreare le stesse modifiche che si verificano sul server principale. Il server di standby rimane in una modalità di sola lettura denominata modalità di ripristino e tutte le modifiche al server di standby non consentito (vale a dire, sono consentite solo transazioni di sola lettura).
Il processo di spedizione dei file WAL dal primario allo standby è chiamato logshipping , e può essere eseguito manualmente (script per sincronizzare le modifiche da $PGDATA/pg_wal
della primaria directory al secondario) o tramite replica in streaming .Varie funzioni come slot di replica , feedback in standby e failover sono stati aggiunti nel tempo per migliorare l'affidabilità e l'utilità della replica in streaming.
Una grande "caratteristica" della replica in streaming è che è tutto o niente. Tutte le modifiche a tutti gli oggetti da tutti i database sul database primario devono essere inviate allo standby e lo standby deve importare ogni modifica. Non è possibile replicare selettivamente una parte del tuo database.
Replica logica
Replica logica , aggiunto nella v10, consente proprio questo:replicare solo un insieme di tabelle su altri server. È meglio spiegato con un esempio. Prendiamo un database chiamato src
in un server e crea una tabella al suo interno:
src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3
Creeremo anche una pubblicazione in questo database (nota che devi avere i privilegi di superutente per farlo):
src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION
Ora andiamo in un database dst
su un altro server e creare una tabella simile:
dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE
E ora impostiamo un abbonamento qui che si collegherà alla pubblicazione sulla fonte e inizierà a inserire le modifiche. (Nota che devi avere un utenterepuser
sul server di origine con privilegi di replica e accesso in lettura alle tabelle.)
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
Le modifiche sono sincronizzate e puoi vedere le righe sul lato di destinazione:
dst=# SELECT * FROM t;
col1 | col2 | col3
------+------+------
1 | 10 | foo
2 | 20 | foo
3 | 30 | foo
(3 rows)
La tabella di destinazione ha una colonna aggiuntiva "col3", che non viene toccata dalla replica. Le modifiche vengono replicate “logicamente” – quindi, finché è possibile inserire una riga con t.col1 e t.col2 solo, il processo di replica lo farà.
Rispetto alla replica in streaming, la funzione di replica logica è perfetta per replicare, ad esempio, un singolo schema o un insieme di tabelle in un database specifico su un altro server.
Replica delle modifiche allo schema
Supponiamo di avere un'applicazione Django con il suo set di tabelle che vivono nel database di origine. È facile ed efficiente impostare la replica logica per trasferire tutte queste tabelle in un altro server, dove è possibile eseguire report, analisi, lavori batch, app di supporto per sviluppatori/clienti e simili senza toccare i dati "reali" e senza influire sull'app di produzione.
Probabilmente il limite maggiore della replica logica attualmente è che non replica le modifiche dello schema:qualsiasi comando DDL eseguito nel database di origine non provoca una modifica simile nel database di destinazione, a differenza della replica in streaming. Ad esempio, se lo facciamo nel database di origine:
src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1
questo viene registrato nel file di registro di destinazione:
ERROR: logical replication target relation "public.t" is missing some replicated columns
e la replica si interrompe. La colonna va aggiunta “manualmente” alla destinazione, a quel punto la replica riprende:
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)
Ciò significa che se la tua applicazione Django ha aggiunto una nuova funzionalità che necessita di nuove colonne o tabelle e devi eseguire django-admin migrate
nel database di origine, l'impostazione della replica si interrompe.
Soluzione alternativa
La soluzione migliore per risolvere questo problema sarebbe mettere in pausa l'abbonamento sulla destinazione, migrare prima la destinazione, quindi l'origine e quindi riprendere l'abbonamento. Puoi mettere in pausa e riprendere gli abbonamenti in questo modo:
-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;
-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;
Se vengono aggiunte nuove tabelle e la tua pubblicazione non è "PER TUTTE LE TABELLE", dovrai aggiungerle alla pubblicazione manualmente:
ALTER PUBLICATION mypub ADD TABLE newly_added_table;
Dovrai anche "aggiornare" l'abbonamento sul lato di destinazione per dire a Postgres di iniziare a sincronizzare le nuove tabelle:
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION
Sequenze
Considera questa tabella alla fonte, con una sequenza:
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)
La sequenza s_a_seq
è stato creato per supportare il a
colonna, di serial
type.Questo genera i valori di incremento automatico per s.a
. Ora replichiamo questo in dst
e inserisci un'altra riga:
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)
Ops, cosa è appena successo? La destinazione ha tentato di avviare la sequenza da zero e ha generato un valore di 1 per a
. Questo perché la replica logica non replica i valori per le sequenze poiché il valore successivo di thesequence non è memorizzato nella tabella stessa.
Soluzione alternativa
Se ci pensi logicamente, non puoi modificare lo stesso valore di "incremento automatico" da due punti senza sincronizzazione bidirezionale. Se hai davvero bisogno di un numero incrementale in ogni riga di una tabella e devi inserirlo in quella tabella da più server, puoi:
- utilizza una fonte esterna per il numero, come ZooKeeper o etcd,
- utilizza intervalli non sovrapposti:ad esempio, il primo server genera e inserisce numeri compresi tra 1 e 1 milione, il secondo tra 1 milione e 2 milioni e così via.
Tabelle senza righe univoche
Proviamo a creare una tabella senza una chiave primaria e a replicarla:
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
E ora anche le righe sono sulla destinazione:
dst=# SELECT * FROM nopk;
foo
----------
new york
boston
(2 rows)
Ora proviamo a eliminare la seconda riga all'origine:
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.
Ciò accade perché la destinazione non sarà in grado di identificare in modo univoco la riga che deve essere eliminata (o aggiornata) senza una chiave primaria.
Soluzione alternativa
Ovviamente puoi modificare lo schema per includere una chiave primaria. Nel caso tu non voglia farlo, ALTER TABLE
e imposta "replica identifica" sulla riga intera o su un indice univoco. Ad esempio:
src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1
L'eliminazione ora riesce e anche la replica:
dst=# SELECT * FROM nopk;
foo
----------
new york
(1 row)
Se la tua tabella non ha davvero modo di identificare in modo univoco le righe, allora sei un po' bloccato. Per ulteriori informazioni, vedere la sezione REPLICA IDENTITY di ALTERTABLE.
Destinazioni partizionate in modo diverso
Non sarebbe bello avere una fonte che è partizionata in un modo e una destinazione in un modo diverso? Ad esempio, alla fonte possiamo mantenere le partizioni per ogni mese e alla destinazione per ogni anno. Presumibilmente la destinazione è una macchina più grande e abbiamo bisogno di conservare i dati storici, ma raramente abbiamo bisogno di quei dati.
Creiamo una tabella partizionata mensilmente all'origine:
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
E prova a creare una tabella partizionata annualmente nella destinazione:
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 si lamenta di aver bisogno della tabella delle partizioni per gennaio 2019, che non abbiamo intenzione di creare sulla destinazione.
Ciò accade perché la replica logica non funziona a livello di tabella di base, ma a livello di tabella figlio. Non esiste una vera soluzione alternativa per questo:se stai riutilizzando le partizioni, la gerarchia delle partizioni deve essere la stessa su entrambi i lati della configurazione della replica logica.
Oggetti di grandi dimensioni
Gli oggetti di grandi dimensioni non possono essere replicati utilizzando la replica logica. Questo probabilmente non è un grosso problema al giorno d'oggi, poiché la conservazione di oggetti di grandi dimensioni non è una pratica comune moderna. È anche più facile archiviare un riferimento a un oggetto di grandi dimensioni su una memoria esterna ridondante (come NFS, S3 ecc.) e replicare quel riferimento piuttosto che archiviare e replicare l'oggetto stesso.