sql >> Databasteknik >  >> RDS >> PostgreSQL

Anpassade triggerbaserade uppgraderingar för PostgreSQL

Första REGEL: Du uppgraderar inte PostgreSQL med triggerbaserad replikering
2nd REGEL: Du uppgraderar INTE PostgreSQL med triggerbaserad replikering
3:e REGEL: Om du uppgraderar PostgreSQL med triggerbaserad replikering, förbered dig på att lida. Och förbered dig väl.

Det måste finnas en mycket allvarlig anledning att inte använda pg_upgrade för att uppgradera PostgreSQL.

OK, låt oss säga att du inte har råd med mer än sekunders driftstopp. Använd pglogical då.

OK låt oss säga att du kör 9.3 och därför inte kan använda pglogical. Använd Londiste.

Hittar du inte läsbar README? Använd SLONY.

För komplicerat? Använd strömmande replikering – marknadsför slaven och kör pg_upgrade på den – byt sedan appar så att de fungerar med den nya marknadsförda servern.

Är din app relativt skrivintensiv hela tiden? Du undersökte alla möjliga lösningar och vill fortfarande ställa in anpassad triggerbaserad replikering? Det finns saker du bör vara uppmärksam på då:

  • Alla tabeller behöver PK. Du bör inte lita på ctid (även med autovakuum inaktiverat)
  • Du måste aktivera trigger för alla begränsningsbundna tabeller (och kan behöva Deferred FK)
  • Sekvenser behöver manuell synkronisering
  • Behörigheter replikeras inte (såvida du inte också ställer in en händelseutlösare)
  • Händelseutlösare kan hjälpa till med automatisering av stöd för nya tabeller, men bättre att inte överkomplicera en redan komplicerad process. (som att skapa en utlösare och en främmande tabell vid skapande av tabeller, även att skapa samma tabell på en främmande server, eller att ändra fjärrservertabell med samma ändring, som du gör på gamla db)
  • För varje påstående är trigger mindre tillförlitlig men förmodligen enklare
  • Du bör levande föreställa dig din redan existerande datamigreringsprocess
  • Du bör planera begränsad tabelltillgänglighet medan du konfigurerar och aktiverar triggerbaserad replikering
  • Du bör absolut känna till dina relationsberoenden och begränsningar innan du går den här vägen.

Tillräckligt med varningar? Vill du spela redan? Låt oss börja med lite kod då.

Innan vi skriver några triggers måste vi bygga upp en mock-up datamängd. Varför? Skulle det inte vara mycket lättare att ha en trigger innan vi har data? Så data skulle replikera till "uppgraderings"-klustret på en gång? Visst skulle det. Men vad vill vi uppgradera då? Bygg bara en datamängd på en nyare version. Så ja, om du planerar att uppgradera till en högre version och behöver lägga till någon tabell, skapa replikeringstriggers innan du lägger data, kommer det att eliminera behovet av att synkronisera ej replikerade data senare. Men sådana nya bord är, kan vi säga, en enkel del. Så låt oss först håna fallet när vi har data innan vi bestämmer oss för att uppgradera.

Låt oss anta att en föråldrad server heter p93 (äldst som stöds) och att den vi replikerar till heter p10 (11 är på väg detta kvartal, men har fortfarande inte hänt ännu):

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

Här använder jag psql, kan alltså använda \c meta-kommando för att ansluta till andra db. Om du vill följa den här koden med en annan klient måste du återansluta istället. Naturligtvis behöver du inte detta steg om du kör detta för första gången. Jag var tvungen att återskapa min sandlåda flera gånger, så jag sparade uttalanden...

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

Så vi skapar två nya databaser. Nu kommer jag att ansluta till den vi vill uppgradera och kommer att skapa flera funky datatyper och använda dem för att fylla i en tabell som vi kommer att betrakta som redan existerande senare:

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

Vad har vi nu?

  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, lite data - varför infogade och raderade jag så mycket? Tja, vi försöker håna en datamängd som funnits ett tag. Så jag försöker göra det spretigt lite. Låt oss flytta en rad till (0,3) till slutet av sidan (0,145):

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

Låt oss nu anta att vi kommer att använda PostgreSQL_fdw (att använda dblink här skulle i princip vara samma och förmodligen snabbare för 9.3, så gör det om du vill).

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

Nu kan vi använda pg_dump -s för att få DDL, men jag har bara det ovan. Vi måste skapa samma tabell i det högre versionsklustret för att replikera data till:

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

Nu går vi tillbaka till 9.3 och använder främmande tabeller för datamigrering (jag kommer att använda f_ konvention för tabellnamn här, f står för främmande):

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

