sql >> Database >  >> RDS >> PostgreSQL

PostgreSQL Logische Replicatie Gotchas

PostgreSQL 10 kwam met de welkome toevoeging van de logische replicatie functie. Dit biedt een flexibelere en gemakkelijkere manier om uw tabellen te repliceren dan het reguliere replicatiemechanisme voor streaming. Het heeft echter enkele beperkingen die u er al dan niet van kunnen weerhouden om het voor replicatie te gebruiken. Lees verder voor meer informatie.

Wat is logische replicatie eigenlijk?

Streaming-replicatie

Vóór v10 was de enige manier om gegevens op een server te repliceren, de wijzigingen op WAL-niveau te repliceren. Tijdens zijn werking zal een PostgreSQL-server (de primaire ) genereert een reeks WAL-bestanden. Het basisidee is om deze bestanden naar een andere PostgreSQL-server (de standby ) die deze bestanden inneemt en "afspeelt" om dezelfde wijzigingen die op de primaire server plaatsvinden opnieuw te creëren. De standby-server blijft in een alleen-lezen modus genaamd deherstelmodus , en eventuele wijzigingen aan de standby-server zijn niet toegestaan ​​(dat wil zeggen, alleen-lezen transacties zijn toegestaan).

Het proces van het verzenden van de WAL-bestanden van de primaire naar stand-by wordt logshipping genoemd , en kan handmatig worden gedaan (scripts om te rsync veranderen van primary's$PGDATA/pg_wal directory naar secundaire) of via streaming-replicatie .Verschillende functies zoals replicatieslots , stand-by-feedback en failover zijn in de loop van de tijd toegevoegd om de betrouwbaarheid en bruikbaarheid van streamingreplicatie te verbeteren.

Een groot "kenmerk" van streamingreplicatie is dat het alles of niets is. Alle wijzigingen aan alle objecten van alle databases op de primaire moeten naar de stand-by worden verzonden en de stand-by moet elke wijziging importeren. Het is niet mogelijk om selectief een deel van uw database te repliceren.

Logische replicatie

Logische replicatie , toegevoegd in v10, maakt het mogelijk om precies dat te doen:alleen een set tabellen repliceren naar andere servers. Het is het beste uit te leggen met een voorbeeld. Laten we een database nemen met de naam src in een server, en maak er een tabel in:

src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3

We gaan ook een publicatie maken in deze database (merk op dat u hiervoor superuser-privileges nodig heeft):

src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION

Laten we nu naar een database gaan dst op een andere server en maak een vergelijkbare tabel:

dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE

En we hebben nu een abonnement ingesteld hier die verbinding maakt met de publicatie op de bron en begint met het invoeren van de wijzigingen. (Merk op dat u een gebruikersrepuser nodig heeft op de bronserver met replicatierechten en leestoegang tot de tabellen.)

dst=# CREATE SUBSCRIPTION mysub CONNECTION 'user=repuser password=reppass host=127.0.0.1 port=5432 dbname=src' PUBLICATION mypub;
NOTICE:  created replication slot "mysub" on publisher
CREATE SUBSCRIPTION

De wijzigingen worden gesynchroniseerd en u kunt de rijen aan de bestemmingszijde zien:

dst=# SELECT * FROM t;
 col1 | col2 | col3
------+------+------
    1 |   10 | foo
    2 |   20 | foo
    3 |   30 | foo
(3 rows)

De bestemmingstabel heeft een extra kolom "col3", die niet wordt aangeraakt door herplicatie. De wijzigingen worden "logisch" gerepliceerd - dus zolang het mogelijk is om een ​​rij met t.col1 en t.col2 alleen in te voegen, zal het replicatieproces dat doen.

Vergeleken met streamingreplicatie is de logische replicatiefunctie perfect voor het repliceren van bijvoorbeeld een enkel schema of een reeks tabellen in een specifieke database naar een andere server.

Replicatie van schemawijzigingen

Stel dat u een Django-toepassing hebt met een reeks tabellen die in de brondatabase leven. Het is eenvoudig en efficiënt om logische replicatie in te stellen om al deze tabellen naar een andere server over te brengen, waar u rapportage, analyses, batchtaken, apps voor ontwikkelaars/klantenondersteuning en dergelijke kunt uitvoeren zonder de "echte" gegevens aan te raken en zonder de productie-app te beïnvloeden.

Mogelijk is de grootste beperking van logische replicatie momenteel dat het geen schemawijzigingen repliceert - elke DDL-opdracht die wordt uitgevoerd in de brondatabase veroorzaakt geen vergelijkbare wijziging in de doeldatabase, in tegenstelling tot streamingreplicatie. Als we dit bijvoorbeeld doen bij de brondatabase:

src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1

dit wordt vastgelegd in het bestemmingslogbestand:

ERROR:  logical replication target relation "public.t" is missing some replicated columns

en de replicatie stopt. De kolom moet op de bestemming "handmatig" worden toegevoegd, waarna de replicatie wordt hervat:

dst=# SELECT * FROM t;
 col1 | col2 | col3
------+------+------
    1 |   10 | foo
    2 |   20 | foo
    3 |   30 | foo
(3 rows)

dst=# ALTER TABLE t ADD newcol int;
ALTER TABLE
dst=# SELECT * FROM t;
 col1 | col2 | col3 | newcol
------+------+------+--------
    1 |   10 | foo  |
    2 |   20 | foo  |
    3 |   30 | foo  |
   -1 |  -10 | foo  |   -100
(4 rows)

Dit betekent dat als uw Django-toepassing een nieuwe functie heeft toegevoegd die nieuwe kolommen of tabellen nodig heeft, en u django-admin migrate moet uitvoeren op de brondatabase breekt de replicatie-setup.

Tussenoplossing

U kunt dit probleem het beste oplossen door het abonnement op de bestemming te onderbreken, eerst de bestemming te migreren, dan de bron en vervolgens het abonnement te hervatten. U kunt abonnementen als volgt onderbreken en hervatten:

-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;

-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;

Als er nieuwe tabellen worden toegevoegd en uw publicatie is niet "VOOR ALLE TABELLEN", moet u deze handmatig aan de publicatie toevoegen:

ALTER PUBLICATION mypub ADD TABLE newly_added_table;

U moet ook het abonnement aan de bestemmingszijde "vernieuwen" om Postgres te laten beginnen met het synchroniseren van de nieuwe tabellen:

dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION

Sequences

Beschouw deze tabel bij de bron, met een volgorde:

src=# CREATE TABLE s (a serial PRIMARY KEY, b text);
CREATE TABLE
src=# INSERT INTO s (b) VALUES ('foo'), ('bar'), ('baz');
INSERT 0 3
src=# SELECT * FROM s;
 a |  b
---+-----
 1 | foo
 2 | bar
 3 | baz
(3 rows)

src=# SELECT currval('s_a_seq'), nextval('s_a_seq');
 currval | nextval
---------+---------
       3 |       4
(1 row)

De reeks s_a_seq is gemaakt om de a . te ondersteunen kolom, van serial type.Dit genereert de automatisch oplopende waarden voor s.a . Laten we dit nu repliceren naar dst , en voeg nog een rij toe:

dst=# SELECT * FROM s;
 a |  b
---+-----
 1 | foo
 2 | bar
 3 | baz
(3 rows)

dst=# INSERT INTO s (b) VALUES ('foobaz');
ERROR:  duplicate key value violates unique constraint "s_pkey"
DETAIL:  Key (a)=(1) already exists.
dst=#  SELECT currval('s_a_seq'), nextval('s_a_seq');
 currval | nextval
---------+---------
       1 |       2
(1 row)

Oeps, wat is er net gebeurd? De bestemming probeerde de reeks helemaal opnieuw te starten en genereerde een waarde van 1 voor a . Dit komt omdat logische replicatie de waarden voor reeksen niet repliceert, omdat de volgende waarde van deze reeks niet in de tabel zelf wordt opgeslagen.

Tussenoplossing

Als je er logisch over nadenkt, kun je dezelfde "auto-increment"-waarde niet van twee plaatsen wijzigen zonder bidirectionele synchronisatie. Als je echt een oplopend getal in elke rij van een tabel nodig hebt en in die tabel van meerdere servers moet invoegen, kun je:

  • gebruik een externe bron voor het nummer, zoals ZooKeeper of etcd,
  • gebruik niet-overlappende bereiken - de eerste server genereert en voegt bijvoorbeeld getallen in het bereik van 1 tot 1 miljoen toe, de tweede in het bereik van 1 miljoen tot 2 miljoen, enzovoort.

Tabellen zonder unieke rijen

Laten we proberen een tabel te maken zonder een primaire sleutel en deze te repliceren:

src=# CREATE TABLE nopk (foo text);
CREATE TABLE
src=# INSERT INTO nopk VALUES ('new york');
INSERT 0 1
src=# INSERT INTO nopk VALUES ('boston');
INSERT 0 1

En de rijen staan ​​nu ook op de bestemming:

dst=# SELECT * FROM nopk;
   foo
----------
 new york
 boston
(2 rows)

Laten we nu proberen de tweede rij bij de bron te verwijderen:

src=# DELETE FROM nopk WHERE foo='boston';
ERROR:  cannot delete from table "nopk" because it does not have a replica identity and publishes deletes
HINT:  To enable deleting from the table, set REPLICA IDENTITY using ALTER TABLE.

Dit gebeurt omdat de bestemming de rij die moet worden verwijderd (of bijgewerkt) niet uniek kan identificeren zonder een primaire sleutel.

Tussenoplossing

U kunt het schema natuurlijk wijzigen om een ​​primaire sleutel op te nemen. Als je dat niet wilt, ALTER TABLE en stel de "replica-identificatie" in op de volledige rij of een unieke index. Bijvoorbeeld:

src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1

Het verwijderen is nu gelukt, en de replicatie ook:

dst=# SELECT * FROM nopk;
   foo
----------
 new york
(1 row)

Als uw tabel echt geen manier heeft om rijen uniek te identificeren, zit u vast. Zie het gedeelte REPLICA IDENTITEIT van ALTERTABLE voor meer informatie.

Anders gepartitioneerde bestemmingen

Zou het niet fijn zijn om een ​​bron te hebben die op een manier is gepartitioneerd en een bestemming op een andere manier? Zo kunnen we bij de bron voor elke maand partities bijhouden en voor elk jaar op de bestemming. Vermoedelijk is de bestemming een grotere machine, en we moeten historische gegevens bewaren, maar die gegevens hebben we zelden nodig.

Laten we een maandelijks gepartitioneerde tabel bij de bron maken:

src=# CREATE TABLE measurement (
src(#     logdate         date not null,
src(#     peaktemp        int
src(# ) PARTITION BY RANGE (logdate);
CREATE TABLE
src=#
src=# CREATE TABLE measurement_y2019m01 PARTITION OF measurement
src-# FOR VALUES FROM ('2019-01-01') TO ('2019-02-01');
CREATE TABLE
src=#
src=# CREATE TABLE measurement_y2019m02 PARTITION OF measurement
src-# FOR VALUES FROM ('2019-02-01') TO ('2019-03-01');
CREATE TABLE
src=#
src=# GRANT SELECT ON measurement, measurement_y2019m01, measurement_y2019m02 TO repuser;
GRANT

En probeer een jaarlijks gepartitioneerde tabel te maken op de bestemming:

dst=# CREATE TABLE measurement (
dst(#     logdate         date not null,
dst(#     peaktemp        int
dst(# ) PARTITION BY RANGE (logdate);
CREATE TABLE
dst=#
dst=# CREATE TABLE measurement_y2018 PARTITION OF measurement
dst-# FOR VALUES FROM ('2018-01-01') TO ('2019-01-01');
CREATE TABLE
dst=#
dst=# CREATE TABLE measurement_y2019 PARTITION OF measurement
dst-# FOR VALUES FROM ('2019-01-01') TO ('2020-01-01');
CREATE TABLE
dst=#
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ERROR:  relation "public.measurement_y2019m01" does not exist
dst=#

Postgres klaagt dat het de partitietabel nodig heeft voor januari 2019, die we niet van plan zijn te maken op de bestemming.

Dit gebeurt omdat logische replicatie niet op het niveau van de basistabel werkt, maar op het niveau van de onderliggende tabel. Hier is geen echte oplossing voor:als u partities opnieuw gebruikt, moet de partitiehiërarchie aan beide zijden van de alogische replicatie-instellingen hetzelfde zijn.

Grote objecten

Grote objecten kunnen niet worden gerepliceerd met logische replicatie. Dit is tegenwoordig waarschijnlijk niet zo erg, aangezien het opslaan van grote objecten geen alledaagse praktijk is. Het is ook gemakkelijker om een ​​verwijzing naar een groot object op een externe, redundante opslag (zoals NFS, S3 enz.) op te slaan en die verwijzing te repliceren in plaats van het object zelf op te slaan en te repliceren.


  1. .NET 4:EDMX-bestand configureren in een andere assembly in Web.Config

  2. Hoe te controleren of een database bestaat in SQL Server?

  3. Android - Betere aanpak bij het laden van SD-KAART-afbeeldingen

  4. Eenvoudige parametrering en triviale plannen - deel 2