PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Aggiornamenti personalizzati basati su trigger per PostgreSQL

1a REGOLA: Non aggiorni PostgreSQL con la replica basata su trigger
2a REGOLA: NON aggiorni PostgreSQL con la replica basata su trigger
3a REGOLA: Se aggiorni PostgreSQL con la replica basata su trigger, preparati a soffrire. E preparatevi bene.

Ci deve essere una ragione molto seria per non usare pg_upgrade per aggiornare PostgreSQL.

OK, diciamo che non puoi permetterti più di secondi di inattività. Usa pglogical allora.

OK, diciamo che esegui 9.3 e quindi non puoi usare pglogical. Usa Londiste.

Non riesci a trovare un README leggibile? Usa SLONY.

Troppo complicato? Usa la replica in streaming:promuovi lo slave ed esegui pg_upgrade su di esso, quindi cambia app per funzionare con il nuovo server promosso.

La tua app è relativamente ad alta intensità di scrittura tutto il tempo? Hai esaminato tutte le possibili soluzioni e desideri ancora configurare la replica basata su trigger personalizzata? Ci sono cose a cui dovresti prestare attenzione allora:

  • Tutte le tabelle richiedono PK. Non dovresti fare affidamento su ctid (anche con autovacuum disabilitato)
  • Dovrai abilitare il trigger per tutte le tabelle vincolate con vincoli (e potrebbe essere necessario Deferred FK)
  • Le sequenze richiedono la sincronizzazione manuale
  • Le autorizzazioni non vengono replicate (a meno che tu non imposti anche un attivatore di eventi)
  • I trigger di eventi possono aiutare con l'automazione del supporto per le nuove tabelle, ma è meglio non complicare eccessivamente un processo già complicato. (come creare un trigger e una tabella esterna sulla creazione della tabella, creare anche la stessa tabella su un server esterno o alterare la tabella del server remoto con la stessa modifica, si fa sul vecchio db)
  • Per ogni istruzione il trigger è meno affidabile ma probabilmente più semplice
  • Dovresti immaginare vividamente il tuo processo di migrazione dei dati preesistente
  • Dovresti pianificare l'accessibilità limitata delle tabelle durante la configurazione e l'abilitazione della replica basata su trigger
  • Dovresti assolutamente conoscere le tue dipendenze e vincoli delle tue relazioni prima di procedere in questo modo.

Basta avvertimenti? Vuoi già giocare? Cominciamo con un po' di codice allora.

Prima di scrivere qualsiasi trigger, dobbiamo creare un set di dati mock up. Come mai? Non sarebbe molto più facile avere un trigger prima di avere i dati? Quindi i dati si replicherebbero immediatamente nel cluster di "aggiornamento"? Certo che sarebbe. Ma allora cosa vogliamo aggiornare? Basta creare un set di dati su una versione più recente. Quindi sì, se pianifichi l'aggiornamento a una versione successiva e devi aggiungere una tabella, creare trigger di replica prima di inserire i dati, eliminerà la necessità di sincronizzare i dati non replicati in un secondo momento. Ma tali nuove tabelle sono, potremmo dire, una parte facile. Quindi, per prima cosa, prendiamo in giro il caso quando disponiamo di dati prima di decidere di eseguire l'aggiornamento.

Supponiamo che un server obsoleto si chiami p93 (il più vecchio supportato) e quello su cui replichiamo si chiami p10 (11 è in arrivo questo trimestre, ma non è ancora successo):

\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;

Qui uso psql, quindi posso usare \c meta-comando per connettermi ad altri db. Se vuoi seguire questo codice con un altro client, dovrai invece riconnetterti. Ovviamente non è necessario questo passaggio se lo esegui per la prima volta. Ho dovuto ricreare più volte la mia sandbox, quindi ho salvato le istruzioni...

create database p93; --old db (I use 9.3 as oldest supported ATM version)
create database p10; --new db 

Quindi creiamo due nuovi database. Ora mi collegherò a quello che vogliamo aggiornare e creerò diversi tipi di dati funkey e li userò per compilare una tabella che considereremo preesistente in seguito:

\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;

Ora cosa abbiamo?

  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, alcuni dati - perché ho inserito e poi cancellato così tanto? Bene, proviamo a simulare un set di dati che esisteva da un po'. Quindi sto cercando di renderlo un po' sparso. Spostiamoci di un'altra riga (0,3) alla fine della pagina (0,145):

update t set j = '{}' where i =3; --(0,4)

Ora supponiamo che useremo PostgreSQL_fdw (usare dblink qui sarebbe sostanzialmente lo stesso e probabilmente più veloce per 9.3, quindi per favore fallo se lo desideri).

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');

Ora possiamo usare pg_dump -s per ottenere il DDL, ma ce l'ho appena sopra. Dobbiamo creare la stessa tabella nel cluster di versione superiore per replicare i dati su:

