sql >> Database >  >> RDS >> PostgreSQL

Aangepaste op triggers gebaseerde upgrades voor PostgreSQL

1e REGEL: U voert geen upgrade uit van PostgreSQL met op triggers gebaseerde replicatie
2e REGEL: U voert GEEN upgrade uit van PostgreSQL met op triggers gebaseerde replicatie
3e REGEL: Als u PostgreSQL upgradet met op triggers gebaseerde replicatie, bereid u dan voor om te lijden. En bereid je goed voor.

Er moet een zeer serieuze reden zijn om de pg_upgrade niet te gebruiken voor het upgraden van de PostgreSQL.

Oké, laten we zeggen dat je je niet meer dan seconden downtime kunt veroorloven. Gebruik dan pglogical.

OK, laten we zeggen dat je 9.3 draait en dus pglogical niet kunt gebruiken. Gebruik Londiste.

Kunt u geen leesbare README vinden? Gebruik SLONY.

Te ingewikkeld? Gebruik streaming-replicatie - promoot de slaaf en voer pg_upgrade erop uit - en schakel vervolgens tussen apps om met de nieuwe gepromote server te werken.

Uw app is altijd relatief schrijfintensief? U heeft alle mogelijke oplossingen bekeken en wilt toch een aangepaste, op triggers gebaseerde replicatie instellen? Er zijn dingen waar u dan op moet letten:

  • Alle tabellen hebben PK nodig. U moet niet vertrouwen op ctid (zelfs als autovacuüm is uitgeschakeld)
  • U moet trigger inschakelen voor alle tabellen met gebonden beperkingen (en mogelijk uitgestelde FK nodig)
  • Sequenties moeten handmatig worden gesynchroniseerd
  • Machtigingen worden niet gerepliceerd (tenzij je ook een gebeurtenistrigger instelt)
  • Event-triggers kunnen helpen bij het automatiseren van ondersteuning voor nieuwe tabellen, maar het is beter om een ​​al gecompliceerd proces niet te ingewikkeld te maken. (zoals het maken van een trigger en een vreemde tabel bij het maken van tabellen, ook het maken van dezelfde tabel op een buitenlandse server, of het wijzigen van een externe servertabel met dezelfde wijziging, doe je op oude db)
  • Voor elke instructie is de trigger minder betrouwbaar, maar waarschijnlijk eenvoudiger
  • U moet zich uw bestaande gegevensmigratieproces levendig voorstellen
  • U moet een beperkte toegankelijkheid van tabellen plannen terwijl u op triggers gebaseerde replicatie instelt en inschakelt
  • Je moet absoluut de afhankelijkheden en beperkingen van je relaties kennen voordat je deze kant op gaat.

Genoeg waarschuwingen? Wil je al spelen? Laten we dan beginnen met wat code.

Voordat we triggers schrijven, moeten we een mock-up dataset bouwen. Waarom? Zou het niet veel gemakkelijker zijn om een ​​trigger te hebben voordat we gegevens hebben? Dus de gegevens zouden in één keer naar het "upgrade" -cluster worden gerepliceerd? Natuurlijk zou het. Maar wat willen we dan upgraden? Bouw gewoon een dataset op een nieuwere versie. Dus ja, als u van plan bent te upgraden naar een hogere versie en een tabel moet toevoegen, replicatietriggers moet maken voordat u de gegevens plaatst, is het niet langer nodig om niet-gerepliceerde gegevens later te synchroniseren. Maar zulke nieuwe tabellen zijn, kunnen we zeggen, een gemakkelijk onderdeel. Laten we dus eerst de casus bekijken wanneer we gegevens hebben voordat we besluiten te upgraden.

Laten we aannemen dat een verouderde server p93 heet (de oudste ondersteunde) en dat degene waarnaar we repliceren p10 heet (11 is onderweg dit kwartaal, maar is nog niet gebeurd):

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

