PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Atualizações baseadas em gatilhos personalizados para PostgreSQL


1ª REGRA: Você não atualiza o PostgreSQL com replicação baseada em gatilho
2ª REGRA: Você NÃO atualiza o PostgreSQL com replicação baseada em gatilho
3ª REGRA: Se você atualizar o PostgreSQL com replicação baseada em gatilho, prepare-se para sofrer. E prepare-se bem.

Deve haver uma razão muito séria para não usar o pg_upgrade para atualizar o PostgreSQL.

OK, digamos que você não pode permitir mais do que segundos de inatividade. Use pglogical então.

OK, digamos que você execute o 9.3 e, portanto, não possa usar o pglogical. Use Londiste.

Não consegue encontrar o README legível? Use SLONY.

Muito complicado? Use a replicação de streaming - promova o escravo e execute pg_upgrade nele - e alterne os aplicativos para trabalhar com o novo servidor promovido.

Seu aplicativo é relativamente intensivo de gravação o tempo todo? Você analisou todas as soluções possíveis e ainda deseja configurar a replicação baseada em gatilho personalizado? Há coisas que você deve prestar atenção então:
  • Todas as tabelas precisam de PK. Você não deve confiar no ctid (mesmo com o autovacuum desativado)
  • Você precisará habilitar o gatilho para todas as tabelas vinculadas de restrição (e pode precisar de FK adiado)
  • As sequências precisam de sincronização manual
  • As permissões não são replicadas (a menos que você também configure um acionador de evento)
  • Os acionadores de eventos podem ajudar na automação do suporte a novas tabelas, mas é melhor não complicar demais um processo já complicado. (como criar um gatilho e uma tabela externa na criação da tabela, também criar a mesma tabela no servidor externo ou alterar a tabela do servidor remoto com a mesma alteração, você faz no banco de dados antigo)
  • Para cada gatilho de instrução é menos confiável, mas provavelmente mais simples
  • Você deve imaginar vividamente seu processo de migração de dados preexistente
  • Você deve planejar a acessibilidade limitada de tabelas ao configurar e ativar a replicação baseada em gatilho
  • Você deve conhecer totalmente suas dependências e restrições de relações antes de seguir esse caminho.

Avisos suficientes? Já quer jogar? Vamos começar com algum código então.

Antes de escrever qualquer gatilho, temos que construir um conjunto de dados simulado. Por quê? Não seria muito mais fácil ter um gatilho antes de termos dados? Então, os dados seriam replicados para o cluster de “atualização” de uma só vez? Claro que sim. Mas então o que queremos atualizar? Basta construir um conjunto de dados em uma versão mais recente. Então sim, se você planeja atualizar para uma versão superior e precisa adicionar alguma tabela, crie gatilhos de replicação antes de colocar os dados, isso eliminará a necessidade de sincronizar dados não replicados posteriormente. Mas essas novas tabelas são, podemos dizer, uma parte fácil. Então, vamos primeiro simular o caso quando temos dados antes de decidirmos atualizar.

Vamos supor que um servidor desatualizado seja chamado de p93 (mais antigo suportado) e aquele para o qual replicamos seja chamado de p10 (11 está a caminho neste trimestre, mas ainda não aconteceu):
\c PostgreSQL
select pg_terminate_backend(pid) from pg_stat_activity where datname in ('p93','p10');
drop database if exists p93;
drop database if exists p10;

Aqui eu uso o psql, assim posso usar o meta-comando \c para conectar a outro banco de dados. Se você quiser seguir esse código com outro cliente, precisará se reconectar. É claro que você não precisa dessa etapa se executar isso pela primeira vez. Eu tive que recriar minha sandbox várias vezes, então salvei declarações…
create database p93; --old db (I use 9.3 as oldest supported ATM version)
create database p10; --new db 

Então criamos dois novos bancos de dados. Agora vou me conectar ao que queremos atualizar e criar vários tipos de dados funkey e usá-los para preencher uma tabela que consideraremos como pré-existente posteriormente:
\c p93
create type myenum as enum('a', 'b');--adding some complex types
create type mycomposit as (a int, b text); --and again...
create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);
insert into t values(0, now(), '{"a":{"aa":[1,3,2]}}', 'foo', 'b', (3,'aloha'));
insert into t (j,e) values ('{"b":null}', 'a');
insert into t (t) select chr(g) from generate_series(100,240) g;--add some more data
delete from t where i > 3 and i < 142; --mockup activity and mix tuples to be not sequential
insert into t (t) select null;