\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);

Ora torniamo alla 9.3 e utilizziamo le tabelle esterne per la migrazione dei dati (Userò f_ convenzione per i nomi delle tabelle qui, f sta per straniero):

\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');

Infine! Creiamo una funzione di inserimento e un trigger.

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;

Qui e in seguito userò i link per un codice più lungo. Primo, in modo che il testo parlato non sprofondasse nel linguaggio macchina. Secondo perché utilizzo diverse versioni delle stesse funzioni per riflettere come il codice dovrebbe evolversi su richiesta.

--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;

Risultato:

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)

Cosa vediamo qui? Vediamo che i dati appena inseriti vengono replicati correttamente nel database p10. E di conseguenza viene annullato se la transazione non riesce. Fin qui tutto bene. Ma non potresti non notare (sì, sì - non no) che la tabella su p93 è molto più grande - i vecchi dati non sono stati replicati. Come lo arriviamo lì? Ben semplice:

insert into … select local.* from ...outer join foreign where foreign.PK is null 

farebbe. E questa non è la preoccupazione principale qui - dovresti piuttosto preoccuparti di come gestirai i dati preesistenti su aggiornamenti ed eliminazioni - perché le istruzioni eseguite correttamente su una versione inferiore db falliranno o influiranno solo su zero righe su quelle superiori - solo perché non ci sono dati preesistenti ! E qui veniamo alla frase dei secondi di inattività. (Se fosse un film, ovviamente qui avremmo un flashback, ma ahimè - se la frase "secondi di inattività" non ha attirato la tua attenzione prima, dovrai andare sopra e cercare la frase...)

Per abilitare tutti i trigger di istruzioni è necessario bloccare la tabella, copiare tutti i dati e quindi abilitare i trigger, in modo che le tabelle sui database di versioni inferiori e superiori sarebbero sincronizzate e tutte le istruzioni avrebbero solo le stesse (o estremamente vicine, perché la distribuzione differirà, guarda ancora sopra il primo esempio per la colonna ctid) Ma eseguire tale "attivazione della replica" sul tavolo in una transazione biiiiiig non sarà un secondo di inattività. Potenzialmente renderà il sito di sola lettura per ore. Soprattutto se il tavolo è grossolanamente legato da FK con altri grandi tavoli.

Bene, la sola lettura non è un tempo di inattività completo. Ma in seguito cercheremo di lasciare tutti i SELECTS e alcuni INSERT, DELETE, UPDATE funzionanti (su nuovi dati, fallendo su vecchi). Lo spostamento di una tabella o di una transazione in sola lettura può essere eseguito in molti modi:potrebbe essere un approccio PostgreSQLs o un livello di applicazione o anche revocare temporaneamente le autorizzazioni in base. Questi stessi approcci possono essere un argomento per il proprio blog, quindi lo citerò solo.

Comunque. Torna ai trigger. Per eseguire la stessa azione, richiedendo di lavorare su una riga distinta (UPDATE, DELETE) su una tabella remota come fai su locale, dobbiamo utilizzare le chiavi primarie, poiché la posizione fisica sarà diversa. E le chiavi primarie vengono create su tabelle diverse con colonne diverse, quindi dobbiamo creare una funzione univoca per ogni tabella o provare a scrivere un generico. Supponiamo (per semplicità) di avere solo una colonna PK, quindi questa funzione dovrebbe aiutare. Quindi finalmente! Facciamo qui una funzione di aggiornamento. E ovviamente un trigger:

create trigger tgu before update on t for each row execute procedure tgf_u();
Scarica il whitepaper oggi Gestione e automazione di PostgreSQL con ClusterControlScopri cosa devi sapere per distribuire, monitorare, gestire e ridimensionare PostgreSQLScarica il whitepaper

E vediamo se funziona:

begin;
        update t set j = '{"updated":true}' where i = 144;
        select * from t where i = 144;
        select * from f_t where i = 144;
Rollback;

Risultato di:

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 mentre è ancora caldo, aggiungiamo anche la funzione di eliminazione del trigger e la replica:

create trigger tgd before delete on t for each row execute procedure tgf_d();

E controlla:

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

Dare:

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

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

Come ricordiamo (chi potrebbe dimenticarlo!) non stiamo trasformando il supporto di "replica" in transazione. E dovremmo se vogliamo dati coerenti. Come detto sopra, TUTTI i trigger di istruzioni su TUTTE le tabelle relative a FK devono essere abilitati in un'unica transazione, preparata in precedenza sincronizzando i dati. Altrimenti potremmo cadere in:

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;

Dare:

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;

Yayki! Abbiamo eliminato una riga sulla versione inferiore db e non su quella più recente! Solo perché non c'era. Ciò non accadrebbe se lo facessimo nel modo giusto (begin;sync;enable trigger;end;). Ma il modo giusto renderebbe le tabelle di sola lettura per molto tempo! Il lettore più irriducibile direbbe anche "perché dovresti attivare la replica basata sul trigger allora?".