Till sist! Vi skapar en infogningsfunktion och 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;

Här och senare kommer jag att använda länkar för längre kod. För det första för att talad text inte skulle sjunka i maskinspråk. För det andra eftersom jag använder flera versioner av samma funktioner för att reflektera hur koden ska utvecklas på begäran.

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

Resultat:

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)

Vad ser vi här? Vi ser att nyligen infogade data replikeras till databas p10 framgångsrikt. Och följaktligen rullas tillbaka om transaktionen misslyckas. Än så länge är allt bra. Men du kunde inte inte märka (ja, ja - inte inte) att tabellen på p93 är mycket större - gammal data replikerades inte. Hur får vi det dit? Tja enkelt:

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

skulle göra. Och det här är inte huvudproblemet här - du borde snarare oroa dig för hur du kommer att hantera befintliga data om uppdateringar och raderingar - eftersom uttalanden som körs på lägre version db kommer att misslyckas eller bara påverka noll rader på högre - bara för att det inte finns några befintliga data ! Och här kommer vi till sekundernas stilleståndsfras. (Om det var en film, här skulle vi naturligtvis ha en tillbakablick, men tyvärr - om frasen "seconds of downtime" inte fångade din uppmärksamhet tidigare, måste du gå ovanför och leta efter frasen...)

För att aktivera alla satsutlösare måste du frysa tabellen, kopiera all data och sedan aktivera triggers, så tabeller på databaser med lägre och högre versioner skulle vara synkroniserade och alla satser skulle bara ha samma (eller extremt nära, eftersom fysiska fördelningen kommer att skilja sig, se igen ovan på det första exemplet för ctid kolumn) påverka. Men att köra en sådan "slå på-replikering" på tabellen i en biiiiiig transaktion kommer inte att vara några sekunders driftstopp. Potentiellt kommer det att göra webbplatsen skrivskyddad i timmar. Speciellt om bordet är grovt bundet av FK med andra stora bord.

Tja skrivskyddad är inte fullständig driftstopp. Men senare kommer vi att försöka låta alla SELECTS och vissa INSERT,DELETE,UPDATE fungera (på nya data, misslyckas på gamla). Att flytta tabell eller transaktion till skrivskyddad kan göras på många sätt - skulle det vara någon PostgreSQLs tillvägagångssätt, eller applikationsnivå, eller till och med tillfälligt återkalla enligt behörigheter. Dessa tillvägagångssätt i sig kan vara ett ämne för sin egen blogg, så jag kommer bara att nämna det.

I alla fall. Tillbaka till triggers. För att kunna göra samma åtgärd, som kräver att du arbetar på en distinkt rad (UPPDATERA, DELETE) på fjärrtabellen som du gör på lokala måste vi använda primärnycklar, eftersom den fysiska platsen kommer att skilja sig åt. Och primärnycklar skapas på olika tabeller med olika kolumner, så vi måste antingen skapa unika funktioner för varje tabell eller försöka skriva något generiskt. Låt oss (för enkelhetens skull) anta att vi bara har en kolumn PK, då borde den här funktionen hjälpa. Så äntligen! Låt oss ha en uppdateringsfunktion här. Och uppenbarligen en trigger:

create trigger tgu before update on t for each row execute procedure tgf_u();
Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda Whitepaper

Och låt oss se om det fungerar:

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

Resulterar till:

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. Och medan det fortfarande är varmt, låt oss lägga till raderingsutlösarfunktion och replikering också:

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

Och kontrollera:

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

Ge:

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

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

Som vi minns (vem kunde glömma detta!) vänder vi inte till "replikeringsstöd" i transaktionen. Och det borde vi göra om vi vill ha konsekventa data. Som nämnts ovan bör ALLA satsutlösare på ALLA FK-relaterade tabeller aktiveras i en transaktion, i förväg förberedd genom att synkronisera data. Annars kan vi hamna i:

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;

Ge:

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! Vi tog bort en rad på lägre version db och inte på nyare! Bara för att det inte fanns där. Detta skulle inte hända om vi gjorde det på rätt sätt (start;sync;enable trigger;slut;). Men det rätta sättet skulle göra tabeller skrivskyddade under lång tid! Den mest hårda läsaren skulle till och med säga "varför skulle du göra triggerbaserad replikering överhuvudtaget då?".

Du kan göra det med pg_upgrade som "normala" människor skulle göra. Och vid strömmande replikering kan du göra alla uppsättningar skrivskyddade. Pausa xlog-replay och uppgradera master medan applikationen fortfarande är RO-slaven.