Hier gebruik ik psql, dus kan \c meta-commando gebruiken om verbinding te maken met andere db. Als u deze code met een andere client wilt volgen, moet u in plaats daarvan opnieuw verbinding maken. Natuurlijk heb je deze stap niet nodig als je dit voor de eerste keer uitvoert. Ik moest mijn sandbox verschillende keren opnieuw maken, dus heb ik verklaringen opgeslagen ...

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

Dus maken we twee nieuwe databases. Nu zal ik verbinding maken met degene die we willen upgraden en zal ik verschillende funkey-gegevenstypen maken en deze gebruiken om een ​​tabel in te vullen die we later als reeds bestaand beschouwen:

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

Wat hebben we 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, wat gegevens - waarom heb ik zoveel ingevoegd en vervolgens verwijderd? Welnu, we proberen een dataset na te bootsen die al een tijdje bestaat. Dus ik probeer het een beetje verspreid te maken. Laten we nog een rij (0,3) naar het einde van de pagina (0,145) verplaatsen:

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

Laten we nu aannemen dat we PostgreSQL_fdw zullen gebruiken (het gebruik van dblink hier zou in principe hetzelfde zijn en waarschijnlijk sneller voor 9.3, dus doe dit alstublieft als u dat wenst).

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 kunnen we pg_dump -s gebruiken om de DDL te krijgen, maar ik heb het hierboven. We moeten dezelfde tabel maken in de hogere versiecluster om gegevens te repliceren naar:

\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 gaan we terug naar 9.3 en gebruiken buitenlandse tabellen voor gegevensmigratie (ik gebruik f_ conventie voor tabelnamen hier, f staat voor buitenlands):

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

Tenslotte! We creëren een invoegfunctie en 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;

Hier en later zal ik links gebruiken voor langere code. Ten eerste, zodat gesproken tekst niet wegzakt in machinetaal. Ten tweede omdat ik verschillende versies van dezelfde functies gebruik om aan te geven hoe de code op verzoek moet evolueren.

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

Resultaat:

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)

Wat zien we hier? We zien dat nieuw ingevoegde gegevens met succes worden gerepliceerd naar database p10. En dienovereenkomstig wordt teruggedraaid als de transactie mislukt. Tot zover goed. Maar je kon niet niet merken (ja, ja - niet niet) dat de tabel op p93 veel groter is - oude gegevens werden niet gerepliceerd. Hoe krijgen we het daar? Nou simpel:

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

zou doen. En dit is hier niet de grootste zorg - u moet zich liever zorgen maken over het beheren van reeds bestaande gegevens over updates en verwijderingen - omdat instructies die met succes op een lagere versie db worden uitgevoerd, zullen mislukken of gewoon nul rijen op hogere zullen beïnvloeden - alleen omdat er geen reeds bestaande gegevens zijn ! En hier komen we bij de seconden van downtime-zin. (Als het een film was, zouden we hier natuurlijk een flashback hebben, maar helaas - als de zin "seconden downtime" niet eerder je aandacht trok, moet je naar boven gaan en de zin zoeken...)

Om alle triggers voor instructies in te schakelen, moet u de tabel bevriezen, alle gegevens kopiëren en vervolgens triggers inschakelen, zodat tabellen op databases met een lagere en hogere versie synchroon zouden zijn en alle instructies hetzelfde zouden hebben (of extreem dichtbij, omdat fysieke distributie zal verschillen, kijk nogmaals hierboven naar het eerste voorbeeld voor ctid-kolom) beïnvloeden. Maar het uitvoeren van een dergelijke "replicatie inschakelen" op tafel in één biiiiiig-transactie zal geen seconden downtime zijn. Mogelijk maakt het de site urenlang alleen-lezen. Vooral als de tafel grofweg door FK is verbonden met andere grote tafels.

