🐘

PostgreSQLでロジカルレプリケーションを試す

2023/04/23に公開

はじめに

災害対策のため、データベースの複製が欲しい場面がありました。
クラウドの利用が許されるなら、クロスリージョンリードレプリカなどを採用するところですが、災害時にインターネットが不通になる可能性を考慮すると、オンプレで複製を持っておく手段を用意する必要がありました。

PostgreSQLのレプリケーション

PostgreSQLのレプリケーションは大きく「ストリーミングレプリケーション」と「ロジカルレプリケーション」の2種類あります。

ストリーミングレプリケーション

マスターノードとスタンバイノードで構成し、マスタの更新情報をスタンバイへ転送する方式です。マスターは参照/更新ができる一方、スタンバイは参照のみです。

ロジカルレプリケーション

パブリケーション(発行)とサブスクリプション(購読)を設定し、パブリケーションの変更をサブスクリプションへ転送する方式です。テーブル単位のパブリッシュを設定できたり、INSERTのみパブリッシュする(DELETEやUPDATEを同期しない)といった設定も作成できたり、比較的柔軟にレプリケーションを行うことができます。パブリケーション側テーブルの参照/更新はもちろん、サブスクライバ側テーブルでの参照/更新も可能です。
DDLがレプリケーションされないなどの制約はありますが、どのノードからでも参照/更新ができるため、今回はこちらの方式を検証しました。

概要

こんな感じの構成を作ってみます。PostgreSQL 15で試しました。

PostgreSQLはDockerコンテナで準備しました。
以下が今回検証した流れです。

  1. DockerでPostgreSQLコンテナを2つ起動
  2. PostgreSQLの設定変更(wal_level)
  3. パブリケーションとサブスクリプションの設定
  4. 動作確認

やってみる

Dockerはインストールしている前提です。

PostgreSQLコンテナを起動

docker-compose.ymlを作成します。
イメージにはpostgresを指定しています。コンテナ名はdb1db2とし、2台起動するようにしています。
db1を発行者、db2を購読者として設定していきます。

docker-compose.yml
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_levellogicalへ変更します。db2でも同様のコマンドを実行します。

db1、db2での操作
psql -U admin
ALTER SYSTEM SET wal_level = logical;

wal_levelの反映には再起動が必要なので、コンテナを再起動します。

ホストでの操作
docker-compose restart

パブリケーションとサブスクリプションの設定

db1tbl_aを作成し、発行(パブリケーション)の設定をします。

db1での操作
psql -U admin
CREATE TABLE tbl_a (id INTEGER PRIMARY KEY, name TEXT);
CREATE PUBLICATION pub_from_db1 FOR TABLE tbl_a;

db2tbl_aを作成し、購読(サブスクリプション)の設定をします。

db2での操作
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に同期されるか確認してみましょう。

db1での操作
INSERT INTO tbl_a VALUES (1, 'Insert from db1');
db2での操作
SELECT * FROM tbl_a;
 id |      name       
----+-----------------
  1 | Insert from db1

同期されていることが確認できました。
続いて、db2にレコードを作成し、db1と差分がある状態で、db1を更新した場合の挙動を確認してみましょう。

db2での操作
INSERT INTO tbl_a VALUES (2, 'Insert from db2');
SELECT * FROM tbl_a;
 id |      name       
----+-----------------
  1 | Insert from db1
  2 | Insert from db2
db1での操作
UPDATE tbl_a SET name = 'Update from db1';
db2での操作
SELECT * FROM tbl_a;
 id |      name       
----+-----------------
  2 | Insert from db2
  1 | Update from db1

db1から同期されたレコードは更新されていますが、db2で作成したレコードは更新されていません。
続いてdb1のレコードを削除してみましょう。

db1での操作
DELETE FROM tbl_a;
db2での操作
SELECT * FROM tbl_a;
 id |      name       
----+-----------------
  2 | Insert from db2

気持ちとしては全レコード削除されてほしいところですが、db2で作成したレコードは残ってしまいました。
全て削除する場合はTRUNCATEを使用しましょう。

db1での操作
TRUNCATE tbl_a;
db2での操作
SELECT * FROM tbl_a;
 id | name 
----+------
(0 rows)

db2の全レコードが削除されました。

まとめ

PostgreSQL 15時点のロジカルレプリケーションでは同一テーブルで双方向の同期はできません。
別テーブルで双方向同期(例えば、DB1:テーブルA→DB2:テーブルA、DB2:テーブルB→DB1:テーブルBとする設定)は可能です。
今回の検証で単純なレプリケーションはできましたが、差分や競合が発生した場合の挙動は扱いが難しく、本番運用は見送ろうかなと思っています。また、DDLが同期されないため変更が大変になりそうなのもネックです。

災害に備えたシステムは皆さんどのように構築していますか?
(インターネットが遮断されている前提だとクライアントアプリを作るしかないですが、世の中そんなものでしょうか?)

Discussion