PostgreSQL 10には、論理レプリケーションが追加されました。 特徴。これにより、通常のストリーミングレプリケーションメカニズムよりも柔軟で簡単にテーブルをレプリケートできます。ただし、複製に使用することを妨げる場合と妨げない場合があるいくつかの制限があります。詳細については、以下をお読みください。
とにかく論理レプリケーションとは何ですか?
v10より前は、サーバーに存在するデータを複製する唯一の方法は、WALレベルで変更を複製することでした。運用中、PostgreSQLサーバー(プライマリ )一連のWALファイルを生成します。基本的な考え方は、これらのファイルを別のPostgreSQLサーバー(スタンバイ)に転送することです。 )これらのファイルを取り込んで「再生」し、プライマリサーバーで発生しているのと同じ変更を再現します。スタンバイサーバーは、リカバリモードと呼ばれる読み取り専用モードのままです。 、およびスタンバイサーバーへの変更はありません 許可されます(つまり、読み取り専用トランザクションのみが許可されます)。
WALファイルをプライマリからスタンバイに転送するプロセスはログシップと呼ばれます。 、手動で実行できます(プライマリの$PGDATA/pg_wal
からのrsync変更へのスクリプト ディレクトリからセカンダリへ)またはストリーミングレプリケーション 。レプリケーションスロットなどのさまざまな機能 、スタンバイフィードバック およびフェイルオーバー ストリーミングレプリケーションの信頼性と有用性を向上させるために、時間の経過とともに追加されました。
ストリーミングレプリケーションの大きな「機能」の1つは、それがすべてかゼロかということです。プライマリ上のすべてのデータベースからのすべてのオブジェクトへのすべての変更はスタンバイに送信する必要があり、スタンバイはすべての変更をインポートする必要があります。データベースの一部を選択的に複製することはできません。
論理レプリケーション 、v10で追加されたものは、まさにそれを可能にします–テーブルのセットのみを他のサーバーに複製します。それは例で最もよく説明されます。 src
というデータベースを見てみましょう サーバーにテーブルを作成し、その中にテーブルを作成します:
src=> CREATE TABLE t (col1 int, col2 int);
CREATE TABLE
src=> INSERT INTO t VALUES (1,10), (2,20), (3,30);
INSERT 0 3
出版物も作成します このデータベース内(これを行うにはスーパーユーザー権限が必要であることに注意してください):
src=# CREATE PUBLICATION mypub FOR ALL TABLES;
CREATE PUBLICATION
それでは、データベースのdst
に移動しましょう。 別のサーバーで同様のテーブルを作成します:
dst=# CREATE TABLE t (col1 int, col2 int, col3 text NOT NULL DEFAULT 'foo');
CREATE TABLE
次に、サブスクリプションを設定します ここで、ソース上のパブリケーションに接続し、変更のプルを開始します。 (user repuser
が必要であることに注意してください レプリケーション権限とテーブルへの読み取りアクセス権を持つソースサーバーで。)
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
変更が同期され、宛先側に行が表示されます:
dst=# SELECT * FROM t;
col1 | col2 | col3
------+------+------
1 | 10 | foo
2 | 20 | foo
3 | 30 | foo
(3 rows)
宛先テーブルには、複製の影響を受けない追加の列「col3」があります。変更は「論理的に」複製されます。したがって、t.col1とt.col2のみで行を挿入できる限り、複製プロセスは複製します。
ストリーミングレプリケーションと比較すると、論理レプリケーション機能は、たとえば、特定のデータベース内の単一のスキーマまたはテーブルのセットを別のサーバーにレプリケーションするのに最適です。
ソースデータベースに存在するテーブルのセットを備えたDjangoアプリケーションがあると仮定します。論理レプリケーションをセットアップして、これらすべてのテーブルを別のサーバーに取り込むのは簡単で効率的です。このサーバーでは、「実際の」データに触れることなく、本番アプリに影響を与えることなく、レポート、分析、バッチジョブ、開発者/カスタマーサポートアプリなどを実行できます。
おそらく現在の論理レプリケーションの最大の制限は、スキーマの変更をレプリケートしないことです。ストリーミングレプリケーションとは異なり、ソースデータベースで実行されるDDLコマンドは、宛先データベースで同様の変更を引き起こしません。たとえば、ソースデータベースでこれを行う場合:
src=# ALTER TABLE t ADD newcol int;
ALTER TABLE
src=# INSERT INTO t VALUES (-1, -10, -100);
INSERT 0 1
これは宛先ログファイルに記録されます:
ERROR: logical replication target relation "public.t" is missing some replicated columns
レプリケーションが停止します。列は宛先で「手動で」追加する必要があり、その時点でレプリケーションが再開されます。
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)
つまり、Djangoアプリケーションに新しい列またはテーブルを必要とする新機能が追加されており、django-admin migrate
を実行する必要がある場合です。 ソースデータベースでは、レプリケーションの設定が中断されます。
この問題を修正する最善の策は、宛先でサブスクリプションを一時停止し、最初に宛先を移行し、次にソースを移行してから、サブスクリプションを再開することです。次のようにサブスクリプションを一時停止および再開できます:
-- pause replication (destination side)
ALTER SUBSCRIPTION mysub DISABLE;
-- resume replication
ALTER SUBSCRIPTION mysub ENABLE;
新しいテーブルが追加され、パブリケーションが「FOR ALL TABLES」でない場合は、それらを手動でパブリケーションに追加する必要があります。
ALTER PUBLICATION mypub ADD TABLE newly_added_table;
また、宛先側でサブスクリプションを「更新」して、Postgresに新しいテーブルの同期を開始するように指示する必要があります。
dst=# ALTER SUBSCRIPTION mysub REFRESH PUBLICATION;
ALTER SUBSCRIPTION
ソースでこのテーブルを考えてみましょう。シーケンスは次のとおりです。
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)
シーケンスs_a_seq
a
をバックアップするために作成されました serial
の列 type。これにより、s.a
の自動インクリメント値が生成されます。 。それでは、これをdst
に複製しましょう。 、および別の行を挿入します:
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)
おっと、何が起こったの?宛先はシーケンスを最初から開始しようとし、a
に対して1の値を生成しました 。これは、シーケンスの次の値がテーブル自体に格納されていないため、論理レプリケーションではシーケンスの値がレプリケートされないためです。
論理的に考えると、双方向の同期がなければ、2つの場所から同じ「自動インクリメント」値を変更することはできません。テーブルの各行に増分番号が本当に必要であり、複数のサーバーからそのテーブルに挿入する必要がある場合は、次のことができます。
- ZooKeeperやetcdなどの外部ソースを番号に使用します
- 重複しない範囲を使用します。たとえば、最初のサーバーは100万から100万の範囲の数値を生成して挿入し、2番目のサーバーは100万から200万の範囲の数値を生成して挿入します。
主キーなしでテーブルを作成し、それを複製してみましょう:
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
そして、行も宛先にあります:
dst=# SELECT * FROM nopk;
foo
----------
new york
boston
(2 rows)
次に、ソースの2番目の行を削除してみましょう:
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.
これは、主キーがないと、宛先が削除(または更新)する必要のある行を一意に識別できないために発生します。
もちろん、主キーを含めるようにスキーマを変更することもできます。それをしたくない場合は、ALTER TABLE
「レプリカID」を全行または一意のインデックスに設定します。例:
src=# ALTER TABLE nopk REPLICA IDENTITY FULL;
ALTER TABLE
src=# DELETE FROM nopk WHERE foo='boston';
DELETE 1
これで削除が成功し、レプリケーションも成功します:
dst=# SELECT * FROM nopk;
foo
----------
new york
(1 row)
テーブルに行を一意に識別する方法が本当にない場合は、問題があります。詳細については、ALTERTABLEのREPLICAIDENTITYセクションを参照してください。
ある方法で分割され、別の方法で宛先指定されるソースがあると便利ではないでしょうか。たとえば、ソースでは毎月のパリティを保持し、宛先では毎年のパリティを保持できます。おそらく、宛先はより大きなマシンであり、履歴データを保持する必要がありますが、そのデータが必要になることはめったにありません。
ソースで月次パーティションテーブルを作成しましょう:
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
そして、目的地で年次パーティションテーブルを作成してみてください:
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は、2019年1月のパーティションテーブルが必要であると不満を述べていますが、これは宛先に作成するつもりはありません。
これは、論理レプリケーションがベーステーブルレベルではなく、子テーブルレベルで機能するために発生します。これに対する実際の回避策はありません。パーティションを使用する場合、パーティション階層は論理レプリケーション設定の両側で同じである必要があります。
大きなオブジェクトは、論理レプリケーションを使用してレプリケートすることはできません。大きなオブジェクトを保存することは現代の一般的な慣習ではないため、これはおそらく今日では大したことではありません。また、オブジェクト自体を保存して複製するよりも、外部の冗長なストレージ(NFS、S3など)に大きなオブジェクトへの参照を保存し、その参照を複製する方が簡単です。