Goed alleen-lezen is geen volledige downtime. Maar later zullen we proberen alle SELECTS en sommige INSERT, DELETE, UPDATE werkend te laten (op nieuwe gegevens, mislukt op oude). Het verplaatsen van een tabel of transactie naar alleen-lezen kan op veel manieren worden gedaan - zou het een PostgreSQL-benadering zijn, of applicatieniveau, of zelfs tijdelijk intrekken overeenkomstig de machtigingen. Deze benaderingen zelf kunnen een onderwerp zijn voor een eigen blog, dus ik zal het alleen vermelden.

In ieder geval. Terug naar triggers. Om dezelfde actie uit te voeren, waarbij aan een aparte rij (UPDATE, DELETE) op een externe tafel moet worden gewerkt als op een lokale tafel, moeten we primaire sleutels gebruiken, omdat de fysieke locatie zal verschillen. En primaire sleutels worden gemaakt op verschillende tabellen met verschillende kolommen, dus we moeten ofwel een unieke functie voor elke tabel creëren of proberen een generieke functie te schrijven. Laten we (voor de eenvoud) aannemen dat we maar één kolom PK's hebben, dan zou deze functie moeten helpen. Eindelijk dus! Laten we hier een update-functie hebben. En natuurlijk een trigger:

create trigger tgu before update on t for each row execute procedure tgf_u();
Download de whitepaper vandaag PostgreSQL-beheer en -automatisering met ClusterControlLees wat u moet weten om PostgreSQL te implementeren, bewaken, beheren en schalenDownload de whitepaper

En eens kijken of het werkt:

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

Met als resultaat:

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É. En terwijl het nog steeds hot is, laten we ook de verwijder-triggerfunctie en replicatie toevoegen:

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

En controleer:

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

Geven:

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

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

Zoals we ons herinneren (wie kan dit vergeten!) veranderen we "replicatie" -ondersteuning niet in een transactie. En dat zouden we moeten doen als we consistente gegevens willen. Zoals hierboven vermeld, moeten ALLE triggers voor instructies op ALLE FK-gerelateerde tabellen worden ingeschakeld in één transactie, vooraf voorbereid door gegevens te synchroniseren. Anders zouden we kunnen vallen 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;

Geven:

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! We hebben een rij verwijderd op een lagere versie db en niet op een nieuwere! Gewoon omdat het er niet was. Dit zou niet gebeuren als we het op de juiste manier zouden doen (begin;synchronisatie;trigger inschakelen;einde;). Maar de juiste manier zou tabellen lange tijd alleen-lezen maken! De meest doorgewinterde lezer zou zelfs zeggen 'waarom zou je dan überhaupt op triggers gebaseerde replicatie doen?'.

Je kunt het met pg_upgrade doen zoals "normale" mensen zouden doen. En in het geval van streaming-replicatie kunt u alle sets alleen-lezen maken. Pauzeer xlog-replay en upgrade master terwijl de applicatie nog steeds RO de slave is.

Precies! Ben ik er niet mee begonnen?

De op triggers gebaseerde replicatie komt op het podium wanneer je iets heel speciaals nodig hebt. U kunt bijvoorbeeld proberen SELECT en enige wijziging toe te staan ​​op nieuw gemaakte gegevens, niet alleen op RO. Laten we zeggen dat je een online vragenlijst hebt - gebruiker registreert, beantwoordt, krijgt zijn bonus-vrije-punten-anders-niemand-heeft-geweldige-dingen en vertrekt. Met zo'n structuur kun je alleen wijzigingen verbieden aan gegevens die nog niet in een hogere versie zijn, waardoor de hele gegevensstroom voor nieuwe gebruikers mogelijk is.

U laat dus weinig online ATM-werkende mensen in de steek en laat nieuwkomers werken zonder zelfs maar te merken dat u midden in een upgrade zit. Klinkt vreselijk, maar zei ik niet hypothetisch? ik niet? Nou, ik meende het.

Het maakt niet uit wat het echte geval zou kunnen zijn, laten we eens kijken hoe u het kunt implementeren. De verwijder- en updatefuncties zullen veranderen. En laten we nu het laatste scenario bekijken:

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