Agora o que temos?
  ctid   |  i  |           ts           |          j           |  t  | e |     c     
---------+-----+------------------------+----------------------+-----+---+-----------
 (0,1)   |   0 | 2018-07-08 08:03:00+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
 (0,2)   |   1 | 2018-07-08 08:03:00+03 | {"b":null}           |     | a | 
 (0,3)   |   2 | 2018-07-08 08:03:00+03 |                      | d   |   | 
 (0,4)   |   3 | 2018-07-08 08:03:00+03 |                      | e   |   | 
 (0,143) | 142 | 2018-07-08 08:03:00+03 |                      | ð   |   | 
 (0,144) | 143 | 2018-07-08 08:03:00+03 |                      |     |   | 
(6 rows)

OK, alguns dados - por que inseri e excluí tanto? Bem, tentamos simular um conjunto de dados que existia há algum tempo. Então eu estou tentando torná-lo um pouco espalhado. Vamos mover mais uma linha (0,3) para o final da página (0,145):
update t set j = '{}' where i =3; --(0,4)

Agora vamos supor que usaremos PostgreSQL_fdw (usar dblink aqui seria basicamente o mesmo e provavelmente mais rápido para 9.3, então faça isso se desejar).
create extension PostgreSQL_fdw;
create server p10 foreign data wrapper PostgreSQL_fdw options (host 'localhost', dbname 'p10'); --I know it's the same 9.3 server - change host to other version and use other cluster if you wish. It's not important for the sandbox...
create user MAPPING FOR vao SERVER p10 options(user 'vao', password 'tsun');

Agora podemos usar pg_dump -s para obter o DDL, mas eu apenas o tenho acima. Temos que criar a mesma tabela no cluster de versão superior para replicar dados para:
\c p10
create type myenum as enum('a', 'b');--adding some complex types
create type mycomposit as (a int, b text); --and again...
create table t(i serial not null primary key, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit);

Agora voltamos para 9.3 e usamos tabelas estrangeiras para migração de dados (vou usar f_ convenção para nomes de tabelas aqui, f significa estrangeiro):
\c p93
create foreign table f_t(i serial, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit) server p10 options (TABLE_name 't');

Finalmente! Criamos uma função de inserção e acionamos.
create or replace function tgf_i() returns trigger as $$
begin
  execute format('insert into %I select ($1).*','f_'||TG_RELNAME) using NEW;
  return NEW;
end;
$$ language plpgsql;

Aqui e mais tarde usarei links para códigos mais longos. Primeiro, para que o texto falado não afundasse na linguagem de máquina. Segundo porque eu uso várias versões das mesmas funções para refletir como o código deve evoluir sob demanda.
--OK - first table ready - lets try logical trigger based replication on inserts:
insert into t (t) select 'one';
--and now transactional:
begin;
  insert into t (t) select 'two';
  select ctid, * from f_t;
  select ctid, * from t;
rollback;
select ctid, * from f_t where i > 143;
select ctid, * from t where i > 143;

Resultante:
INSERT 0 1
BEGIN
INSERT 0 1
 ctid  |  i  |           ts           | j |  t  | e | c 
-------+-----+------------------------+---+-----+---+---
 (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
 (0,2) | 145 | 2018-07-08 08:27:15+03 |   | two |   | 
(2 rows)

  ctid   |  i  |           ts           |          j           |  t  | e |     c     
---------+-----+------------------------+----------------------+-----+---+-----------
 (0,1)   |   0 | 2018-07-08 08:27:15+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha)
 (0,2)   |   1 | 2018-07-08 08:27:15+03 | {"b":null}           |     | a | 
 (0,3)   |   2 | 2018-07-08 08:27:15+03 |                      | d   |   | 
 (0,143) | 142 | 2018-07-08 08:27:15+03 |                      | ð   |   | 
 (0,144) | 143 | 2018-07-08 08:27:15+03 |                      |     |   | 
 (0,145) |   3 | 2018-07-08 08:27:15+03 | {}                   | e   |   | 
 (0,146) | 144 | 2018-07-08 08:27:15+03 |                      | one |   | 
 (0,147) | 145 | 2018-07-08 08:27:15+03 |                      | two |   | 