Exakt! Började jag inte med det?

Den triggerbaserade replikeringen kommer på scenen när du behöver något väldigt speciellt. Du kan till exempel försöka tillåta SELECT och viss modifiering på nyskapade data, inte bara RO. Låt oss säga att du har ett frågeformulär online - användaren registrerar sig, svarar, får sina bonus-fria-poäng-andra-ingen-behöver-bra-grejer och lämnar. Med en sådan struktur kan du bara förbjuda ändringar av data som inte finns i en högre version ännu, vilket tillåter hela dataflödet för nya användare.

Så du överger ett fåtal online-bankomater som arbetar och låter nykomlingar arbeta utan att ens märka att du är mitt uppe i en uppgradering. Låter hemskt, men sa jag inte hypotetiskt? det gjorde jag inte? Tja, jag menade det.

Oavsett vilket fall i verkligheten kan vara, låt oss titta på hur du kan implementera det. Raderings- och uppdateringsfunktionerna kommer att ändras. Och låt oss kolla det sista scenariot nu:

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

Raden raderades inte på den lägre versionen, eftersom den inte hittades på den högre. Samma sak skulle hända med uppdaterad. Prova själv. Nu kan du starta datasynkronisering utan att stoppa många ändringar av tabellen som du inkluderar i triggerbaserad replikering.

Är det bättre? Värre? Det är annorlunda - det har många brister och vissa fördelar jämfört med globala RO-system. Mitt mål var att visa varför någon skulle vilja använda en så komplicerad metod över normala - att få specifika förmågor över en stabil, välkänd process. Till viss kostnad förstås...

Så nu när vi känner oss lite säkrare för datakonsistens och medan vår befintliga data i tabell t synkroniseras med p10, kan vi prata om andra tabeller. Hur skulle det hela fungera med FK (jag nämnde trots allt FK så man gånger, jag måste ta med det i provet). Tja, varför vänta?

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

Det är verkligen värt att slå in dessa tre till en funktion med målet att "trigga" många tabeller. Men jag kommer inte. Eftersom jag inte tänker lägga till fler tabeller - databaser med två refererade relationer är redan ett så rörigt nät!

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

Resultat i:

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)

På nytt. Det verkar som att datakonsistensen är på plats. Du kan också börja synkronisera data för ny tabell c...

Trött? Det är jag definitivt.

Slutsats

Avslutningsvis skulle jag vilja lyfta fram några misstag jag gjorde när jag undersökte detta tillvägagångssätt. Medan jag byggde upp uppdateringssatsen, dynamiskt listade alla kolumner från pg_attribute, förlorade jag en hel timme. Föreställ dig hur besviken jag blev när jag senare upptäckte att jag helt glömde bort UPDATE (lista) =(lista) konstruktion! Och funktionen blev mycket kortare och mer läsbar.

Så misstag nummer ett var - att försöka bygga allt själv, bara för att det ser så lättillgängligt ut. Det är det fortfarande, men som alltid har någon förmodligen redan gjort det bättre - att spendera två minuter bara för att kontrollera om det verkligen är så kan spara dig en timmes funderande senare.

Och för det andra - saken såg mycket enklare ut för mig där de visade sig vara mycket djupare, och jag överkomplicerade många fall som hålls perfekt av PostgreSQL-transaktionsmodellen.

Så först efter att ha försökt bygga sandlådan fick jag en ganska klar förståelse för uppskattningarna av denna metod.

Så planering behövs naturligtvis, men planera inte mer än du faktiskt kan göra.

Erfarenhet kommer med övning.

Min sandlåda påminde mig om en datorstrategi - du sitter vid den efter lunch och tänker - "aha, här bygger jag Pyramyd, där får jag bågskytte, sedan konverterar jag till Sons of Ra och bygger 20 långbågsmän, och här attackerar jag de patetiska grannar. Två timmar av ära.” Och plötsligt befinner du dig nästa morgon, två timmar innan jobbet med ”Hur kom jag hit? Varför måste jag skriva på denna förödmjukande allians med icke tvättade barbarer för att rädda min sista långbåge och behöver jag verkligen sälja min så hårt byggda pyramid för den?”

Läsningar:

  • 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

  1. MSSQL-fel "Den underliggande leverantören misslyckades vid Open"

  2. java.lang.NoSuchFieldError:NONE i viloläge med Spring 3, maven, JPA, c3p0

  3. Använd inte sp_depends i SQL Server (den är utfasad)

  4. T-SQL få antal arbetsdagar mellan 2 datum