De rij is niet verwijderd in de lagere versie, omdat deze niet werd gevonden in de hogere. Hetzelfde zou gebeuren met bijgewerkt. Probeer het zelf. Nu kunt u gegevenssynchronisatie starten zonder veel wijzigingen aan de tabel te stoppen die u opneemt in op triggers gebaseerde replicatie.

Is het beter? Slechter? Het is anders - het heeft veel gebreken en enkele voordelen ten opzichte van het wereldwijde RO-systeem. Mijn doel was om aan te tonen waarom iemand zo'n gecompliceerde methode zou willen gebruiken in plaats van normaal - om specifieke vaardigheden te krijgen via een stabiel, bekend proces. Tegen een bepaalde prijs natuurlijk…

Dus nu we ons een beetje veiliger voelen voor gegevensconsistentie en terwijl onze reeds bestaande gegevens in tabel t worden gesynchroniseerd met p10, kunnen we praten over andere tabellen. Hoe zou het allemaal werken met FK (ik heb FK tenslotte al genoemd, dus ik moet het in het voorbeeld opnemen). Nou, waarom wachten?

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

Het is zeker de moeite waard om die drie samen te voegen tot een functie met als doel veel tabellen te "triggeren". Maar dat zal ik niet doen. Aangezien ik geen tabellen meer ga toevoegen, is de database met twee relaties waarnaar wordt verwezen al zo'n rommelig netwerk!

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

Resultaat in:

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)

Nog een keer. Het lijkt erop dat de gegevensconsistentie aanwezig is. U kunt ook beginnen met het synchroniseren van gegevens voor nieuwe tabel c...

Moe? Dat ben ik zeker.

Conclusie

Tot slot wil ik enkele fouten benadrukken die ik heb gemaakt bij het onderzoeken van deze benadering. Terwijl ik de update-instructie aan het maken was, waarbij alle kolommen van pg_attribute dynamisch werden weergegeven, verloor ik behoorlijk wat uur. Stel je voor hoe teleurgesteld ik was om later te ontdekken dat ik UPDATE (lijst) =(lijst) constructie volledig was vergeten! En de functie werd veel korter en beter leesbaar.

Dus fout nummer één was - alles zelf proberen te bouwen, gewoon omdat het zo bereikbaar lijkt. Dat is het nog steeds, maar zoals altijd heeft iemand het waarschijnlijk al beter gedaan - twee minuten besteden om te controleren of het inderdaad zo is, kan je later een uur nadenken besparen.

En ten tweede leek het mij veel eenvoudiger, waar ze veel dieper bleken te zijn, en ik heb veel gevallen te ingewikkeld gemaakt die perfect worden beheerd door het PostgreSQL-transactiemodel.

Dus pas nadat ik had geprobeerd de sandbox te bouwen, kreeg ik een enigszins duidelijk begrip van deze benaderingsschattingen.

Plannen is dus duidelijk nodig, maar plan niet meer dan u daadwerkelijk kunt doen.

Ervaring komt met oefening.

Mijn zandbak deed me denken aan een computerstrategie - je zit er na de lunch voor en denkt - "aha, hier bouw ik Pyramyd, daar krijg ik boogschieten, dan bekeer ik me tot Sons of Ra en bouw 20 handboogmannen, en hier val ik de zielige buren. Twee uur glorie.” En PLOTSELING sta je de volgende ochtend, twee uur voor je werk, met 'Hoe ben ik hier gekomen? Waarom moet ik deze vernederende alliantie met ongewassen barbaren ondertekenen om mijn laatste handboogman te redden en moet ik er echt mijn zo hard gebouwde Pyramide voor verkopen?”

Lezingen:

  • 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. PostgreSQL:tabel maken als deze niet bestaat AS

  2. Genereer willekeurige int-waarde van 3 tot 6

  3. Hoe een set tekens op te halen met SUBSTRING in SQL?

  4. Prestaties van sys.partitions