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

Tilpassede triggerbaserede opgraderinger til PostgreSQL

1. REGEL: Du opgraderer ikke PostgreSQL med trigger-baseret replikering
2. REGEL: Du opgraderer IKKE PostgreSQL med trigger-baseret replikering
3. REGEL: Hvis du opgraderer PostgreSQL med trigger-baseret replikering, skal du forberede dig på at lide. Og forbered dig godt.

Der må være en meget seriøs grund til ikke at bruge pg_upgrade til at opgradere PostgreSQL.

OK, lad os sige, at du ikke har råd til mere end sekunders nedetid. Brug derefter pglogical.

OK lad os sige, at du kører 9.3 og dermed ikke kan bruge pglogical. Brug Londiste.

Kan du ikke finde læsbar README? Brug SLONY.

For indviklet? Brug streaming-replikering - promover slaven og kør pg_upgrade på den - skift derefter apps til at arbejde med den nye promoverede server.

Er din app relativt skrivekrævende hele tiden? Du undersøgte alle mulige løsninger og ønsker stadig at konfigurere tilpasset triggerbaseret replikering? Der er ting, du bør være opmærksom på derefter:

  • Alle tabeller skal bruge PK. Du bør ikke stole på ctid (selv med autovakuum deaktiveret)
  • Du bliver nødt til at aktivere trigger for alle begrænsningsbundne tabeller (og kan have brug for Deferred FK)
  • Sekvenser kræver manuel synkronisering
  • Tilladelser replikeres ikke (medmindre du også konfigurerer en hændelsesudløser)
  • Hændelsesudløsere kan hjælpe med automatisering af understøttelse af nye tabeller, men det er bedre ikke at overkomplicere en allerede kompliceret proces. (som at oprette en trigger og en fremmed tabel ved tabeloprettelse, også oprette samme tabel på en fremmed server eller ændre fjernservertabel med samme ændring, som du gør på gammel db)
  • For hvert udsagn er trigger mindre pålidelig, men sandsynligvis enklere
  • Du bør levende forestille dig din allerede eksisterende datamigreringsproces
  • Du bør planlægge begrænset tilgængelighed til tabeller, mens du opsætter og aktiverer triggerbaseret replikering
  • Du bør absolut kende dine relationers afhængigheder og begrænsninger, før du går denne vej.

Nok advarsler? Vil du allerede spille? Lad os så begynde med noget kode.

Før vi skriver nogen triggere, skal vi bygge nogle mock up datasæt. Hvorfor? Ville det ikke være meget nemmere at have en trigger, før vi har data? Så dataene ville replikere til "opgraderings"-klyngen på én gang? Selvfølgelig ville det. Men hvad vil vi så opgradere? Byg blot et datasæt på en nyere version. Så ja, hvis du planlægger at opgradere til en højere version og har brug for at tilføje en tabel, oprette replikeringsudløsere, før du lægger dataene, vil det eliminere behovet for at synkronisere ikke-replikerede data senere. Men sådanne nye borde er, kan vi sige, en nem del. Så lad os først håne sagen, når vi har data, før vi beslutter os for at opgradere.

Lad os antage, at en forældet server hedder p93 (ældst understøttet), og den, vi replikerer til, hedder p10 (11 er på vej i dette kvartal, men er stadig ikke sket endnu):

\c PostgreSQLselect pg_terminate_backend(pid) fra pg_stat_activity, hvor datname in ('p93','p10'); drop database hvis eksisterer p93; drop database hvis eksisterer p10; 

Her bruger jeg psql, kan derfor bruge \c meta-kommando til at oprette forbindelse til andre db. Hvis du vil følge denne kode med en anden klient, skal du oprette forbindelse igen i stedet. Selvfølgelig behøver du ikke dette trin, hvis du kører dette for første gang. Jeg var nødt til at genskabe min sandkasse flere gange, så jeg gemte udsagn...

