Replikering mellom utdanningsregisteret og opptak
Vi forsøker å bruke mønsteret som er beskrevet i Referensiell integritet på tvers av subgrafer.
Denne dokumentasjonen skal beskrive hva som replikeres og hvorfor, og utvides med erfaringer underveis (inkl. teste diverse scenarioer).
Hvorfor
Opptak refererer til organisasjoner, campuser og utdanningsinstanser som eies av utdanningsregisteret. Opptak har i dag bare vedlikeholdte kopier, inkludert eksterne ID-er som ren tekst, uten fremmednøkler. Replikeringen gir opptak nøklene lokalt, slik at de kan referere dem med ekte fremmednøkler og oppnå referensiell integritet.
Hva replikeres
Utdanningsregisteret eier og publiserer data, og opptak abonnerer på disse. Foreløpig replikerer vi kun primærnøkler og navn:
| Tabell | Kolonner | Nøkkel |
|---|---|---|
organisasjon.organisasjon | organisasjonskode, navn_original | organisasjonskode |
organisasjon.campus | organisasjonskode, campuskode, navn | organisasjonskode, campuskode |
utdanning.utdanningsinstans | utdanningsinstansnr, navn_original | utdanningsinstansnr |
kodeverk.nkr | nkrkode, navn | nkrkode |
kodeverk.nus | nuskode, navn_bokmal | nuskode |
organisasjon.termin | arstall, terminkode | arstall, terminkode |
utdanning.utdanningsorganisering | utdanningsorganiseringkode, navn | utdanningsorganiseringkode |
organisasjon.termin har ingen navnekolonne, så der replikeres kun nøkkelen.
Publikasjonene settes opp hos utdanningsregisteret i fs-plattform!4900.
Testing
Oppsett
Vi baserer testing oppsettet på denne veiledningen. Vi kjører tester lokalt før vi fortsetter i test og prod.
Lokalt oppsett
Testene kjøres først lokalt i fs-plattform-repoet under fs-plattform\utdanningsregisteret modulen, med utdanningsregisterets lokale database som
publisher og en egen «opptak» abonnent i Docker:
- Publisher: utdanningsregisterets lokale database. Migreringen
v039__opptak_replikering.sqloppretter publikasjoneneopptak_organisasjon,opptak_campusogopptak_utdanningsinstansmed kolonnelister, samt nødvendige grants. - Abonnent: en egen Postgres-container
opptak-subscriberpå port 5433, på samme Docker nettverk som publisher. Init skriptet oppretter replika tabellene slik opptak skal ha dem (kun med de publiserte kolonnene) og den setter opp en abonnering mot utdanningsregisteret.
Her er to filer, en for å starte opp en abonnent container, og den andre er init scripten den trenger for å bli riktig konfigurert.
compose.yml
# Engangs lokal opptak abonnent for å verifisere logisk replikering.
# Det er nødvendig at publisher kjører med riktig migreringer (mise run db-up + mise run lb).
services:
opptak-subscriber:
image: postgres:17
container_name: opptak-subscriber
environment:
POSTGRES_PASSWORD: opptak
ports:
- "127.0.0.1:5433:5432"
volumes:
- ./init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"]
interval: 2s
timeout: 5s
retries: 30
networks:
default:
name: utdanningsregisteret_default
external: true
init/01-opptak-replika.sql
-- Replika tabeller slik opptak skal ha de, (De skal ha flere enn disse, men vi inkluderer det som en egen test i denne dokumentasjonen)
-- pluss subscription mot lokal utdanningsregisteret-publisher.
-- Kjøres automatisk ved første oppstart via docker-entrypoint-initdb.d.
-- Feiler oppstarten? Sjekk at publisher kjører og at migreringene (v039 og
-- publikasjonene for de fire nye tabellene) er applisert.
CREATE SCHEMA organisasjon;
CREATE SCHEMA utdanning;
CREATE SCHEMA kodeverk;
CREATE TABLE organisasjon.organisasjon (
organisasjonskode bigint NOT NULL PRIMARY KEY,
navn_original text NOT NULL
);
CREATE TABLE organisasjon.campus (
organisasjonskode bigint NOT NULL,
campuskode text NOT NULL,
navn text NOT NULL,
PRIMARY KEY (organisasjonskode, campuskode)
);
CREATE TABLE utdanning.utdanningsinstans (
utdanningsinstansnr bigint NOT NULL PRIMARY KEY,
navn_original text NOT NULL
);
CREATE TABLE kodeverk.nkr (
nkrkode text NOT NULL PRIMARY KEY,
navn text NOT NULL
);
CREATE TABLE kodeverk.nus (
nuskode text NOT NULL PRIMARY KEY,
navn_bokmal text NOT NULL
);
CREATE TABLE organisasjon.termin (
arstall numeric NOT NULL,
terminkode text NOT NULL,
PRIMARY KEY (arstall, terminkode)
);
CREATE TABLE utdanning.utdanningsorganisering (
utdanningsorganiseringkode text NOT NULL PRIMARY KEY,
navn text NOT NULL
);
CREATE SUBSCRIPTION utdanningsregisteret_data
CONNECTION 'host=postgresql port=5432 dbname=utdanningsregisteret user=utdanningsregisteret_local_replication password=utdanningsregisteret'
PUBLICATION opptak_organisasjon, opptak_campus, opptak_utdanningsinstans,
opptak_nkr, opptak_nus, opptak_termin, opptak_utdanningsorganisering;
IntelliJ-tilkobling til abonnenten: localhost:5433, db postgres, bruker postgres,
passord opptak.
Oppstart:
# Publisher, fra utdanningsregisteret/ i fs-plattform:
mise run db-up # Starter databasen lokalt
mise run lb # Liquibase migrering mot denne databasen
# Abonnent, fra katalogen med compose.yml (init-SQL oppretter tabeller + subscription):
docker compose up -d --wait
Man kan sjekke om replikeringen er satt opp riktig på denne måten:
I publisher databasen:
-- Forventer: en rad, slot_name = utdanningsregisteret_data, plugin = pgoutput, active = true, wal_status = reserved
SELECT slot_name, plugin, active, wal_status FROM pg_replication_slots;
I abonnent databasen:
-- Forventer: en_rad, subname = utdanningsregisteret_data, subenabled = true, subpublications = {opptak_organisasjon,opptak_campus,opptak_utdanningsinstans}
SELECT subname, subenabled, subpublications FROM pg_subscription;
-- Denne viser status på replikeringen, f.eks. hvis den har begynt med den initielle overføringen, så skal srsubstate = i, d = data blir kopiert, r = ready/klar)
SELECT srrelid::regclass AS tabell, srsubstate FROM pg_subscription_rel;
Informasjon om srsubstate er dokumentert her
catalog-pg-subscription-rel.
Riving gjøres i riktig rekkefølge. Abonnementet må fjernes først, ellers blir replikeringsslotten hengende igjen hos publisher og blokkerer neste oppstart:
DROP SUBSCRIPTION utdanningsregisteret_data;
docker compose down -v
Ble abonnenten fjernet uten DROP SUBSCRIPTION, fjernes slotten manuelt hos publisher:
SELECT pg_drop_replication_slot('utdanningsregisteret_data');
Oppsett mot testmiljøet
Mot testmiljøet brukes samme abonnent container som lokalt. Forskjellen er tilkoblingsstrengen i
CREATE SUBSCRIPTION i init skriptet: den må peke på utdanningsregisterets testdatabasen i stedet
for den lokale publisheren, med nettverkstilgang via Tailscale.
CREATE SUBSCRIPTION utdanningsregisteret_data
CONNECTION 'host=<test-host> port=5432 dbname=<dbnavn> user=<replikeringsbruker> password=<passord>'
PUBLICATION opptak_organisasjon, opptak_campus, opptak_utdanningsinstans;
Kjører du uten den lokale publisheren, må networks-blokken i compose.yml fjernes: den peker
på det lokale docker nettverket utdanningsregisteret_default, som bare finnes når den lokale
databasen kjører. Mot test går trafikken uansett via Tailscale.
Abonnementet oppretter en replikeringsslot hos testdatabasen, og den holder på WAL der så lenge
abonnenten din er utilgjengelig. Rydd alltid med DROP SUBSCRIPTION
når man er ferdig, og sjekk pg_replication_slots hos test databasen etterpå.
max_slot_wal_keep_size begrenser skaden, men en glemt slot skal ikke bli testmiljøets problem.
Diverse test caser
Initiell synkronisering
Alle rader i de tre tabellene er på plass hos opptak, og antall rader stemmer med eieren.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Etter å fulgt det over så skal man ikke trenge å gjøre noe mer.
-
Resultat: Radantallene er like hos publisher og abonnent:
-- Antall rader publisher/abonnent:select count(*) from organisasjon.organisasjon; -- 177/177select count(*) from organisasjon.campus; -- 378/378select count(*) from utdanning.utdanningsinstans; -- 31/31
Minimering
Kun data på de publiserte kolonnene overføres til opptak.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Kjør skriptet under stegvis, mot riktig datakilde per steg (publisher eller abonnent, se kommentarene):
02-minimering.sql
-- Test: Minimering-- Kun data i de publiserte kolonnene overføres til abonnenten. Verifiseres på-- tre måter: kolonnelistene stemmer med replikaenes kolonner, en UPDATE av en-- ikke-publisert kolonne hos publisher gir ingen endring hos abonnenten, og en-- ekstra kolonne hos abonnenten (med samme navn som en ikke-publisert kolonne-- hos publisher) forblir NULL. Det siste beviser at dataene faktisk ikke-- sendes, ikke bare at de mangler et sted å lande.-- ===== 1. Strukturell sjekk =====-- PUBLISHER: kolonnelistene i publikasjoneneSELECT pubname, schemaname, tablename, attnamesFROM pg_publication_tablesWHERE pubname LIKE 'opptak_%';-- ABONNENT: kolonnene i replikaene (forventet: kun nøkkel + navn)SELECT table_schema, table_name, column_nameFROM information_schema.columnsWHERE table_schema IN ('organisasjon', 'utdanning')ORDER BY table_schema, table_name, ordinal_position;-- ===== 2. Endring i ikke-publisert kolonne sendes ikke =====-- ABONNENT: legg til en kolonne som finnes hos publisher, men ikke er publisert.-- Ekstra kolonner hos abonnenten er lov og røres aldri av replikeringen.ALTER TABLE organisasjon.organisasjon ADD COLUMN url text;-- PUBLISHER: testrad med url satt fra start (sektor/land lånes fra eksisterende rad)INSERT INTO organisasjon.organisasjon (organisasjonskode, navn_original, sektorkode, landkode, url)SELECT 999902, 'Minimeringstest E2E', sektorkode, landkode, 'https://e2e.example.org'FROM organisasjon.organisasjon LIMIT 1;-- PUBLISHER: endre den ikke-publiserte kolonnenUPDATE organisasjon.organisasjonSET url = 'https://e2e2.example.org'WHERE organisasjonskode = 999902;-- ABONNENT (etter ~2 sek): raden finnes med kode + navn, og url er NULL.-- Verken insert eller update sendte url-verdien.SELECT organisasjonskode, navn_original, urlFROM organisasjon.organisasjon WHERE organisasjonskode = 999902; -
Resultat: Hos abonnenten kom raden frem med nøkkel og navn, mens
urlforbleNULLgjennom både insert og update hos publisher (som er korrekt):+-----------------+-------------------+----+|organisasjonskode|navn_original |url |+-----------------+-------------------+----+|999902 |Minimeringstest E2E|null|+-----------------+-------------------+----+
Fortløpende endringer
Insert, update og delete hos eieren oppdaterer opptak i løpet av veldig kort tid (etter noen få sekunder maks).
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Skriptet under kjøres stegvis: etter hver endring hos publisher sjekkes samme rad hos abonnenten med en select spørring.
03-fortlopende-endringer.sql
-- Test: Fortløpende endringer-- Insert, update og delete hos publisher oppdaterer abonnenten i løpet av-- veldig kort tid (noen få sekunder maks).-- ===== INSERT: organisasjon =====-- ABONNENT: forventet 0 rader før insertSELECT * FROM organisasjon.organisasjon WHERE organisasjonskode = 999903;-- PUBLISHER (sektor/land lånes fra eksisterende rad):INSERT INTO organisasjon.organisasjon (organisasjonskode, navn_original, sektorkode, landkode)SELECT 999903, 'Testhøgskolen E2E', sektorkode, landkodeFROM organisasjon.organisasjon LIMIT 1;-- ABONNENT (etter ~1-2 sek): forventet 1 radSELECT * FROM organisasjon.organisasjon WHERE organisasjonskode = 999903;-- ===== UPDATE: publisert kolonne =====-- PUBLISHER:UPDATE organisasjon.organisasjonSET navn_original = 'Testhøgskolen E2E (nytt navn)'WHERE organisasjonskode = 999903;-- ABONNENT: forventet nytt navnSELECT navn_original FROM organisasjon.organisasjon WHERE organisasjonskode = 999903;-- ===== INSERT + DELETE: campus (må henge på et eksisterende lærested) =====-- PUBLISHER:INSERT INTO organisasjon.campus (organisasjonskode, campuskode, navn, campustype_kode)SELECT l.organisasjonskode, 'E2E-TESTCAMPUS', 'Testcampus E2E',(SELECT campustype_kode FROM organisasjon.campustype LIMIT 1)FROM organisasjon.larested l LIMIT 1RETURNING organisasjonskode, campuskode;-- ABONNENT: forventet 1 radSELECT * FROM organisasjon.campus WHERE campuskode = 'E2E-TESTCAMPUS';-- PUBLISHER: slett campusen igjenDELETE FROM organisasjon.campus WHERE campuskode = 'E2E-TESTCAMPUS';-- ABONNENT: forventet 0 rader, delete replikeres ogsåSELECT * FROM organisasjon.campus WHERE campuskode = 'E2E-TESTCAMPUS'; -
Resultat: OK. Alle tre operasjonstypene replikeres, og hver endring var synlig hos abonnenten umiddelbart ved manuell sjekk:
- Insert (organisasjon): raden fantes ikke hos abonnenten før insert hos publisher, og dukket opp rett etterpå.
- Update:
navn_originalendret seg fra «Testhøgskolen E2E» til «Testhøgskolen E2E (nytt navn)» hos abonnenten. - Insert + delete (campus): raden
150, E2E-TESTCAMPUS, Testcampus E2Edukket opp, og forsvant igjen etter delete hos publisher.
Skrivebeskyttelse
Prinsippet er at replikerte data er skrivebeskyttet hos mottakeren. Hvordan den faktiske opptak databasen setter opp rollene sine, er utenfor scope (vi antar de ikke granter write, update, delete på disse replikerinsgtabellene uten videre...).
Roller med skriverettigheter (abonnementets eier, migreringsrollen, superbrukere) kan uansett ikke sperres ute, så testen dokumenterer i tillegg konsekvensene dersom noen av dem likevel endrer replikerte data. En insert som kolliderer med eieren stopper replikeringen (høylytt og synlig i overvåkningen), mens en update gir et stille avvik som først overskrives neste gang eieren endrer raden.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Kjør skriptet under stegvis, mot riktig datakilde per steg (publisher eller abonnent, se kommentarene):
04-skrivebeskyttelse.sql
-- Test: Skrivebeskyttelse-- ===== 1. Ulovlig insert stopper replikeringen =====-- ABONNENT (som superbruker): skriv en lokal rad rett inn i replikaenINSERT INTO organisasjon.organisasjon VALUES (999904, 'Lokalt påfunn E2E');-- PUBLISHER: eieren oppretter, uvitende om dette, en rad med samme nøkkelINSERT INTO organisasjon.organisasjon (organisasjonskode, navn_original, sektorkode, landkode)SELECT 999904, 'Testorganisasjon E2E', sektorkode, landkodeFROM organisasjon.organisasjon LIMIT 1;-- ABONNENT: replikeringen stopper på insert_exists-konflikt. Se telleren øke-- her, og selve konflikten i `docker logs opptak-subscriber`.-- (Viewet finnes kun hos abonnenten. Telleren er kumulativ og nullstilles-- aldri av seg selv.SELECT subname, apply_error_count FROM pg_stat_subscription_stats;-- Merk: ALLE senere endringer fra eieren står nå i kø bak konflikten.-- ABONNENT: fiks ved å fjerne den lokale raden. Replikeringen tar seg inn av-- seg selv i løpet av noen sekunder, og eierens rad dukker opp:DELETE FROM organisasjon.organisasjon WHERE organisasjonskode = 999904;SELECT * FROM organisasjon.organisasjon WHERE organisasjonskode = 999904;-- ===== 2. Ulovlig update gir stille avvik =====-- ABONNENT (som superbruker): endre den replikerte raden lokaltUPDATE organisasjon.organisasjonSET navn_original = 'Stille avvik E2E'WHERE organisasjonskode = 999904;-- ABONNENT: ingen feil noe sted. Abonnenten viser nå data eieren aldri har-- skrevet, og replikeringen går videre som om ingenting har skjedd-- (telleren står stille på verdien fra steg 1):SELECT subname, apply_error_count FROM pg_stat_subscription_stats;-- PUBLISHER: neste gang eieren endrer raden...UPDATE organisasjon.organisasjonSET navn_original = 'Testorganisasjon E2E (endret)'WHERE organisasjonskode = 999904;-- ABONNENT: ...overskrives det lokale avviket. Frem til da var avviket-- usynlig, ingen konflikt, ingen logglinje:SELECT navn_original FROM organisasjon.organisasjon WHERE organisasjonskode = 999904;-- ===== 3. Mitigering: trigger som avviser skriving lokalt =====-- Vanlige triggere fyrer for alle roller, superbrukere inkludert, men hoppes-- over av replikeringen (apply kjører med session_replication_role = replica,-- som skrur av ORIGIN-triggere). Uhell stoppes; en superbruker kan fortsatt-- bevisst skru av triggeren.-- NB: ikke opprett den som ENABLE ALWAYS, da stopper den også replikeringen.-- ABONNENT:CREATE FUNCTION organisasjon.avvis_lokale_skriv() RETURNS trigger AS $$BEGINRAISE EXCEPTION 'Replikert tabell %.%: skriving er ikke tillatt',TG_TABLE_SCHEMA, TG_TABLE_NAME;END $$ LANGUAGE plpgsql;CREATE TRIGGER avvis_lokale_skrivBEFORE INSERT OR UPDATE OR DELETE ON organisasjon.organisasjonFOR EACH ROW EXECUTE FUNCTION organisasjon.avvis_lokale_skriv();-- ABONNENT (som superbruker): avvises nå (forventet: ERROR fra triggeren)UPDATE organisasjon.organisasjonSET navn_original = 'Skal feile E2E'WHERE organisasjonskode = 999904;-- PUBLISHER: replikeringen påvirkes ikke av triggerenUPDATE organisasjon.organisasjonSET navn_original = 'Testorganisasjon E2E (endret igjen)'WHERE organisasjonskode = 999904;-- ABONNENT: eierens endring kom frem selv med triggeren påSELECT navn_original FROM organisasjon.organisasjon WHERE organisasjonskode = 999904; -
Resultat:
- Ulovlig insert: den lokale raden stoppet replikeringen da eieren satte inn samme
nøkkel.
pg_stat_subscription_statsvisteutdanningsregisteret_data, 4, altså fire feilede apply forsøk. Etter sletting av den lokale raden tok replikeringen seg inn av seg selv i løpet av kort tid, og eierens rad kom frem. - Ulovlig update: endringen ga ingen indikasjon på at noe feil har skjedd,
pg_stat_subscription_statsvisteutdanningsregisteret_data, 0(jeg nullstile databasen etter insert testen så det ikke skulle vise den gamle verdien). Replikeringen gikk videre som normalt. Da eieren senere oppdaterte samme rad, ble det lokale avviket stille overskrevet av eierens verdi.
- Ulovlig insert: den lokale raden stoppet replikeringen da eieren satte inn samme
nøkkel.
Mulig mitigering: en vanlig trigger på replika-tabellene som avviser
insert/update/delete for alle, mens
replikeringen skal gå upåvirket. Endringer som kommer fra publisher kjøres med
session_replication_role = replica, som hopper over vanlige (ORIGIN) triggere.
Triggeren må ikke opprettes som ENABLE ALWAYS, da ville den også stoppet replikeringen.
Fremmednøkler
En tabell med fremmednøkkel mot en replikert tabell avviser referanser som ikke finnes, og
godtar bare de som er gyldige. Lokalt demonstreres dette med en minitabell i
abonnent databasen. I den faktiske opptak databasen er det utdanningstilbud_v2 som får slike
fremmednøkler.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Kjør skriptet under stegvis, alt går mot abonnenten:
05-fremmednokler.sql
-- Test: Fremmednøkler-- Skriving lokalt mot en tabell med fremmednøkkel til replikaen avvises når-- referansen ikke finnes, og godtas når den finnes. Lokalt brukes en-- mini-utgave av opptaks utdanningstilbud_v2; i test/prod kjøres testen mot-- den ekte tabellen.-- ABONNENT: FK-tabell mot replikaenCREATE SCHEMA opptak_demo_fk;CREATE TABLE opptak_demo_fk.utdanningstilbud_mini (utdanningstilbudsnr int PRIMARY KEY,utdanningsinstansnr bigint NOT NULLREFERENCES utdanning.utdanningsinstans (utdanningsinstansnr),navn text NOT NULL);-- ABONNENT: gyldig referanse godtas (forventet: 1 rad)INSERT INTO opptak_demo_fk.utdanningstilbud_miniSELECT 1, utdanningsinstansnr, 'Testtilbud E2E'FROM utdanning.utdanningsinstans LIMIT 1;-- ABONNENT: ugyldig referanse avvises (forventet: ERROR fk-brudd)INSERT INTO opptak_demo_fk.utdanningstilbud_mini VALUES (2, -1, 'Skal feile E2E'); -
Resultat: Fungerte som tiltenkt. Gyldig referanse ble godtatt, og ugyldig referanse ble avvist med fremmednøkkelbrudd.
Sletting hos eieren
Sletting replikeres selv om opptaks fremmednøkler refererer til disse.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Kjør skriptet under stegvis, mot riktig datakilde per steg (publisher eller abonnent, se kommentarene):
06-sletting-hos-eieren.sql
-- Test: Sletting hos eieren-- En delete hos publisher replikeres forbi abonnentens fremmednøkler-- (apply-workeren kjører med session_replication_role = replica) og-- etterlater en ugyldig FK-rad hos abonnenten.-- PUBLISHER: opprett testinstans (klon med campuskode = NULL, jf. test 03)INSERT INTO utdanning.utdanningsinstans(organisasjonskode, campuskode, arstall_fra, terminkode_fra, arstall_til, terminkode_til,utdanningsinstanskode, navn_original, utdanningsorganiseringkode, utdanningsstatuskode,datasystemkode_kilde, utdanningsmulighetnr)SELECT organisasjonskode, NULL, arstall_fra, terminkode_fra, arstall_til, terminkode_til,utdanningsinstanskode, 'Sletteinstans E2E', utdanningsorganiseringkode,utdanningsstatuskode, datasystemkode_kilde, utdanningsmulighetnrFROM utdanning.utdanningsinstansWHERE terminkode_fra IS NOT NULLLIMIT 1RETURNING utdanningsinstansnr;-- ABONNENT (etter ~2 sek): FK-tabell som refererer testinstansenCREATE SCHEMA opptak_demo_slett;CREATE TABLE opptak_demo_slett.utdanningstilbud_mini (utdanningstilbudsnr int PRIMARY KEY,utdanningsinstansnr bigint NOT NULLREFERENCES utdanning.utdanningsinstans (utdanningsinstansnr),navn text NOT NULL);INSERT INTO opptak_demo_slett.utdanningstilbud_miniSELECT 1, utdanningsinstansnr, 'Testtilbud E2E'FROM utdanning.utdanningsinstansWHERE navn_original = 'Sletteinstans E2E';-- PUBLISHER: slett instansen som tilbudet referererDELETE FROM utdanning.utdanningsinstans WHERE navn_original = 'Sletteinstans E2E';-- ABONNENT: instansen er borte, men tilbudet står igjen uten feilmelding-- noe sted. Forventet: 1 rad med instans_finnes = false.SELECT t.utdanningstilbudsnr, t.navn,(i.utdanningsinstansnr IS NOT NULL) AS instans_finnesFROM opptak_demo_slett.utdanningstilbud_mini tLEFT JOIN utdanning.utdanningsinstans i USING (utdanningsinstansnr); -
Resultat: Som forventet. Slettingen hos publisher gikk rett igjennom uten feilmelding noe sted, og tilbudet sto igjen som med en ugyldig FK referanse hos abonnenten.
Avbrudd og gjenopptak
Etter at mottakerdatabasen har vært nede, tar replikeringen igjen etterslepet av seg selv.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Kjør skriptet under stegvis. SQL mot angitt datakilde.
07-avbrudd-og-gjenopptak.sql
-- Test: Avbrudd og gjenopptak-- Etter at abonnenten har vært nede, tar replikeringen igjen etterslepet av-- seg selv.-- 1. Terminal: stopp abonnenten (slotten står igjen hos publisher):-- docker stop opptak-subscriber-- 2. PUBLISHER: gjør en endring mens abonnenten er nedeINSERT INTO organisasjon.organisasjon (organisasjonskode, navn_original, sektorkode, landkode)SELECT 999907, 'Avbruddstest E2E', sektorkode, landkodeFROM organisasjon.organisasjon LIMIT 1;-- 3. PUBLISHER: slotten er inaktiv og etterslepet vokser med hver endringSELECT slot_name, active, wal_status,pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS etterslepFROM pg_replication_slotsWHERE slot_name = 'utdanningsregisteret_data';-- 4. Terminal: start abonnenten igjen:-- docker start opptak-subscriber-- 5. ABONNENT (etter noen sekunder): endringen fra nedetiden er kommet fremSELECT * FROM organisasjon.organisasjon WHERE organisasjonskode = 999907;-- 6. PUBLISHER: slotten er aktiv igjen og etterslepet nær nullSELECT slot_name, active, wal_status,pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS etterslepFROM pg_replication_slotsWHERE slot_name = 'utdanningsregisteret_data'; -
Resultat:
-
Abonnent nede: etter stopp av abonnenten og en insert hos publisher viste
pg_replication_slotshos publisher at slotten var inaktiv, med replication lag (etterslep):{"slot_name": "utdanningsregisteret_data","active": false,"wal_status": "reserved","etterslep": "592 bytes"}Etterslepet vokste sakte videre (1032 bytes noen minutter senere...) uten flere endringer hos publisher. Som var forventet, etterslepet måles i WAL-en, som er en felles logg over alle endringer på hele instansen, ikke bare de publiserte tabellene. Feltene i målingen er dokumentert i pg_replication_slots. For overvåkningen betyr det at etterslep på en frakoblet slot alltid kryper oppover.
-
Gjenopptak: etter restart av abonnenten (
docker start) tok replikeringen igjen etterslepet av seg selv, og slotten ble aktiv igjen:{"slot_name": "utdanningsregisteret_data","active": true,"wal_status": "reserved","etterslep": "56 bytes"}Merk at etterslepet aldri blir eksakt 0. Målingen går mot
restart_lsn, som med vilje ligger noen få bytes bak.
-
Utvidelse med nye tabeller
Det ble bestilt replikering av flere tabeller: kodeverk.nkr, kodeverk.nus,
organisasjon.termin og utdanning.utdanningsorganisering. Testen innebærer å sjekke om prosessen for å
utvide replikeringen mens den er i drift vil fungere med disse nye publikasjonene hos eieren, replika tabeller hos
opptak, og ALTER SUBSCRIPTION ... ADD PUBLICATION/REFRESH PUBLICATION hos abonnenten. De nye
tabellene skal gjøre en initiell synkronisering uten at de andre eksisterende tabellene kopieres på nytt, og uten
at løpende replikering forstyrres.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Kjør skriptet under stegvis, mot riktig datakilde per steg (publisher eller abonnent, se kommentarene). Publisher SQLen i steg 1 er utkastet til migreringen som senere skal inn i utdanningsregisteret databasen
08-utvidelse-nye-tabeller.sql
-- Test: Utvidelse med nye tabeller-- Utvider replikeringen i drift med kodeverk.nkr, kodeverk.nus,-- organisasjon.termin og utdanning.utdanningsorganisering. Verifiserer at de-- nye tabellene initialsynkroniseres uten at de eksisterende kopieres på-- nytt, og uten at løpende replikering forstyrres.-- ===== 0. Baseline =====-- ABONNENT: tre tabeller, alle 'r'.SELECT srrelid::regclass AS tabell, srsubstate FROM pg_subscription_rel ORDER BY 1;-- ===== 1. Publikasjoner og grants hos eieren =====-- PUBLISHER:GRANT USAGE ON SCHEMA kodeverk TO rds_replication;GRANT SELECT ON kodeverk.nkr, kodeverk.nus TO rds_replication;GRANT SELECT ON organisasjon.termin TO rds_replication;GRANT SELECT ON utdanning.utdanningsorganisering TO rds_replication;CREATE PUBLICATION opptak_nkrFOR TABLE kodeverk.nkr (nkrkode, navn);CREATE PUBLICATION opptak_nusFOR TABLE kodeverk.nus (nuskode, navn_bokmal);CREATE PUBLICATION opptak_terminFOR TABLE organisasjon.termin (arstall, terminkode);CREATE PUBLICATION opptak_utdanningsorganiseringFOR TABLE utdanning.utdanningsorganisering (utdanningsorganiseringkode, navn);-- ===== 2. Replika-tabeller hos abonnenten =====-- ABONNENT: må finnes før abonnementet utvidesCREATE SCHEMA kodeverk;CREATE TABLE kodeverk.nkr (nkrkode text NOT NULL PRIMARY KEY,navn text NOT NULL);CREATE TABLE kodeverk.nus (nuskode text NOT NULL PRIMARY KEY,navn_bokmal text NOT NULL);CREATE TABLE organisasjon.termin (arstall numeric NOT NULL,terminkode text NOT NULL,PRIMARY KEY (arstall, terminkode));CREATE TABLE utdanning.utdanningsorganisering (utdanningsorganiseringkode text NOT NULL PRIMARY KEY,navn text NOT NULL);-- ===== 3. Utvid abonnementet =====-- ABONNENT: ADD PUBLICATION kjører refresh med copy_data for de NYE-- tabellene; de eksisterende røres ikke.ALTER SUBSCRIPTION utdanningsregisteret_dataADD PUBLICATION opptak_nkr, opptak_nus, opptak_termin, opptak_utdanningsorganisering;-- ===== 4. Verifisering =====-- ABONNENT: nå sju rader. De fire nye går i -> d -> r, de tre gamle står-- urørt i 'r' hele tiden. (Mest sannsynlig så vil man se r med en gang siden det skal gå ganske fort)SELECT srrelid::regclass AS tabell, srsubstate FROM pg_subscription_rel ORDER BY 1;-- ABONNENT: abonnementet lister alle sju publikasjoneneSELECT subname, subpublications FROM pg_subscription;-- ABONNENT: radantall for de nye tabellene, skal matche samme spørring hos-- PUBLISHER:SELECT (SELECT count(*) FROM kodeverk.nkr) AS nkr,(SELECT count(*) FROM kodeverk.nus) AS nus,(SELECT count(*) FROM organisasjon.termin) AS termin,(SELECT count(*) FROM utdanning.utdanningsorganisering) AS organisering;-- ===== 5. Løpende replikering virker for gamle og nye tabeller =====-- PUBLISHER: kontroll på at replikeringen av de GAMLE tabellene ikke ble-- forstyrret av utvidelsen. sektorkode/landkode lånes fra en eksisterende rad-- kun for at publisher skal godta insert-en; de replikeres ikke.INSERT INTO organisasjon.organisasjon (organisasjonskode, navn_original, sektorkode, landkode)SELECT 999908, 'Utvidelsestest E2E', sektorkode, landkodeFROM organisasjon.organisasjon LIMIT 1;-- (nkrkode har domene med sjekk ^[A-ZÆØÅ.0-9_-]{1,3}$, altså maks 3 tegn,-- og eqfkode har FK til kodeverk.eqf med gyldige verdier 1-8)INSERT INTO kodeverk.nkr (nkrkode, navn, eqfkode, status_aktiv_kode)VALUES ('E2E', 'Testnivå E2E', '1', false);-- ABONNENT (etter ~2 sek): begge radene er på plass (nkr kun med nøkkel + navn)SELECT * FROM organisasjon.organisasjon WHERE organisasjonskode = 999908;SELECT * FROM kodeverk.nkr WHERE nkrkode = 'E2E'; -
Resultat: Utvidelsen fungerte som forventet:
-
Før utvidelsen viste
pg_subscription_relde tre eksisterende tabellene medsrsubstate = 'r', og etterADD PUBLICATIONsju tabeller, alle'r'. At radantallene stemte og løpende replikering var uforstyrret, tyder likevel på at kun de nye tabellene ble synkronisert, som dokumentert forADD PUBLICATION. -
pg_subscriptionlistet alle sju publikasjonene. -
Radantallene for de nye tabellene stemte med publisher (sjekket begge):
{ "nkr": 10, "nus": 5977, "termin": 747, "organisering": 3 } -
Replikering opplegget virket etterpå for både gammel og ny tabell. Insert hos publisher i
organisasjon.organisasjonogkodeverk.nkrdukket opp hos abonnenten.
-
Endring av kolonneliste
Utvidelse av kolonnelisten for en allerede publisert tabell. Abonnenten må få kolonnen migrert inn først. Verifiser at nye endringer kommer med data på den nye kolonnen, og at eksisterende rader ikke backfylles automatisk, slik at prosedyren må inkludere en resynkronisering eller backfill av tabellen.
Den nye kolonnen må legges til som nullable hos abonnenten: eksisterende rader står uten verdi (NULL) til de resynkroniseres eller oppdateres hos eieren. Etter resynkroniseringen kan den strammes til NOT NULL. Dette gjelder bare kolonner som legges til i etterkant. Replika-tabellene har ellers NOT NULL som normalt.
-
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Kjør skriptet under stegvis, mot riktig datakilde per steg (publisher eller abonnent, se kommentarene). Testen bruker
kodeverk.nkrog kolonnenstatus_aktiv_kode.09-endring-av-kolonneliste.sql
-- Test: Endring av kolonneliste-- Utvider kolonnelisten for en allerede publisert tabell (kodeverk.nkr får-- med status_aktiv_kode). Verifiserer at nye endringer bærer den nye-- kolonnen, at eksisterende rader IKKE backfylles automatisk, og at en-- kontrollert resynkronisering av tabellen fyller hullene.-- ===== 1. Kolonnen på plass hos abonnenten FØRST =====-- Mangler kolonnen hos abonnenten når en endring kommer, stopper abonnementet-- (missing replicated column) til kolonnen er lagt til.-- ABONNENT (nullable, ingen default):ALTER TABLE kodeverk.nkr ADD COLUMN status_aktiv_kode boolean;-- ===== 2. Utvid kolonnelisten hos eieren =====-- PUBLISHER:ALTER PUBLICATION opptak_nkrSET TABLE kodeverk.nkr (nkrkode, navn, status_aktiv_kode);-- ===== 3. Eksisterende rader backfylles IKKE =====-- ABONNENT: forventet at ALLE rader fortsatt har NULL i den nye kolonnenSELECT count(*) AS rader_med_null FROM kodeverk.nkr WHERE status_aktiv_kode IS NULL;-- ===== 4. Nye endringer bærer den nye kolonnen =====-- PUBLISHER:INSERT INTO kodeverk.nkr (nkrkode, navn, eqfkode, status_aktiv_kode)VALUES ('E2E', 'Testnivå E2E', '1', false);-- ABONNENT (etter ~2 sek): forventet 1 rad med status_aktiv_kode = falseSELECT * FROM kodeverk.nkr WHERE nkrkode = 'E2E';-- ===== 5. Implisitt backfill når eieren oppdaterer en rad =====-- PUBLISHER: no-op-update på en eksisterende rad sender alle publiserte kolonnerUPDATE kodeverk.nkr SET navn = navn WHERE nkrkode = '8';-- ABONNENT: raden '8' har nå fått verdi, resten står fortsatt med NULL.SELECT nkrkode, status_aktiv_kode FROM kodeverk.nkr ORDER BY nkrkode;-- ===== 6. Kontrollert resynkronisering av tabellen =====-- Med en publikasjon per tabell kan én tabell resynkroniseres uten å røre de-- andre. Drop publikasjonen fra abonnementet, tøm tabellen, ta publikasjonen inn igjen.-- (For tabeller med lokale fremmednøkler mot seg må avhengighetene håndteres-- før truncate.)-- ABONNENT:ALTER SUBSCRIPTION utdanningsregisteret_data DROP PUBLICATION opptak_nkr;TRUNCATE kodeverk.nkr;ALTER SUBSCRIPTION utdanningsregisteret_data ADD PUBLICATION opptak_nkr;-- ABONNENT (etter noen sekunder): full initiell synk med den nye kolonnen.-- Forventet: 11 rader, 0 med NULL.SELECT count(*) AS rader, count(*) FILTER (WHERE status_aktiv_kode IS NULL) AS rader_med_nullFROM kodeverk.nkr;-- ===== 7. Tilbakestilling =====-- PUBLISHER: tilbake til opprinnelig kolonnelisteALTER PUBLICATION opptak_nkr SET TABLE kodeverk.nkr (nkrkode, navn);-- ABONNENT: fjern kolonnen igjenALTER TABLE kodeverk.nkr DROP COLUMN status_aktiv_kode;-- PUBLISHER: fjern testraden; sjekker samtidig at strømmen lever etter-- tilbakestillingenDELETE FROM kodeverk.nkr WHERE nkrkode = 'E2E';-- ABONNENT: forventet 10 rader og kun nkrkode + navnSELECT count(*) FROM kodeverk.nkr;SELECT column_name FROM information_schema.columnsWHERE table_schema = 'kodeverk' AND table_name = 'nkr' ORDER BY ordinal_position; -
Resultat: Alle stegene gikk som forventet:
-
Kolonnelisten tas i bruk umiddelbart:
ALTER PUBLICATION ... SET TABLE, uten behov forREFRESH PUBLICATION. -
Rekkefølgen er viktig: en endring hos publisher før abonnenten hadde kolonnen, stopper abonnementet i en retry loop helt til kolonnen blir lagt til for abonnenten. Da tok replikeringen seg inn selv og raden kom frem. Fra abonnentens logg:
ERROR: logical replication target relation "kodeverk.nkr" is missing replicated column: "status_aktiv_kode" -
Det er ingen automatisk backfill: alle eksisterende rader sto med
NULLi den nye kolonnen. En update hos eieren på raden8ga stille, delvis backfill av kun den ene raden. -
Kontrollert resynkronisering virket:
DROP PUBLICATION+TRUNCATE+ADD PUBLICATIONfor kun denne tabellen ga full initiell synk med den nye kolonnen. 11 rader, 0 medNULL. En publikasjon per tabell gjør at man kan resynkronisere en tabell uten å røre de andre tabellene.
-
Gjenoppretting etter snapshot-restore
Replikeringskoblingen overlever ikke at en av databasene gjenopprettes fra snapshot, og må starte helt på nytt med ny synkronisering.
-
Abonnenten erstattes: abonnementet ble aldri fjernet, så publisher sitter igjen med en foreldreløs slot som holder på WAL, og ny
CREATE SUBSCRIPTIONmed samme navn feiler. Fix: fjern slotten hos publisher medpg_drop_replication_slot(replikeringsbrukeren har selv rettighetene som trengs) og abonner på nytt. -
Publisher gjenopprettes: slotten finnes ikke lenger, og replikaene hos opptak kan være foran den gjenopprettede publisheren. Prosedyre: koble fra abonnementet (
ALTER SUBSCRIPTION ... SET (slot_name = NONE)førDROP), tøm replika-tabellene og abonner på nytt. -
Oppsett: Bruk lokalt oppsett.
-
Kjøring: Første delen: "abonnenten erstattes" kjøres ved å fjerne og gjenskape abonnent containeren uten
DROP SUBSCRIPTIONførst. Den andre delen: "publisher gjenopprettes" kjøres med skriptet under. (en snapshot restore simuleres ved at slotten fjernes og publisher rulles tilbake i tid mens abonnementet er deaktivert).10-gjenoppretting-snapshot-restore.sql
-- Test: Gjenoppretting etter snapshot restore-- Simulerer at publisher er gjenopprettet fra snapshot: slotten finnes ikke-- lenger, og publisher har mistet data som abonnenten allerede har fått-- (replikaene ligger foran).-- ===== 0. Baseline =====-- PUBLISHER: test rad som blir replikert riktigINSERT INTO organisasjon.organisasjon (organisasjonskode, navn_original, sektorkode, landkode)SELECT 999909, 'Snapshottest E2E', sektorkode, landkodeFROM organisasjon.organisasjon LIMIT 1;-- ABONNENT (etter ~2 sek): raden er på plassSELECT * FROM organisasjon.organisasjon WHERE organisasjonskode = 999909;-- ===== 1. Simuler snapshot restore av publisher =====-- ABONNENT: stopp abonnementet, så slotten blir inaktiv og kan fjernesALTER SUBSCRIPTION utdanningsregisteret_data DISABLE;-- PUBLISHER: slotten forsvinner (slots overlever ikke en restore)...SELECT pg_drop_replication_slot('utdanningsregisteret_data');-- PUBLISHER: ...og publisher er "tilbake i tid": baseline-raden finnes ikke-- lenger. Slettingen replikeres ikke, siden slotten er borte.DELETE FROM organisasjon.organisasjon WHERE organisasjonskode = 999909;-- ===== 2. Feilbildet =====-- ABONNENT: slå på abonnementet igjen. Apply workeren feiler fordi-- slotten ikke finnes; se `docker logs opptak-subscriber`:-- ERROR: could not start WAL streaming: replication slot "..." does not existALTER SUBSCRIPTION utdanningsregisteret_data ENABLE;-- ABONNENT og PUBLISHER: replikaen ligger foran publisher: raden finnes i abonnenten, men ikke i publisher.SELECT * FROM organisasjon.organisasjon WHERE organisasjonskode = 999909;-- ===== 3. Runbook: reetabler koblingen =====-- ABONNENT: koble abonnementet fra den ikke eksisterende slotten og fjern det.-- SET (slot_name = NONE) gjør at DROP ikke prøver å fjerne noen slot hos-- publisher (den finnes jo ikke).ALTER SUBSCRIPTION utdanningsregisteret_data DISABLE;ALTER SUBSCRIPTION utdanningsregisteret_data SET (slot_name = NONE);DROP SUBSCRIPTION utdanningsregisteret_data;-- ABONNENT: tøm replikaene, så foreldet tilstand ikke blir liggendeTRUNCATE organisasjon.organisasjon, organisasjon.campus, organisasjon.termin,utdanning.utdanningsinstans, utdanning.utdanningsorganisering,kodeverk.nkr, kodeverk.nus;-- ABONNENT: abonner på nytt; full initiell synk, og ny slot opprettes hos publisherCREATE SUBSCRIPTION utdanningsregisteret_dataCONNECTION 'host=postgresql port=5432 dbname=utdanningsregisteret user=utdanningsregisteret_local_replication password=utdanningsregisteret'PUBLICATION opptak_organisasjon, opptak_campus, opptak_utdanningsinstans,opptak_nkr, opptak_nus, opptak_termin, opptak_utdanningsorganisering;-- ===== 4. Verifisering =====-- ABONNENT: alle sju tabellene tilbake i 'r'SELECT srrelid::regclass AS tabell, srsubstate FROM pg_subscription_rel ORDER BY 1;-- ABONNENT: raden publisher mistet er borte her også; replikaen speiler-- publisher igjen (forventet 0)SELECT count(*) FROM organisasjon.organisasjon WHERE organisasjonskode = 999909;-- PUBLISHER: ny slot, aktivSELECT slot_name, active, wal_status FROM pg_replication_slots; -
Resultat:
- Abonnenten erstattes: Abonnent containeren ble fjernet og gjenskapt uten
DROP SUBSCRIPTION. Init skriptet feilet da medreplication slot "utdanningsregisteret_data" already exists, siden publisher satt igjen med den foreldreløse slotten. Løst etter prosedyren over:pg_drop_replication_slothos publisher, deretter nytt abonnement med full initiell synkronisering, uten tap av data. - Publisher gjenopprettes: Med slotten borte feilet apply workeren (
replication slot "utdanningsregisteret_data" does not exist), og replikaen lå foran publisher: test raden fantes hos abonnenten, men ikke hos publisher. Vi reetablerte koblingen: frakobling (DISABLE,SET (slot_name = NONE),DROP SUBSCRIPTION), tømming av replikaene og nytt abonnement ga full initiell synk med alle sju tabellene tilbake i'r', den foreldreløse raden borte, og en ny aktiv slot hos publisher.
- Abonnenten erstattes: Abonnent containeren ble fjernet og gjenskapt uten
Overvåkning og varsling
Denne testen er still "work in progress", og har derfor bare fått testet noen deler av det. Men replikeringslag opplegget i grafana + varsling har blitt testet med test databasen og ser ut til å fungere.
Varsel utløses både når abonnementet stopper og når slot etterslepet vokser hos eieren. Denne testen kan bare bli gjort i testmiljøet. Vi sjekker CloudWatch metrikker, Grafana dashboards og varslinger, som ikke er noe vi får testet lokalt.
Ting som burde overvåkes:
- Hos publisher (RDS/CloudWatch):
OldestReplicationSlotLag. - Hos publisher (spørringsbasert):
pg_replication_slotsmedactive = false,wal_status != 'reserved', og etterslep viapg_wal_lsn_diff. - Hos abonnenten:
pg_stat_subscriptionogpg_subscription_rel.srsubstate(alle skal være'r').
Slik kan replikeringsetterslep se ut i praksis. Grafen under viser CloudWatch metrikken
OldestReplicationSlotLag for utdanningsregisterets dev instans, der en slot uten aktiv
konsument fikk etterslepet til å vokse jevnt til rundt 100 GB på over en uke, før slotten ble
fjernet og etterslepet falt til null:

-
Oppsett: Bruk oppsett mot testmiljøet. Krever at varsler er konfigurert riktig.
-
Kjøring:
Signalverifiserin. Provoser feiltilstandene og mål hva signalene viser. Måleverdiene er grunnlaget for tersklene i varslene:
- Test at abonnementet stopper:
ALTER SUBSCRIPTION utdanningsregisteret_data DISABLEhos abonnenten, eller mer realistisk en konfliktrad i en replikert tabell hos abonnenten. Noter hva abonnent signalene (pg_stat_subscription,pg_stat_subscription_stats) og publisher-signalene (pg_replication_slots.active) viser. - La abonnementet stå deaktivert en stund og følg
OldestReplicationSlotLagi Grafana (CloudWatch, dimensjonDBInstanceIdentifierfor test-instansen). Etterslepet vokser av seg selv med aktiviteten på instansen. Noter veksttakten. Å la det løpe helt tilmax_slot_wal_keep_size(5 GB) invaliderer slotten er valgfritt og tar tid. gjøres det, noter atwal_statusgår tillost. - Reaktiver abonnementet, og verifiser at replikeringen tar igjen etterslepet og at signalene normaliserer seg. Hold vinduet med deaktivert abonnement kort, og rydd alltid opp slotten etterpå (se advarselen i oppsettet).
- Test at abonnementet stopper:
-
Resultat:
-
Del 1, målinger: ved oppkobling av abonnement mot testdatabasen (initiell synk av rundt 200 000 rader) steg
OldestReplicationSlotLagforbigående til rundt 67 MB og falt tilbake av seg selv da synkroniseringen var ferdig. Etterslepet er transient og skalerer med skriveaktivitet på instansen ganger varigheten av synkroniseringen, ikke med datamengden. I normal drift ligger etterslepet på noen titalls bytes (aldri eksakt null). Taket ermax_slot_wal_keep_sizepå 5 GB, der slotten invalideres i stedet for å fylle disken. -
Første varselregel avdekket feil terskel: en eksisterende Grafana-regel («Ureg Dev Database Replication Lag», CloudWatch
OldestReplicationSlotLag, evaluering hvert 5. minutt, pending-periode 5 minutter) gikk tilPENDINGog deretterALERTING. Overgangen var tidsstyrt, ikke størrelsesstyrt: verdien var 56 bytes i begge evalueringene, og terskelen «over 1» tolkes i metrikkens enhet, altså 1 byte. Siden etterslep aldri er null, vil regelen alltid fyre. Dette bekrefter i praksis lærdommen fra Avbrudd og gjenopptak: varsle på vedvarende eller stor vekst, aldri på at etterslepet er større enn null. -
Terskelforslag basert på målingene: varsel ved rundt 1 GB (1073741824 bytes, godt over transienten ved resynkronisering) og kritisk ved rundt 2,5 GB (halvveis til invalideringstaket), begge med pending-periode på 10-15 minutter så en initiell synk eller kort abonnent-restart ikke varsler. Terskelfeltet i Grafana tar rå bytes; for lesbarhet kan en math-expression dele på 1024^3 så regelen kan skrives som «over 1» GB. På sikt bør regelen suppleres med et spørringsbasert varsel på
pg_replication_slots.active = falseover tid, som fanger «ingen konsument» umiddelbart. -
Provosert etterslep i praksis: grafen under er fra testkjøringen der abonnenten ble stoppet mens slotten sto igjen hos testdatabasen. Etterslepet vokser i trappetrinn (cirka 67 MB om gangen, i takt med at
restart_lsnhenger etter) mens abonnenten er nede, og faller rett til null da abonnenten startes igjen og tar igjen etterslepet. De stiplede linjene er Grafana sine markeringer av tilstandsendringer i varselregelen underveis:
-
(Del 2, varslingsverifisering med justerte terskler, er ikke kjørt ennå.)
-
Se også
- Kom i gang med logisk replikering i Postgres – praktisk innføring i mekanismen