(8 rows)

ROLLBACK
 ctid  |  i  |           ts           | j |  t  | e | c 
-------+-----+------------------------+---+-----+---+---
 (0,1) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
(1 row)

  ctid   |  i  |           ts           | j |  t  | e | c 
---------+-----+------------------------+---+-----+---+---
 (0,146) | 144 | 2018-07-08 08:27:15+03 |   | one |   | 
(1 row)

O que vemos aqui? Vemos que os dados recém-inseridos são replicados para o banco de dados p10 com sucesso. E, consequentemente, é revertido se a transação falhar. Até agora tudo bem. Mas você não pode não notar (sim, sim - não não) que a tabela em p93 é muito maior - os dados antigos não se replicaram. Como podemos chegar lá? Bem simples:
insert into … select local.* from ...outer join foreign where foreign.PK is null 

faria. E essa não é a principal preocupação aqui - você deve se preocupar em como gerenciará dados pré-existentes em atualizações e exclusões - porque as instruções executadas com êxito na versão inferior do banco de dados falharão ou apenas afetarão zero linhas na versão superior - apenas porque não há dados preexistentes ! E aqui chegamos aos segundos de tempo de inatividade. (Se fosse um filme, claro que aqui teríamos um flashback, mas infelizmente - se a frase “seconds of downtime” não chamou sua atenção antes, você terá que ir acima e procurar a frase...)

Para habilitar todos os gatilhos de instruções, você precisa congelar a tabela, copiar todos os dados e, em seguida, habilitar os gatilhos, para que as tabelas em bancos de dados de versões inferiores e superiores estejam em sincronia e todas as instruções tenham apenas o mesmo (ou extremamente próximo, porque física distribuição será diferente, novamente olhe acima no primeiro exemplo para a coluna ctid) effect. Mas executar essa “replicação de ativação” na tabela em uma transação biiiiiig não será segundos de tempo de inatividade. Potencialmente, tornará o site somente leitura por horas. Especialmente se a mesa estiver aproximadamente ligada por FK com outras mesas grandes.

Bem somente leitura não é tempo de inatividade completo. Mas depois vamos tentar deixar todos os SELECTS e alguns INSERT,DELETE,UPDATE funcionando (nos dados novos, falhando nos antigos). Mover a tabela ou transação para somente leitura pode ser feito de várias maneiras - seria alguma abordagem do PostgreSQL, ou nível de aplicativo, ou mesmo revogando temporariamente as permissões de acordo. Essas abordagens em si podem ser um tópico para seu próprio blog, portanto, vou apenas mencioná-las.

Qualquer maneira. De volta aos gatilhos. Para fazer a mesma ação, exigindo trabalhar em uma linha distinta (UPDATE, DELETE) na tabela remota como você faz no local, precisamos usar chaves primárias, pois a localização física será diferente. E as chaves primárias são criadas em tabelas diferentes com colunas diferentes, portanto, temos que criar uma função única para cada tabela ou tentar escrever alguma genérica. Vamos (para simplificar) assumir que temos apenas uma coluna PKs, então esta função deve ajudar. Então, finalmente! Vamos ter uma função de atualização aqui. E obviamente um gatilho:
create trigger tgu before update on t for each row execute procedure tgf_u();
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper
E vamos ver se funciona:
begin;
        update t set j = '{"updated":true}' where i = 144;
        select * from t where i = 144;
        select * from f_t where i = 144;
Rollback;

Resultante para:
BEGIN
psql:blog.sql:71: INFO:  (144,"2018-07-08 09:09:20+03","{""updated"":true}",one,,)
UPDATE 1
  i  |           ts           |        j         |  t  | e | c 
-----+------------------------+------------------+-----+---+---
 144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
(1 row)

  i  |           ts           |        j         |  t  | e | c 
-----+------------------------+------------------+-----+---+---
 144 | 2018-07-08 09:09:20+03 | {"updated":true} | one |   | 
(1 row)

ROLLBACK