opret database p93; --gamle db (jeg bruger 9.3 som ældste understøttede ATM-version) opret database p10; --ny db  

Så vi laver to friske databaser. Nu vil jeg oprette forbindelse til den, vi vil opgradere, og vil oprette flere funkey datatyper og bruge dem til at udfylde en tabel, som vi vil betragte som allerede eksisterende senere:

\c p93create type myenum as enum('a', 'b');--tilføj nogle komplekse typer, opret type mycomposit som (a int, b tekst); --og igen...opret tabel t(i serial not null primærnøgle, 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'));indsæt i t (j,e) værdier ('{"b":null}', 'a'); indsæt i t (t) vælg chr(g) fra generate_series(100.240) g;--tilføj noget mere datadelete fra t hvor i> 3 og i <142; --mockup-aktivitet og blandingstupler skal ikke sekventielt indsættes i t (t) vælg null; 

Hvad har vi nu?

ctid | jeg | ts | j | t | e | c ----+-----+------------------------+-------- ---------------+-----+----+------------ (0,1) | 0 | 08-07-2018 08:03:00+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha) (0,2) | 1 | 08-07-2018 08:03:00+03 | {"b":null} | | en | (0,3) | 2 | 08-07-2018 08:03:00+03 | | d | | (0,4) | 3 | 08-07-2018 08:03:00+03 | | e | | (0.143) | 142 | 08-07-2018 08:03:00+03 | | ð | | (0.144) | 143 | 08-07-2018 08:03:00+03 | | | | (6 rækker)

OK, nogle data - hvorfor indsatte og slettede jeg så meget? Nå, vi prøver at håne et datasæt, der eksisterede i et stykke tid. Så jeg forsøger at få det spredt lidt. Lad os flytte en række mere (0,3) til slutningen af ​​siden (0,145):

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

Lad os nu antage, at vi vil bruge PostgreSQL_fdw (at bruge dblink her ville grundlæggende være det samme og sandsynligvis hurtigere for 9.3, så gør det venligst, hvis du ønsker det).

opret udvidelse PostgreSQL_fdw;opret server p10 udenlandsk dataindpakning PostgreSQL_fdw muligheder (vært 'localhost', dbnavn 'p10'); --Jeg ved, det er den samme 9.3-server - skift vært til en anden version og brug en anden klynge, hvis du ønsker det. Det er ikke vigtigt for sandkassen...opret bruger MAPPING FOR vao SERVER p10 muligheder(bruger 'vao', adgangskode 'tsun'); 

Nu kan vi bruge pg_dump -s til at få DDL, men jeg har det bare ovenfor. Vi er nødt til at oprette den samme tabel i den højere versionsklynge for at replikere data til:

\c p10create type myenum as enum('a', 'b');--tilføj nogle komplekse typer, opret type mycomposit som (a int, b tekst); --og igen...opret tabel t(i serial not null primærnøgle, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit); 

Nu vender vi tilbage til 9.3 og bruger udenlandske tabeller til datamigrering (jeg vil bruge f_ konvention for tabelnavne her, f står for fremmed):

\c p93opret fremmed tabel f_t(i seriel, ts timestamptz(0) default now(), j json, t text, e myenum, c mycomposit) server p10 muligheder (TABLE_name 't'); 

Endelig! Vi opretter en indsættelsesfunktion og trigger.

opret eller erstat funktion tgf_i() returnerer trigger som $$begin execute format('indsæt i %I select ($1).*','f_'||TG_RELNAME) ved hjælp af NEW; return NEW;end;$$ sprog plpgsql; 

Her og senere vil jeg bruge links til længere kode. For det første for at den talte tekst ikke skulle synke i maskinsprog. For det andet fordi jeg bruger flere versioner af de samme funktioner til at afspejle, hvordan koden skal udvikle sig efter behov.