Puoi farlo con pg_upgrade come farebbero le persone "normali". E in caso di replica in streaming puoi rendere tutto impostato in sola lettura. Metti in pausa la riproduzione di xlog e aggiorna il master mentre l'applicazione è ancora RO slave.

Esattamente! Non ho iniziato con questo?

La replica basata sul trigger arriva sul palco quando hai bisogno di qualcosa di molto speciale. Ad esempio, puoi provare a consentire SELECT e alcune modifiche sui dati appena creati, non solo su RO. Diciamo che hai un questionario online:l'utente si registra, risponde, ottiene il suo bonus-punti-gratis-altro-nessuno-ha-bisogno-di-grandi-cose e se ne va. Con tale struttura puoi semplicemente vietare le modifiche ai dati che non sono ancora su una versione superiore, consentendo l'intero flusso di dati per i nuovi utenti.

Quindi abbandoni pochi lavoratori ATM online, lasciando lavorare i nuovi arrivati ​​senza nemmeno accorgerti che sei nel bel mezzo di un aggiornamento. Suona orribile, ma non l'ho detto per ipotesi? non l'ho fatto? Beh, lo intendevo.

Non importa quale potrebbe essere il caso della vita reale, diamo un'occhiata a come puoi implementarlo. Le funzioni di eliminazione e aggiornamento cambieranno. E controlliamo ora l'ultimo scenario:

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

La riga non è stata eliminata sulla versione inferiore, perché non è stata trovata su quella superiore. La stessa cosa accadrebbe con aggiornato. Prova tu stesso. Ora puoi avviare la sincronizzazione dei dati senza interrompere molte modifiche alla tabella che includi nella replica basata su trigger.

È meglio? Peggio? È diverso:presenta molti difetti e alcuni vantaggi rispetto al sistema RO globale. Il mio obiettivo era dimostrare perché qualcuno avrebbe voluto utilizzare un metodo così complicato rispetto al normale - per acquisire abilità specifiche su un processo stabile e ben noto. Ad un certo costo ovviamente...

Quindi, ora che ci sentiamo un po' più sicuri per la coerenza dei dati e mentre i nostri dati preesistenti nella tabella t si sincronizzano con p10, possiamo parlare di altre tabelle. Come funzionerebbe tutto con FK (dopotutto ho menzionato FK così tanti volte, devo includerlo nel campione). Bene, perché aspettare?

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();

Vale sicuramente la pena racchiudere quei tre in una funzione con l'obiettivo di "attivare" molte tabelle. Ma non lo farò. Dato che non aggiungerò altre tabelle, il database di due relazioni di riferimento è già una rete così incasinata!

--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;

Risultato:

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)

Ancora. Sembra che la coerenza dei dati sia a posto. Puoi anche iniziare a sincronizzare i dati per la nuova tabella c...

Stanco? Sicuramente lo sono.

Conclusione

In conclusione, vorrei evidenziare alcuni errori che ho commesso esaminando questo approccio. Mentre stavo compilando la dichiarazione di aggiornamento, elencando dinamicamente tutte le colonne da pg_attribute, ho perso un'ora. Immagina quanto sono stato deluso nello scoprire in seguito che mi sono completamente dimenticato del costrutto UPDATE (list) =(list)! E la funzione è diventata molto più breve e più leggibile.

Quindi l'errore numero uno è stato:provare a costruire tutto da soli, solo perché sembra così raggiungibile. Lo è ancora, ma come sempre qualcuno probabilmente ha già fatto meglio:spendere due minuti solo per verificare se è così può davvero farti risparmiare un'ora a pensare in seguito.

E in secondo luogo, la cosa mi è sembrata molto più semplice dove si sono rivelati molto più profondi e ho complicato eccessivamente molti casi che sono perfettamente tenuti dal modello di transazione PostgreSQL.

Quindi, solo dopo aver provato a costruire la sandbox, ho avuto una comprensione alquanto chiara delle stime di questo approccio.

Quindi è ovviamente necessaria una pianificazione, ma non pianificare più di quanto puoi effettivamente fare.

L'esperienza viene con la pratica.

La mia sandbox mi ha ricordato una strategia per computer - ci si siede dopo pranzo e si pensa - "aha, qui costruisco Pyramyd, lì prendo tiro con l'arco, poi mi converto a Sons of Ra e costruisco 20 uomini con arco lungo, e qui attacco i patetici vicinato. Due ore di gloria". E IMPROVVISAMENTE ti ritrovi la mattina dopo, due ore prima del lavoro, con “Come sono arrivato qui? Perché devo firmare questa umiliante alleanza con barbari non lavati per salvare il mio ultimo uomo con l'arco lungo e ho davvero bisogno di vendere la mia piramide così costruita per questo?"

Letture:

  • 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