OK. E enquanto ainda está quente, vamos adicionar a função de gatilho de exclusão e a replicação também:
create trigger tgd before delete on t for each row execute procedure tgf_d();

E verifique:
begin;
        delete from t where i = 144;
        select * from t where i = 144;
        select * from f_t where i = 144;
Rollback;

Dando:
DELETE 1
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

Como lembramos (quem poderia esquecer isso!) não estamos transformando o suporte de “replicação” em transação. E devemos se quisermos dados consistentes. Como dito acima, TODOS os gatilhos de instruções em TODAS as tabelas relacionadas ao FK devem ser habilitados em uma transação, previamente preparada pela sincronização de dados. Caso contrário, podemos cair em:
begin;
        select * from t where i = 3;
        delete from t where i = 3;
        select * from t where i = 3;
        select * from f_t where i = 3;
Rollback;

Dando:
p93=# begin;
BEGIN
p93=#         select * from t where i = 3;
 i |           ts           | j  | t | e | c 
---+------------------------+----+---+---+---
 3 | 2018-07-08 09:16:27+03 | {} | e |   | 
(1 row)

p93=#         delete from t where i = 3;
DELETE 1
p93=#         select * from t where i = 3;
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

p93=#         select * from f_t where i = 3;
 i | ts | j | t | e | c 
---+----+---+---+---+---
(0 rows)

p93=# rollback;

Oba! Excluímos uma linha no banco de dados da versão inferior e não no mais recente! Só porque não estava lá. Isso não aconteceria se fizéssemos da maneira correta (começar;sincronizar;ativar gatilho;fim;). Mas o caminho certo tornaria as tabelas somente leitura por um longo tempo! O leitor mais hardcore diria até mesmo 'por que você faria a replicação baseada em gatilho então?'.

Você pode fazer isso com pg_upgrade como as pessoas “normais”. E no caso de replicação de streaming, você pode tornar todo o conjunto somente leitura. Pause a reprodução do xlog e atualize o mestre enquanto o aplicativo ainda é o escravo.

Exatamente! Eu não comecei com isso?

A replicação baseada em gatilhos aparece quando você precisa de algo muito especial. Por exemplo, você pode tentar permitir SELECT e algumas modificações em dados recém-criados, não apenas RO. Digamos que você tenha um questionário on-line - o usuário se cadastra, responde, recebe seus pontos-grátis-bônus-outros-ninguém-precisa-grandes-coisas e sai. Com essa estrutura você pode apenas proibir modificações em dados que ainda não estão em versão superior, permitindo todo o fluxo de dados para novos usuários.

Assim, você abandona poucos funcionários de caixas eletrônicos on-line, deixando os recém-chegados trabalharem sem nem perceber que você está no meio de uma atualização. Parece horrível, mas eu não disse hipoteticamente? eu não? Bem, eu quis dizer isso.

Não importa qual seja o caso da vida real, vamos ver como você pode implementá-lo. As funções de exclusão e atualização serão alteradas. E vamos verificar o último cenário agora:
BEGIN
psql:blog.sql:86: ERROR:  This data is not replicated yet, thus can't be deleted
psql:blog.sql:87: ERROR:  current transaction is aborted, commands ignored until end of transaction block
psql:blog.sql:88: ERROR:  current transaction is aborted, commands ignored until end of transaction block
ROLLBACK

A linha não foi excluída na versão inferior, porque não foi encontrada na versão superior. A mesma coisa aconteceria com atualizado. Tente você mesmo. Agora você pode iniciar a sincronização de dados sem interromper muitas modificações na tabela que você inclui na replicação baseada em gatilho.

É melhor? Pior? É diferente - tem muitas falhas e alguns benefícios sobre o sistema RO global. Meu objetivo era demonstrar por que alguém iria querer usar um método tão complicado sobre o normal - para obter habilidades específicas em um processo estável e bem conhecido. Com algum custo, é claro…