--OK - første tabel klar - lad os prøve logisk trigger baseret replikering på inserts:insert into t (t) vælg 'one';--og nu transactional:begin; indsæt i t (t) vælg 'to'; vælg ctid, * fra f_t; vælg ctid, * fra t; rollback; vælg ctid, * fra f_t hvor i> 143; vælg ctid, * fra t hvor i> 143;

Resultat:

INSERT 0 1BEGININSERT 0 1 ctid | jeg | ts | j | t | e | c -------+-----+------------------------+---+-----+ ---+--- (0,1) | 144 | 08-07-2018 08:27:15+03 | | en | | (0,2) | 145 | 08-07-2018 08:27:15+03 | | to | | (2 rækker) ctid | jeg | ts | j | t | e | c ----+-----+------------------------+-------- ---------------+-----+----+------------ (0,1) | 0 | 08-07-2018 08:27:15+03 | {"a":{"aa":[1,3,2]}} | foo | b | (3,aloha) (0,2) | 1 | 08-07-2018 08:27:15+03 | {"b":null} | | en | (0,3) | 2 | 08-07-2018 08:27:15+03 | | d | | (0.143) | 142 | 08-07-2018 08:27:15+03 | | ð | | (0.144) | 143 | 08-07-2018 08:27:15+03 | | | | (0.145) | 3 | 08-07-2018 08:27:15+03 | {} | e | | (0.146) | 144 | 08-07-2018 08:27:15+03 | | en | | (0.147) | 145 | 08-07-2018 08:27:15+03 | | to | | (8 rækker)ROLLBACK ctid | jeg | ts | j | t | e | c -------+-----+------------------------+---+-----+ ---+--- (0,1) | 144 | 08-07-2018 08:27:15+03 | | en | | (1 række) ctid | jeg | ts | j | t | e | c ----+-----+------------------------+---+---- -+---+--- (0,146) | 144 | 08-07-2018 08:27:15+03 | | en | | (1 række) 

Hvad ser vi her? Vi ser, at nyligt indsatte data er replikeret til database p10 med succes. Og derfor rulles tilbage, hvis transaktionen mislykkes. Så langt så godt. Men du kunne ikke ikke bemærke (ja, ja - ikke ikke), at tabellen på p93 er meget større - gamle data kopierede ikke. Hvordan får vi det der? Godt enkelt:

indsæt i … vælg lokal.* fra ...outer join fremmed, hvor fremmed.PK er null  

ville gøre. Og dette er ikke hovedproblemet her - du bør hellere bekymre dig om, hvordan du vil administrere allerede eksisterende data om opdateringer og sletninger - fordi sætninger, der kører med succes på lavere version db, vil mislykkes eller bare påvirke nul rækker på højere - bare fordi der ikke er allerede eksisterende data ! Og her kommer vi til sekunders nedetid. (Hvis det var en film, ville vi selvfølgelig have et flashback her, men ak - hvis sætningen "sekunders nedetid" ikke fangede din opmærksomhed tidligere, bliver du nødt til at gå ovenover og lede efter sætningen...)

For at aktivere alle sætningsudløsere skal du fryse tabellen, kopiere alle data og derefter aktivere triggere, så tabeller på lavere og højere versioner af databaser ville være synkroniserede, og alle sætninger ville bare have det samme (eller ekstremt tæt på, fordi fysisk fordeling vil afvige, se igen ovenfor på det første eksempel for ctid kolonne) affekt. Men at køre sådan en "tænd replikering" på bordet i en biiiiiig transaktion vil ikke være sekunders nedetid. Det vil potentielt gøre webstedet skrivebeskyttet i timevis. Især hvis bordet er groft bundet af FK med andre store borde.

Nå, skrivebeskyttet er ikke fuldstændig nedetid. Men senere vil vi prøve at lade alle SELECTS og nogle INSERT,DELETE,UPDATE virke (på nye data, fejl på gamle). Flytning af tabel eller transaktion til skrivebeskyttet kan gøres på mange måder - ville det være en PostgreSQL-tilgang, eller applikationsniveau, eller endda midlertidigt tilbagekalde tilladelser. Disse tilgange i sig selv kan være et emne for sin egen blog, derfor vil jeg kun nævne det.

