O PostgreSQL 10 veio com a adição bem-vinda da replicação lógica característica. Isso fornece um meio mais flexível e fácil de replicar suas tabelas do que o mecanismo de replicação de streaming regular. No entanto, ele possui algumas limitações que podem ou não impedi-lo de empregá-lo para replicação. Leia para saber mais.
O que é replicação lógica afinal?
Replicação de streaming
Antes da v10, a única maneira de replicar dados residentes em um servidor era replicar as alterações no nível WAL. Durante sua operação, um servidor PostgreSQL (o primário ) gera uma sequência de arquivos WAL. A idéia básica é levar esses arquivos para outro servidor PostgreSQL (o standby ) que recebe esses arquivos e os "reproduz" para recriar as mesmas alterações que ocorrem no servidor primário. O servidor em espera permanece em um modo somente leitura chamadomodo de recuperação , e quaisquer alterações no servidor em espera não permitidos (ou seja, somente transações somente leitura são permitidas).
O processo de envio dos arquivos WAL do primário para o de espera é chamado de logshipping , e pode ser feito manualmente (scripts para rsync mudam de
$PGDATA/pg_wal
do primário diretório para o secundário) ou por meio de replicação de streaming .Vários recursos como espaços de replicação , feedback em espera e failover foram adicionados ao longo do tempo para melhorar a confiabilidade e a utilidade da replicação de streaming. Um grande “recurso” da replicação de streaming é que é tudo ou nada. Todas as alterações em todos os objetos de todos os bancos de dados no primário devem ser enviadas para o standby, e o standby deve importar todas as alterações. Não é possível replicar seletivamente uma parte de seu banco de dados.
Replicação lógica
Replicação lógica , adicionado na v10, torna possível fazer exatamente isso – replicar apenas um conjunto de tabelas para outros servidores. É melhor explicado com um exemplo. Vamos pegar um banco de dados chamado
src
em um servidor e crie uma tabela nele:src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3
Também vamos criar uma publicação neste banco de dados (observe que você precisa ter privilégios de superusuário para fazer isso):
src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION
Agora vamos para um banco de dados
dst
em outro servidor e crie uma tabela semelhante:dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE
E agora configuramos uma assinatura aqui que se conectará à publicação na fonte e começará a extrair as alterações. (Observe que você precisa ter um usuário
repuser
no servidor de origem com privilégios de replicação e acesso de leitura às tabelas.) 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
As alterações são sincronizadas e você pode ver as linhas no lado de destino:
dst=# SELECT * FROM t;
col1 | col2 | col3
------+------+------
1 | 10 | foo
2 | 20 | foo
3 | 30 | foo
(3 rows)
A tabela de destino tem uma coluna extra “col3”, que não é tocada pela replicação. As alterações são replicadas “logicamente” – portanto, enquanto for possível inserir uma linha apenas com t.col1 e t.col2, o processo de replicação será feito.
Comparado à replicação de streaming, o recurso de replicação lógica é perfeito para replicar, digamos, um único esquema ou um conjunto de tabelas em um banco de dados específico para outro servidor.
Replicação de alterações de esquema
Suponha que você tenha um aplicativo Django com seu conjunto de tabelas vivendo no banco de dados de origem. É fácil e eficiente configurar a replicação lógica para trazer todas essas tabelas para outro servidor, onde você pode executar relatórios, análises, trabalhos em lote, aplicativos de suporte ao desenvolvedor/cliente e similares sem tocar nos dados “reais” e sem afetar o aplicativo de produção.
Possivelmente, a maior limitação da Replicação Lógica atualmente é que ela não replica alterações de esquema – qualquer comando DDL executado no banco de dados de origem não causa uma alteração semelhante no banco de dados de destino, diferentemente da replicação de streaming. Por exemplo, se fizermos isso no banco de dados de origem:
src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1
isso é registrado no arquivo de log de destino:
ERROR: logical replication target relation "public.t" is missing some replicated columns
e a replicação pára. A coluna deve ser adicionada “manualmente” no destino, momento em que a replicação é retomada:
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)
Isso significa que se seu aplicativo Django adicionou um novo recurso que precisa de novas colunas ou tabelas, e você deve executar
django-admin migrate
no banco de dados de origem, a configuração de replicação é interrompida. Solução
Sua melhor aposta para corrigir esse problema seria pausar a assinatura no destino, migrar primeiro o destino, depois a origem e depois retomar a assinatura. Você pode pausar e retomar assinaturas assim:
-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;
-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;
Se novas tabelas forem adicionadas e sua publicação não for “FOR ALL TABLES”, você precisará adicioná-las à publicação manualmente:
ALTER PUBLICATION mypub ADD TABLE newly_added_table;
Você também precisará "atualizar" a assinatura no lado de destino para informar ao Postgres para começar a sincronizar as novas tabelas:
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION
Sequências
Considere esta tabela na fonte, tendo uma sequência:
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)
A sequência
s_a_seq
foi criado para apoiar o a
coluna, de serial
type. Isso gera os valores de incremento automático para s.a
. Agora vamos replicar isso em dst
, e insira outra linha: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, o que acabou de acontecer? O destino tentou iniciar a sequência do zero e gerou um valor de 1 para
a
. Isso ocorre porque a replicação lógica não replica os valores das sequências, pois o próximo valor dessa sequência não é armazenado na própria tabela. Solução
Se você pensar logicamente, você não pode modificar o mesmo valor de “incremento automático” de dois lugares sem sincronização bidirecional. Se você realmente precisa de um número de incremento em cada linha de uma tabela e precisa inserir nessa tabela de vários servidores, você pode:
- use uma fonte externa para o número, como ZooKeeper ou etcd,
- use intervalos sem sobreposição. Por exemplo, o primeiro servidor gera e insere números no intervalo de 1 a 1 milhão, o segundo no intervalo de 1 milhão a 2 milhões e assim por diante.
Tabelas sem linhas exclusivas
Vamos tentar criar uma tabela sem chave primária e replicá-la:
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 as linhas estão agora no destino também:
dst=# SELECT * FROM nopk;
foo
----------
new york
boston
(2 rows)
Agora vamos tentar deletar a segunda linha na fonte:
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.
Isso acontece porque o destino não poderá identificar exclusivamente a linha que precisa ser excluída (ou atualizada) sem uma chave primária.
Solução
É claro que você pode alterar o esquema para incluir uma chave primária. Caso você não queira fazer isso, você
ALTER TABLE
e defina a “identificação da réplica” para a linha completa ou um índice exclusivo. Por exemplo:src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1
A exclusão agora é bem-sucedida e a replicação também:
dst=# SELECT * FROM nopk;
foo
----------
new york
(1 row)
Se sua tabela realmente não tem como identificar linhas de forma exclusiva, você está um pouco preso. Consulte a seção REPLICA IDENTITY de ALTERTABLE para obter mais informações.
Destinos com partições diferentes
Não seria bom ter uma fonte que fosse particionada de uma maneira e um destino de outra maneira? Por exemplo, na origem podemos manter as partições para cada mês e no destino para cada ano. Presumivelmente, o destino é uma máquina maior, e precisamos manter dados históricos, mas raramente precisamos desses dados.
Vamos criar uma tabela particionada mensalmente na fonte:
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 tente criar uma tabela particionada anualmente no destino:
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=#
O Postgres reclama que precisa da tabela de partições para janeiro de 2019, que não temos intenção de criar no destino.
Isso acontece porque a replicação lógica não funciona no nível da tabela base, mas no nível da tabela filha. Não há solução real para isso - se você estiver usando partições, a hierarquia de partições deve ser a mesma em ambos os lados da configuração de replicação lógica.
Objetos grandes
Objetos grandes não podem ser replicados usando replicação lógica. Isso provavelmente não é grande coisa hoje em dia, pois armazenar objetos grandes não é uma prática comum nos dias de hoje. Também é mais fácil armazenar uma referência a um objeto grande em algum armazenamento externo redundante (como NFS, S3 etc.) e replicar essa referência em vez de armazenar e replicar o próprio objeto.