Então, agora quando nos sentimos um pouco mais seguros para consistência de dados e enquanto nossos dados pré-existentes na tabela t estão sincronizando com p10, podemos falar sobre outras tabelas. Como tudo funcionaria com o FK (afinal, eu mencionei o FK tantas vezes, eu tenho que incluí-lo na amostra). Bem, por que esperar?
create table c (i serial, t int references t(i), x text);
--and accordingly a foreign table - the one on newer version...
\c p10
create table c (i serial, t int references t(i), x text);
\c p93
create foreign table f_c(i serial, t int, x text) server p10 options (TABLE_name 'c');
--let’s pretend it had some data before we decided to migrate with triggers to a higher version
insert into c (t,x) values (1,'FK');
--- so now we add triggers to replicate DML:
create trigger tgi before insert on c for each row execute procedure tgf_i();
create trigger tgu before update on c for each row execute procedure tgf_u();
create trigger tgd before delete on c for each row execute procedure tgf_d();

Certamente vale a pena envolver esses três em uma função com o objetivo de "disparar" muitas tabelas. Mas eu não vou. Como não vou adicionar mais tabelas - o banco de dados de duas relações referenciadas já é uma rede tão bagunçada!
--now, what would happen if we tr inserting referenced FK, that does not exist on remote db?..
insert into c (t,x) values (2,'FK');
/* it fails with:
psql:blog.sql:139: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
a new row isn't inserted neither on remote, nor local db, so we have safe data consistencyy, but inserts are blocked?..
Yes untill data that existed untill trigerising gets to remote db - ou cant insert FK with before triggerising keys, yet - a new (both t and c tables) data will be accepted:
*/
insert into t(i) values(4); --I use gap we got by deleting data above, so I dont need to "returning" and know the exact id -less coding in sample script
insert into c(t) values(4);
select * from c;
select * from f_c;

Resulta em:
psql:blog.sql:109: ERROR:  insert or update on table "c" violates foreign key constraint "c_t_fkey"
DETAIL:  Key (t)=(2) is not present in table "t".
CONTEXT:  Remote SQL command: INSERT INTO public.c(i, t, x) VALUES ($1, $2, $3)
SQL statement "insert into f_c select ($1).*"
PL/pgSQL function tgf_i() line 3 at EXECUTE statement
INSERT 0 1
INSERT 0 1
 i | t | x  
---+---+----
 1 | 1 | FK
 3 | 4 | 
(2 rows)

 i | t | x 
---+---+---
 3 | 4 | 
(1 row)

Novamente. Parece que a consistência dos dados está em vigor. Você também pode começar a sincronizar dados para a nova tabela c…

Cansado? Eu definitivamente estou.

Conclusão


Para concluir, gostaria de destacar alguns erros que cometi ao analisar essa abordagem. Enquanto eu estava construindo a instrução de atualização, listando dinamicamente todas as colunas do pg_attribute, perdi uma hora. Imagine como fiquei desapontado ao descobrir mais tarde que esqueci completamente da construção UPDATE (lista) =(lista)! E a função chegou a um estado muito mais curto e legível.

Então o erro número um foi - tentar construir tudo sozinho, só porque parece tão acessível. Ainda é, mas, como sempre, alguém provavelmente já fez melhor - gastar dois minutos apenas para verificar se realmente é assim pode poupar horas de reflexão mais tarde.

E a segunda coisa parecia muito mais simples para mim onde eles se tornaram muito mais profundos, e eu compliquei muitos casos que são perfeitamente mantidos pelo modelo de transação do PostgreSQL.

Então, só depois de tentar construir o sandbox eu consegui uma compreensão um tanto clara das estimativas dessa abordagem.

Portanto, o planejamento é obviamente necessário, mas não planeje mais do que você pode realmente fazer.

A experiência vem com a prática.

Minha caixa de areia me lembrou de uma estratégia de computador - você senta para ela depois do almoço e pensa - "aha, aqui eu construo Pyramyd, lá eu pego arco e flecha, então eu me converto para Sons of Ra e construo 20 homens de arco longo, e aqui eu ataco o patético vizinhos. Duas horas de glória.” E DE REPENTE você se encontra na manhã seguinte, duas horas antes do trabalho com “Como cheguei aqui? Por que eu tenho que assinar essa aliança humilhante com bárbaros não lavados para salvar meu último homem de arco longo e eu realmente preciso vender minha pirâmide tão construída para isso?”

Leituras:
  • https://www.PostgreSQL.org/docs/current/static/different-replication-solutions.html
  • https://stackoverflow.com/questions/15343075/update-multiple-columns-in-a-trigger-function-in-plpgsql