Alligevel. Tilbage til triggere. For at kunne udføre den samme handling, som kræver, at du arbejder på en særskilt række (OPDATERING, SLET) på en ekstern tabel, som du gør på lokalt, skal vi bruge primærnøgler, da den fysiske placering vil være anderledes. Og primærnøgler oprettes på forskellige tabeller med forskellige kolonner, så vi skal enten oprette en unik funktion for hver tabel eller prøve at skrive noget generisk. Lad os (for nemheds skyld) antage, at vi kun har en kolonne PK'er, så burde denne funktion hjælpe. Så endelig! Lad os få en opdateringsfunktion her. Og åbenbart en trigger:

opret trigger tgu før opdatering på t for hver række udfør procedure tgf_u(); 
Download Whitepaper Today PostgreSQL Management &Automation med ClusterControlFå flere oplysninger om, hvad du skal vide for at implementere, overvåge, administrere og skalere PostgreSQLDownload Whitepaper

Og lad os se, om det virker:

begynd; update t set j ='{"updated":true}' hvor i =144; vælg * fra t hvor i =144; vælg * fra f_t hvor i =144;Rollback; 

Resulterer til:

BEGINpsql:blog.sql:71:INFO:(144,"2018-07-08 09:09:20+03","{""updated"":true}",one,,) OPDATERING 1 i | ts | j | t | e | c -----+------------------------+------------------ +-----+---+--- 144 | 08-07-2018 09:09:20+03 | {"updated":true} | en | | (1 række) i | ts | j | t | e | c -----+------------------------+------------------ +-----+---+--- 144 | 08-07-2018 09:09:20+03 | {"updated":true} | en | | (1 række) TILBAGE 

OKAY. Og mens det stadig er varmt, lad os også tilføje delete trigger-funktion og replikering:

opret trigger tgd før sletning på t for hver række udføre proceduren tgf_d(); 

Og tjek:

begynd; slet fra t hvor i =144; vælg * fra t hvor i =144; vælg * fra f_t hvor i =144;Rollback; 

Giver:

SLET 1 i | ts | j | t | e | c ---+----+---+---+---+---(0 rækker) i | ts | j | t | e | c ---+----+---+---+---+---(0 rækker) 

Som vi husker (hvem kunne glemme dette!) vender vi ikke "replikerings"-support i transaktionen. Og det skal vi, hvis vi ønsker konsistente data. Som nævnt ovenfor skal ALLE sætningsudløsere på ALLE FK-relaterede tabeller aktiveres i én transaktion, forudgående forberedt ved at synkronisere data. Ellers kunne vi falde i:

begynd; vælg * fra t hvor i =3; slet fra t hvor i =3; vælg * fra t hvor i =3; vælg * fra f_t hvor i =3;Ruller tilbage; 

Giver:

p93=# begin;BEGINp93=# vælg * fra t hvor i =3; jeg | ts | j | t | e | c ---+------------------------+----+---+---+--- 3 | 08-07-2018 09:16:27+03 | {} | e | | (1 række)p93=# slet fra t hvor i =3;DELETE 1p93=# vælg * fra t hvor i =3; jeg | ts | j | t | e | c ---+----+---+---+---+---(0 rækker)p93=# vælg * fra f_t hvor i =3; jeg | ts | j | t | e | c ---+----+---+---+---+---(0 rækker)p93=# rollback; 

Yayki! Vi slettede en række på lavere version db og ikke på nyere! Bare fordi det ikke var der. Dette ville ikke ske, hvis vi gjorde det på den rigtige måde (begynd;synkronisering;aktiver trigger;slut;). Men den rigtige måde ville gøre tabeller skrivebeskyttet i lang tid! Den hårdeste læser ville endda sige 'hvorfor ville du overhovedet lave triggerbaseret replikering?'.

