1. PRAVIDLO: PostgreSQL neupgradujete pomocí replikace založené na spouštěči
2. PRAVIDLO: NEUPgradujete PostgreSQL replikací založenou na spouštěči
3. PRAVIDLO: Pokud upgradujete PostgreSQL s replikací založenou na spouštěči, připravte se na to, že budete trpět. A dobře se připrav.
Musí existovat velmi vážný důvod nepoužít pg_upgrade pro upgrade PostgreSQL.
Dobře, řekněme, že si nemůžete dovolit více než sekundové prostoje. Pak použijte pglogical.
Dobře, řekněme, že používáte 9.3 a nemůžete tedy používat pglogic. Použijte Londiste.
Nemůžete najít čitelný README? Použijte SLONY.
Příliš komplikované? Použijte streamovací replikaci – propagujte slave a spusťte na něm pg_upgrade – poté přepněte aplikace, aby fungovaly s novým podporovaným serverem.
Je vaše aplikace neustále poměrně náročná na zápis? Podívali jste se na všechna možná řešení a přesto chcete nastavit vlastní replikaci založenou na triggerech? Jsou věci, kterým byste měli věnovat pozornost:
- Všechny tabulky potřebují PK. Neměli byste se spoléhat na ctid (ani s deaktivovaným automatickým vysáváním)
- Budete muset povolit spouštění pro všechny tabulky vázané s omezeními (a možná budete potřebovat Odložené FK)
- Sekvence vyžadují ruční synchronizaci
- Oprávnění se nereplikují (pokud zároveň nenastavíte spouštěč události)
- Spouštěče událostí mohou pomoci s automatizací podpory nových tabulek, ale je lepší nekomplikovat již tak komplikovaný proces. (jako vytvoření spouštěče a cizí tabulky při vytváření tabulky, také vytvoření stejné tabulky na cizím serveru nebo změna tabulky vzdáleného serveru se stejnou změnou, kterou děláte na staré db)
- Pro každý příkaz je spouštěč méně spolehlivý, ale pravděpodobně jednodušší
- Měli byste si živě představit svůj již existující proces migrace dat
- Při nastavování a povolení replikace založené na spouštěči byste měli naplánovat omezenou dostupnost tabulek.
- Než se vydáte touto cestou, měli byste naprosto dokonale znát své vztahy, závislosti a omezení.
Dostatek varování? Už chcete hrát? Začněme tedy nějakým kódem.
Před napsáním jakýchkoli spouštěčů musíme vytvořit nějakou mock-up datovou sadu. Proč? Nebylo by mnohem jednodušší mít spouštěč, než budeme mít data? Takže data by se replikovala do clusteru „upgrade“ najednou? Jistě že ano. Ale co potom chceme upgradovat? Stačí vytvořit datovou sadu na novější verzi. Takže ano, pokud plánujete upgrade na vyšší verzi a potřebujete přidat nějakou tabulku, vytvořit spouštěče replikace před vložením dat, eliminuje to potřebu později synchronizovat nereplikovaná data. Ale takové nové tabulky jsou, můžeme říci, snadná část. Nejprve si tedy vyzkoušíme případ, kdy máme data, než se rozhodneme upgradovat.
Předpokládejme, že zastaralý server se nazývá p93 (nejstarší podporovaný) a ten, na který replikujeme, se nazývá p10 (11 je na cestě toto čtvrtletí, ale ještě se tak nestalo):
\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;
Zde používám psql, takže mohu použít \c meta-command pro připojení k jiné db. Pokud chcete tento kód použít s jiným klientem, budete se muset místo toho znovu připojit. Tento krok samozřejmě nepotřebujete, pokud jej spouštíte poprvé. Musel jsem svůj sandbox několikrát znovu vytvořit, a tak jsem uložil výpisy…
create database p93; --old db (I use 9.3 as oldest supported ATM version)
create database p10; --new db
Vytvoříme tedy dvě nové databáze. Nyní se připojím k tomu, který chceme upgradovat, a vytvořím několik funkey datových typů a použiji je k vyplnění tabulky, kterou později budeme považovat za již existující:
\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;
Co teď máme?
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, některá data – proč jsem jich tolik vložil a následně vymazal? No, snažíme se napodobit sadu dat, která nějakou dobu existovala. Takže se to snažím trochu rozházet. Přesuňme ještě jeden řádek (0,3) na konec stránky (0,145):
update t set j = '{}' where i =3; --(0,4)
Nyní předpokládejme, že budeme používat PostgreSQL_fdw (použití dblink zde by bylo v zásadě stejné a pravděpodobně rychlejší pro 9.3, takže pokud si to přejete, udělejte to).
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');
Nyní můžeme použít pg_dump -s k získání DDL, ale mám to jen výše. Musíme vytvořit stejnou tabulku v clusteru vyšší verze, abychom replikovali data do:
\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);
Nyní se vrátíme do 9.3 a použijeme cizí tabulky pro migraci dat (použiji f_ konvence pro názvy tabulek zde, f znamená cizí):
\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');
Konečně! Vytvoříme funkci insert a 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;
Zde a později použiji odkazy pro delší kód. Za prvé, aby mluvený text neklesal ve strojovém jazyce. Za druhé, protože používám několik verzí stejných funkcí, které odrážejí, jak by se měl kód vyvíjet na vyžádání.
--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;
Výsledek:
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)
co tady vidíme? Vidíme, že nově vložená data jsou úspěšně replikována do databáze p10. A podle toho je vrácena zpět, pokud transakce selže. Zatím je vše dobré. Ale nemohli jste si nevšimnout (ano, ano - ne), že tabulka na p93 je mnohem větší - stará data se nereplikovala. Jak to tam dostaneme? No jednoduché:
insert into … select local.* from ...outer join foreign where foreign.PK is null
by udělal. A to zde není hlavní starost - měli byste se spíše starat o to, jak budete spravovat již existující data o aktualizacích a mazáních - protože příkazy úspěšně spuštěné na nižší verzi db selžou nebo jen ovlivní nula řádků na vyšší - jen proto, že neexistují žádná dříve existující data ! A tady se dostáváme k sekundám prostoje. (Pokud by to byl film, samozřejmě bychom zde měli flashback, ale bohužel - pokud fráze „sekundy výpadku“ nezaujala vaši pozornost dříve, budete muset jít výše a hledat frázi...)
Chcete-li povolit všechny spouštěče příkazů, musíte zmrazit tabulku, zkopírovat všechna data a poté povolit spouštěče, takže tabulky v databázích nižší a vyšší verze by byly synchronizované a všechny příkazy by měly prostě stejné (nebo extrémně blízké, protože fyzické distribuce se bude lišit, znovu se podívejte výše na první příklad pro sloupec ctid) ovlivnit. Ale spuštění takového „zapnutí replikace“ na stole během jedné transakce nebude znamenat několik sekund výpadku. Potenciálně to způsobí, že web bude na hodiny pouze pro čtení. Zvláště pokud je stůl zhruba propojen FK s jinými velkými stoly.
No read-only není úplný výpadek. Později se ale pokusíme ponechat všechny SELECTy a některé INSERT, DELETE, UPDATE funkční (na nových datech, selhání na starých). Přesunutí tabulky nebo transakce do režimu pouze pro čtení lze provést mnoha způsoby – byl by to nějaký přístup PostgreSQL, nebo aplikační úroveň, nebo dokonce dočasné zrušení podle oprávnění. Tyto přístupy samy o sobě mohou být námětem pro vlastní blog, proto je pouze zmíním.
Tak jako tak. Zpět ke spouštěčům. Abychom mohli provést stejnou akci, která vyžaduje práci na odlišném řádku (UPDATE, DELETE) na vzdálené tabulce jako na místní, musíme použít primární klíče, protože fyzické umístění se bude lišit. A primární klíče se vytvářejí na různých tabulkách s různými sloupci, takže buď musíme vytvořit jedinečnou funkci pro každou tabulku, nebo zkusit napsat nějakou obecnou. Předpokládejme (pro jednoduchost), že máme pouze jeden sloupec PK, pak by tato funkce měla pomoci. Tak konečně! Udělejme zde funkci aktualizace. A samozřejmě spouštěč:
create trigger tgu before update on t for each row execute procedure tgf_u();
Stáhněte si Whitepaper Today Správa a automatizace PostgreSQL s ClusterControlZjistěte, co potřebujete vědět k nasazení, monitorování, správě a škálování PostgreSQLStáhněte si Whitepaper A uvidíme, jestli to funguje:
begin;
update t set j = '{"updated":true}' where i = 144;
select * from t where i = 144;
select * from f_t where i = 144;
Rollback;
Výsledkem je:
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. A když je to ještě horké, přidejte funkci spouštění mazání a také replikaci:
create trigger tgd before delete on t for each row execute procedure tgf_d();
A zkontrolujte:
begin;
delete from t where i = 144;
select * from t where i = 144;
select * from f_t where i = 144;
Rollback;
Dávání:
DELETE 1
i | ts | j | t | e | c
---+----+---+---+---+---
(0 rows)
i | ts | j | t | e | c
---+----+---+---+---+---
(0 rows)
Jak si pamatujeme (kdo by to mohl zapomenout!), nepřevádíme podporu „replikace“ na transakci. A měli bychom, pokud chceme konzistentní data. Jak bylo uvedeno výše, VŠECHNY spouštěče příkazů na VŠECH tabulkách souvisejících s FK by měly být povoleny v jedné transakci, předem připravené synchronizací dat. Jinak bychom mohli spadnout do:
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;
Dávání:
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! Smazali jsme řádek na nižší verzi db a ne na novější! Jen proto, že to tam nebylo. To by se nestalo, kdybychom to udělali správným způsobem (začátek;synchronizace;povolení spouštění;konec;). Ale správným způsobem by tabulky byly na dlouhou dobu pouze pro čtení! Nejnáročnější čtenář by dokonce řekl „proč byste pak vůbec dělali replikaci založenou na spouštěči?“.
Můžete to udělat pomocí pg_upgrade jako „normální“ lidé. A v případě streamingové replikace můžete vše nastavit pouze pro čtení. Pozastavte přehrávání xlogu a upgradujte master, dokud je aplikace stále RO otrokem.
Přesně tak! Nezačal jsem s tím já?
Replikace založená na spouštěči přichází na scénu, když potřebujete něco velmi speciálního. Můžete například zkusit povolit SELECT a některé úpravy nově vytvořených dat, nejen RO. Řekněme, že máte online dotazník – uživatel se zaregistruje, odpoví, dostane své bonusové body-zdarma-jiné-nikdo-nepotřebuje skvělé-věci a odejde. S takovou strukturou můžete pouze zakázat úpravy dat, která ještě nejsou na vyšší verzi, a umožnit tak celý datový tok novým uživatelům.
Takže opustíte několik lidí pracujících na online bankomatech a necháte nováčky pracovat, aniž byste si všimli, že jste uprostřed upgradu. Zní to hrozně, ale neřekl jsem to hypoteticky? já ne? No, myslel jsem to vážně.
Bez ohledu na to, jaký případ ze skutečného života by mohl být, podívejme se, jak jej můžete implementovat. Funkce mazání a aktualizace se změní. A nyní se podívejme na poslední scénář:
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
Řádek nebyl smazán na nižší verzi, protože nebyl nalezen ve vyšší verzi. Totéž by se stalo s aktualizací. Zkus to sám. Nyní můžete spustit synchronizaci dat, aniž byste museli zastavit mnoho úprav v tabulce, kterou zahrnete do replikace založené na spouštěči.
Je to lepší? Horší? Je to jiné - má mnoho nedostatků a některé výhody oproti globálnímu systému RO. Mým cílem bylo demonstrovat, proč by někdo chtěl používat tak komplikovanou metodu nad normální - získat specifické schopnosti přes stabilní, dobře známý proces. Samozřejmě za určitou cenu…
Takže teď, když se cítíme trochu bezpečněji, pokud jde o konzistenci dat, a zatímco se naše předchozí data v tabulce t synchronizují s p10, můžeme mluvit o jiných tabulkách. Jak by to celé fungovalo s FK (po tom, co jsem FK tolikrát zmínil, musím ho zařadit do ukázky). Proč čekat?
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();
Určitě se vyplatí tyto tři zabalit do funkce s cílem „spustit“ mnoho stolů. ale nebudu. Protože nebudu přidávat žádné další tabulky - dvě referenční databáze relací už jsou tak zaneřáděná síť!
--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;
Výsledek:
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)
Znovu. Zdá se, že konzistence dat je na místě. Můžete také začít synchronizovat data pro novou tabulku c…
Unavený? Rozhodně ano.
Závěr
Na závěr bych rád upozornil na některé chyby, kterých jsem se při zkoumání tohoto přístupu dopustil. Zatímco jsem sestavoval aktualizační příkaz a dynamicky vypisoval všechny sloupce z pg_attribute, ztratil jsem docela hodinu. Představte si, jak jsem byl zklamán, když jsem později zjistil, že jsem úplně zapomněl na konstrukt UPDATE (seznam) =(seznam)! A funkce byla mnohem kratší a čitelnější.
Takže chyba číslo jedna byla - snažit se vše postavit sami, jen proto, že to vypadá tak dosažitelně. Stále to tak je, ale jako vždy to už někdo pravděpodobně udělal lépe – strávit dvě minuty kontrolou, zda tomu tak skutečně je, vám může později ušetřit hodinu přemýšlení.
A za druhé – věc se mi zdála mnohem jednodušší, když se ukázalo, že jsou mnohem hlubší, a překomplikoval jsem mnoho případů, které dokonale drží transakční model PostgreSQL.
Takže až po pokusu o vybudování sandboxu jsem trochu jasně pochopil odhady tohoto přístupu.
Plánování je tedy zjevně potřeba, ale neplánujte více, než můžete skutečně udělat.
Zkušenosti přicházejí s praxí.
Moje pískoviště mi připomnělo počítačovou strategii – po obědě k němu sedíte a přemýšlíte – „aha, tady postavím Pyramyd, tam dostanu lukostřelbu, pak převedu na Sons of Ra a postavím 20 mužů s dlouhým lukem a tady zaútočím na ubohé sousedé. Dvě hodiny slávy." A NÁHLEDEM se druhý den ráno, dvě hodiny před prací ocitnete s „Jak jsem se sem dostal? Proč musím podepsat toto ponižující spojenectví s neumytými barbary, abych zachránil svého posledního muže s dlouhým lukem a opravdu za to potřebuji prodat svou tak pracně postavenou pyramidu?“
Čtení:
- 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