PostgreSQLでロジカルレプリケーションを試す
はじめに
災害対策のため、データベースの複製が欲しい場面がありました。
クラウドの利用が許されるなら、クロスリージョンリードレプリカなどを採用するところですが、災害時にインターネットが不通になる可能性を考慮すると、オンプレで複製を持っておく手段を用意する必要がありました。
PostgreSQLのレプリケーション
PostgreSQLのレプリケーションは大きく「ストリーミングレプリケーション」と「ロジカルレプリケーション」の2種類あります。
ストリーミングレプリケーション
マスターノードとスタンバイノードで構成し、マスタの更新情報をスタンバイへ転送する方式です。マスターは参照/更新ができる一方、スタンバイは参照のみです。
ロジカルレプリケーション
パブリケーション(発行)とサブスクリプション(購読)を設定し、パブリケーションの変更をサブスクリプションへ転送する方式です。テーブル単位のパブリッシュを設定できたり、INSERTのみパブリッシュする(DELETEやUPDATEを同期しない)といった設定も作成できたり、比較的柔軟にレプリケーションを行うことができます。パブリケーション側テーブルの参照/更新はもちろん、サブスクライバ側テーブルでの参照/更新も可能です。
DDLがレプリケーションされないなどの制約はありますが、どのノードからでも参照/更新ができるため、今回はこちらの方式を検証しました。
概要
こんな感じの構成を作ってみます。PostgreSQL 15で試しました。
PostgreSQLはDockerコンテナで準備しました。
以下が今回検証した流れです。
- DockerでPostgreSQLコンテナを2つ起動
- PostgreSQLの設定変更(wal_level)
- パブリケーションとサブスクリプションの設定
- 動作確認
やってみる
Dockerはインストールしている前提です。
PostgreSQLコンテナを起動
docker-compose.ymlを作成します。
イメージにはpostgres
を指定しています。コンテナ名はdb1
、db2
とし、2台起動するようにしています。
db1
を発行者、db2
を購読者として設定していきます。
version: '3'
services:
db1:
image: postgres
container_name: db1
ports:
- 5433:5432
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
db2:
image: postgres
container_name: db2
ports:
- 5434:5432
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
docker-compose.ymlのディレクトリに移動し、下記コマンドでコンテナを起動します。
docker-compose up -d
PostgreSQLの設定変更
db1
へ接続し、ターミナルからコマンドを実行します。
wal_level
をlogical
へ変更します。db2
でも同様のコマンドを実行します。
psql -U admin
ALTER SYSTEM SET wal_level = logical;
wal_level
の反映には再起動が必要なので、コンテナを再起動します。
docker-compose restart
パブリケーションとサブスクリプションの設定
db1
でtbl_a
を作成し、発行(パブリケーション)の設定をします。
psql -U admin
CREATE TABLE tbl_a (id INTEGER PRIMARY KEY, name TEXT);
CREATE PUBLICATION pub_from_db1 FOR TABLE tbl_a;
db2
でtbl_a
を作成し、購読(サブスクリプション)の設定をします。
psql -U admin
CREATE TABLE tbl_a (id INTEGER PRIMARY KEY, name TEXT);
CREATE SUBSCRIPTION sub_from_db1 CONNECTION 'host=db1 port=5432 user=admin password=admin' PUBLICATION pub_from_db1;
動作確認
db1
にレコードを作成し、db2
に同期されるか確認してみましょう。
INSERT INTO tbl_a VALUES (1, 'Insert from db1');
SELECT * FROM tbl_a;
id | name
----+-----------------
1 | Insert from db1
同期されていることが確認できました。
続いて、db2
にレコードを作成し、db1
と差分がある状態で、db1
を更新した場合の挙動を確認してみましょう。
INSERT INTO tbl_a VALUES (2, 'Insert from db2');
SELECT * FROM tbl_a;
id | name
----+-----------------
1 | Insert from db1
2 | Insert from db2
UPDATE tbl_a SET name = 'Update from db1';
SELECT * FROM tbl_a;
id | name
----+-----------------
2 | Insert from db2
1 | Update from db1
db1
から同期されたレコードは更新されていますが、db2
で作成したレコードは更新されていません。
続いてdb1
のレコードを削除してみましょう。
DELETE FROM tbl_a;
SELECT * FROM tbl_a;
id | name
----+-----------------
2 | Insert from db2
気持ちとしては全レコード削除されてほしいところですが、db2
で作成したレコードは残ってしまいました。
全て削除する場合はTRUNCATE
を使用しましょう。
TRUNCATE tbl_a;
SELECT * FROM tbl_a;
id | name
----+------
(0 rows)
db2
の全レコードが削除されました。
まとめ
PostgreSQL 15時点のロジカルレプリケーションでは同一テーブルで双方向の同期はできません。
別テーブルで双方向同期(例えば、DB1:テーブルA→DB2:テーブルA、DB2:テーブルB→DB1:テーブルBとする設定)は可能です。
今回の検証で単純なレプリケーションはできましたが、差分や競合が発生した場合の挙動は扱いが難しく、本番運用は見送ろうかなと思っています。また、DDLが同期されないため変更が大変になりそうなのもネックです。
災害に備えたシステムは皆さんどのように構築していますか?
(インターネットが遮断されている前提だとクライアントアプリを作るしかないですが、世の中そんなものでしょうか?)
Discussion