Du kan gøre det med pg_upgrade som "normale" mennesker ville. Og i tilfælde af streamingreplikering kan du gøre alt sæt skrivebeskyttet. Sæt xlog-genafspilning på pause, og opgrader master, mens applikationen stadig er RO-slaven.

Nemlig! Begyndte jeg ikke med det?

Den triggerbaserede replikation kommer på scenen, når du har brug for noget helt særligt. For eksempel kan du prøve at tillade SELECT og nogle ændringer på nyoprettede data, ikke kun RO. Lad os sige, at du har et online spørgeskema - bruger registrerer sig, svarer, får sine bonus-gratis-point-andre-ingen-bruger-fantastiske-ting og går. Med en sådan struktur kan du bare forbyde ændringer på data, der ikke er på en højere version endnu, hvilket tillader hele datastrømmen for nye brugere.

Så du forlader få online-hæveautomater, der arbejder, og lader nytilkomne arbejde uden overhovedet at bemærke, at du er midt i en opgradering. Det lyder forfærdeligt, men sagde jeg ikke hypotetisk? det gjorde jeg ikke? Nå, jeg mente det.

Lige meget hvilken sag i det virkelige liv kan være, lad os se, hvordan du kan implementere det. Slet- og opdateringsfunktionerne ændres. Og lad os tjekke det sidste scenarie nu:

BEGINpsql:blog.sql:86:FEJL:Disse data er ikke replikeret endnu, og kan derfor ikke slettes. :blog.sql:88:FEJL:aktuel transaktion er afbrudt, kommandoer ignoreret indtil slutningen af ​​transaktionen blockROLLBACK 

Rækken blev ikke slettet på den lavere version, fordi den ikke blev fundet på den højere. Det samme ville ske med opdateret. Prøv det selv. Nu kan du starte datasynkronisering uden at stoppe mange ændringer på tabellen, som du inkluderer i triggerbaseret replikering.

Er det bedre? Værre? Det er anderledes - det har mange fejl og nogle fordele i forhold til det globale RO-system. Mit mål var at demonstrere, hvorfor nogen ville ønske at bruge en så kompliceret metode frem for normal - at få specifikke evner over en stabil, velkendt proces. Til en vis pris selvfølgelig...

Så når vi nu føler os lidt mere sikre med hensyn til datakonsistens, og mens vores allerede eksisterende data i tabel t synkroniseres med p10, kan vi tale om andre tabeller. Hvordan ville det hele fungere med FK (jeg nævnte jo FK så man gange, jeg er nødt til at inkludere det i prøven). Nå, hvorfor vente?

opret tabel c (i seriel, t int refererer til t(i), x tekst);--og følgelig en fremmed tabel - den i nyere version...\c p10opret tabel c (i seriel, t int referencer t(i), x text);\c p93opret fremmed tabel f_c(i seriel, t int, x text) server p10 muligheder (TABLE_name 'c');--lad os lade som om den havde nogle data, før vi besluttede at migrere med triggere til en højere versionindsæt i c (t,x) værdier (1,'FK');--- så nu tilføjer vi triggere for at replikere DML:opret trigger tgi før indsættelse på c for hver række udfør procedure tgf_i();opret trigger tgu før opdatering på c for hver række eksekver procedure tgf_u();opret trigger tgd før sletning på c for hver række kør procedure tgf_d(); 

Det er bestemt værd at pakke disse tre sammen til en funktion med det formål at "triggerisere" mange tabeller. Men det vil jeg ikke. Da jeg ikke har tænkt mig at tilføje flere tabeller - to refererede relationsdatabaser er allerede sådan et rodet net!

--nu, hvad ville der ske, hvis vi tr indsætte refererede FK, som ikke eksisterer på remote db?..indsæt i c (t,x) værdier (2,'FK');/* det fejler med:psql:blog.sql:139:FEJL:indsæt eller opdatering på tabel "c" overtræder fremmednøglebegrænsning "c_t_fkey"en ny række er ikke indsat hverken på ekstern eller lokal db, så vi har sikker datakonsistens, men inserts er blokeret?..Ja indtil data der eksisterede indtil trigerising kommer til remote db - du kan ikke indsætte FK med før triggerisering af nøgler, endnu - en ny (både t- og c-tabeller) data vil blive accepteret:*/insert into t( i) værdier(4); --Jeg bruger gap, vi fik ved at slette data ovenfor, så jeg behøver ikke at "vende tilbage" og kende den nøjagtige id-mindre kodning i eksempel scriptinsert i c(t) værdier(4);vælg * fra c;vælg * fra f_c; 

Resultat i:

psql:blog.sql:109:FEJL:Indsæt eller opdatering af tabel "c" overtræder fremmednøglebegrænsning "c_t_fkey"DETAIL:Nøgle (t)=(2) er ikke til stede i tabel "t". KONTEKST:Ekstern SQL-kommando:INSERT INTO public.c(i, t, x) VALUES ($1, $2, $3)SQL-sætning "insert into f_c select ($1).*"PL/pgSQL-funktion tgf_i() linje 3 ved EXECUTE statementINSERT 0 1INSERT 0 1 i | t | x ---+---+---- 1 | 1 | FK 3 | 4 | (2 rækker) i | t | x ---+---+--- 3 | 4 | (1 række) 

En gang til. Det ser ud til, at datakonsistensen er på plads. Du kan også begynde at synkronisere data for ny tabel c...

Træt? Det er jeg bestemt.

Konklusion

Afslutningsvis vil jeg gerne fremhæve nogle fejl, jeg begik, mens jeg undersøgte denne tilgang. Mens jeg byggede opdateringserklæringen, dynamisk listede alle kolonner fra pg_attribute, tabte jeg en hel time. Forestil dig, hvor skuffet jeg var over senere at opdage, at jeg helt glemte UPDATE (liste) =(liste) konstruktion! Og funktionen blev meget kortere og mere læsbar.

Så fejl nummer et var - at prøve at bygge alt selv, bare fordi det ser så tilgængeligt ud. Det er det stadig, men som altid er der sikkert nogen, der har gjort det bedre - hvis du bruger to minutter på at tjekke, om det faktisk er det, kan du spare dig for en times tænketid senere.

Og for det andet - ting så meget enklere ud for mig, hvor de viste sig at være meget dybere, og jeg overkomplicerede mange sager, som er perfekt holdt af PostgreSQL-transaktionsmodellen.

Så først efter at have forsøgt at bygge sandkassen fik jeg en noget klar forståelse af denne tilgangsvurderinger.

Så planlægning er naturligvis nødvendig, men planlæg ikke mere, end du faktisk kan gøre.

Erfaring følger med øvelse.

Min sandkasse mindede mig om en computerstrategi - man sidder til den efter frokost og tænker - "aha, her bygger jeg Pyramyd, der får jeg bueskydning, så konverterer jeg til Sons of Ra og bygger 20 langbuemænd, og her angriber jeg de patetiske naboer. To timers herlighed." Og PLUDSELIG befinder du dig næste morgen, to timer før arbejde med “Hvordan kom jeg hertil? Hvorfor skal jeg underskrive denne ydmygende alliance med uvaskede barbarer for at redde min sidste langbuemand, og skal jeg virkelig sælge min så hårdtbyggede pyramide for det?”

Aflæsninger:

  • 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. Med sqlalchemy hvordan man dynamisk binder til databasemotoren på en per-anmodningsbasis

  2. Postgresql-kolonnen blev ikke fundet, men vises i beskrivelsen

  3. Hvad skal man gøre med nulværdier, når man modellerer og normaliserer?

  4. Kalder lagret procedure ved hjælp af VBA