PostgreSQLにおける論理レプリケーションの実装:マルチマスターと選択的データ同期
選択的同期、アップグレード、レポートコピー、注意深くスコープされた双方向設計のためのPostgreSQL論理レプリケーションのセットアップ。
PostgreSQLにおける論理レプリケーションの実装: マルチマスターと選択的データ同期
PostgreSQLの論理レプリケーションは、クラスター全体ではなく選択したテーブルをコピーする必要がある場合に役立ちます。レポートデータベース、バージョンアップグレード、リージョナル読み取りコピー、選択的データ同期に有用ですが、ネイティブのPostgreSQL論理レプリケーションは、ターンキーで競合のないマルチマスターシステムではありません。
論理レプリケーションと物理レプリケーションの比較
物理ストリーミングレプリケーション
- データベースクラスター全体をレプリケート
- バイナリレベルのレプリケーション
- 読み取り専用レプリカ
- 同じPostgreSQLバージョンが必要
- 低オーバーヘッド
論理レプリケーション
- 選択的なテーブル/行レプリケーション
- クロスバージョン互換
- 書き込み可能なサブスクライバー
- 高オーバーヘッド
- 柔軟なデータ分散
論理レプリケーションのユースケース
- 選択的データ分散: 特定のテーブルを異なるリージョンにレプリケート
- 双方向同期実験: 注意深くスコープされたデータベース間の書き込み(通常はアプリケーションレベルの競合ルールあり)
- クロスバージョンアップグレード: 古いPostgreSQLバージョンから新しいバージョンへのレプリケーション
- データ集約: 複数のソースからのデータ統合
- GDPR準拠: 機密性の低いカラムのみをレプリケート
前提条件とセットアップ
設定要件
パブリッシャー(ソース)側:
# postgresql.conf
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
サブスクライバー(ターゲット)側:
# postgresql.conf
max_replication_slots = 10
max_logical_replication_workers = 10
max_worker_processes = 16
PostgreSQLの再起動
sudo systemctl restart postgresql
ネットワーク設定
データベースが通信できることを確認:
# サブスクライバーからパブリッシャーへの接続テスト
psql -h publisher.example.com -U replication_user -d source_db
パブリッシャーのpg_hba.confを設定:
# レプリケーション接続を許可
host source_db replication_user subscriber_ip/32 scram-sha-256
基本的な論理レプリケーションのセットアップ
ステップ1: レプリケーションユーザーの作成
パブリッシャー側:
CREATE ROLE replication_user WITH REPLICATION LOGIN PASSWORD 'secure_password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO replication_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO replication_user;
ステップ2: ソーステーブルの作成
パブリッシャー側:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
);
INSERT INTO users (username, email) VALUES
('alice', '[email protected]'),
('bob', '[email protected]');
ステップ3: パブリケーションの作成
パブリッシャー側:
-- すべてのテーブルを公開
CREATE PUBLICATION my_publication FOR ALL TABLES;
-- または特定のテーブルを公開
CREATE PUBLICATION my_publication FOR TABLE users, orders;
-- または公開テーブルに行フィルターを適用
CREATE PUBLICATION active_users FOR TABLE users WHERE (status = 'active');
パブリケーションの表示:
SELECT * FROM pg_publication;
SELECT * FROM pg_publication_tables;
ステップ4: レプリカテーブルの作成
サブスクライバー側:
-- テーブルは同一構造である必要があります
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP DEFAULT NOW()
);
ステップ5: サブスクリプションの作成
サブスクライバー側:
CREATE SUBSCRIPTION my_subscription
CONNECTION 'host=publisher.example.com port=5432 dbname=source_db user=replication_user password=secure_password'
PUBLICATION my_publication;
ステップ6: レプリケーションの確認
パブリッシャー側:
SELECT * FROM pg_stat_replication;
SELECT * FROM pg_replication_slots;
サブスクライバー側:
SELECT * FROM pg_stat_subscription;
SELECT * FROM users; -- レプリケートされたデータが表示されるはず
高度な設定
カラムレベルのレプリケーション(PostgreSQL 15以降)
特定のカラムのみをレプリケート:
-- パブリッシャー側: 機密性の低いカラムのみをレプリケート
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
credit_card VARCHAR(20), -- レプリケートされない
created_at TIMESTAMP
);
CREATE PUBLICATION customer_basic
FOR TABLE customers (id, name, email, created_at);
行フィルタリング
アクティブなレコードのみをレプリケート:
CREATE PUBLICATION active_data
FOR TABLE orders WHERE (status IN ('pending', 'processing'));
リージョナルデータ分散:
CREATE PUBLICATION us_customers
FOR TABLE customers WHERE (country = 'US');
CREATE PUBLICATION eu_customers
FOR TABLE customers WHERE (country IN ('UK', 'DE', 'FR'));
複数のパブリケーション
-- パブリッシャー: 複数のパブリケーションを作成
CREATE PUBLICATION oltp_data FOR TABLE users, orders;
CREATE PUBLICATION analytics_data FOR TABLE logs, metrics;
-- サブスクライバー: 複数のパブリケーションにサブスクライブ
CREATE SUBSCRIPTION multi_sub
CONNECTION 'host=publisher port=5432 dbname=mydb user=repuser password=pass'
PUBLICATION oltp_data, analytics_data;
双方向レプリケーションの注意点
ネイティブの論理レプリケーションは双方向に配線できますが、PostgreSQLは競合する書き込みを自動的にマージしません。このパターンは、各行に単一の書き込み元があり、キーが衝突せず、アプリケーションが競合を処理できる場合にのみ使用してください。
双方向同期のスケルトン例
データベースAの設定:
-- パブリケーションを作成
CREATE PUBLICATION db_a_pub FOR TABLE shared_table;
-- データベースBにサブスクライブ
CREATE SUBSCRIPTION db_a_sub
CONNECTION 'host=db-b.example.com dbname=mydb user=repuser'
PUBLICATION db_b_pub
WITH (origin = none); -- レプリケーションループを防止
データベースBの設定:
-- パブリケーションを作成
CREATE PUBLICATION db_b_pub FOR TABLE shared_table;
-- データベースAにサブスクライブ
CREATE SUBSCRIPTION db_b_sub
CONNECTION 'host=db-a.example.com dbname=mydb user=repuser'
PUBLICATION db_a_pub
WITH (origin = none);
競合処理
論理レプリケーションは自動的な「最終書き込み勝ち」の競合解決を提供しません。競合する挿入、更新時の行欠落、制約違反、重複キーは、データ問題を修正するまで適用ワーカーを停止させる可能性があります。
-- 競合を追跡するためにレプリカIDを設定
ALTER TABLE shared_table REPLICA IDENTITY FULL;
競合削減戦略:
- タイムスタンプベースの所有権チェック:
updated_atと書き込み元識別子を追加し、アプリケーションが古い書き込みを拒否できるようにします。
CREATE TABLE shared_table (
id SERIAL PRIMARY KEY,
data TEXT,
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_shared_table_timestamp
BEFORE UPDATE ON shared_table
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
- バージョン番号: アプリケーションでバージョンをインクリメントし、古いバージョンに基づく更新を拒否します。
CREATE TABLE shared_table (
id SERIAL PRIMARY KEY,
data TEXT,
version INTEGER DEFAULT 1
);
初期データ同期オプション
オプション1: 自動コピー(デフォルト)
-- サブスクライバーが既存データを自動的にコピー
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (copy_data = true); -- デフォルト
オプション2: 手動初期同期
大規模データセットの場合はpg_dumpを使用:
# パブリッシャーから特定のテーブルをダンプ
pg_dump -h publisher.example.com -U postgres -d source_db \
-t users -t orders --no-owner --no-acl > initial_data.sql
# サブスクライバーにロード
psql -h subscriber.example.com -U postgres -d target_db < initial_data.sql
# 初期コピーなしでサブスクリプションを作成
psql -h subscriber.example.com -U postgres -d target_db -c "
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher dbname=source_db user=repuser'
PUBLICATION my_pub
WITH (copy_data = false);
"
オプション3: 大規模進行中トランザクションのストリーミング
-- コミットを待たずに大規模進行中トランザクションをストリーミング
CREATE SUBSCRIPTION fast_sync
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (
copy_data = true,
streaming = on,
synchronous_commit = off
);
論理レプリケーションの監視
パブリッシャーの監視
-- レプリケーションスロットの表示
SELECT
slot_name,
plugin,
active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag_size
FROM pg_replication_slots;
-- アクティブなレプリケーション接続の表示
SELECT
pid,
usename,
application_name,
client_addr,
state,
sync_state,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)) AS send_lag
FROM pg_stat_replication;
サブスクライバーの監視
-- サブスクリプションステータスの表示
SELECT
subname,
pid,
received_lsn,
latest_end_lsn,
pg_size_pretty(pg_wal_lsn_diff(latest_end_lsn, received_lsn)) AS lag
FROM pg_stat_subscription;
-- レプリケーションエラーの確認
SELECT * FROM pg_stat_subscription WHERE last_msg_receipt_time < NOW() - INTERVAL '5 minutes';
監視スクリプト
#!/bin/bash
# logical-replication-monitor.sh
echo "=== パブリッシャーステータス ==="
psql -h publisher -d mydb -c "
SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots;"
echo ""
echo "=== サブスクライバーステータス ==="
psql -h subscriber -d mydb -c "
SELECT subname, pid,
pg_size_pretty(pg_wal_lsn_diff(latest_end_lsn, received_lsn)) AS lag
FROM pg_stat_subscription;"
トラブルシューティング
問題1: サブスクリプションがデータを受信しない
サブスクリプションステータスの確認:
SELECT subname, subenabled, subconninfo FROM pg_subscription;
SELECT subname, pid, received_lsn, latest_end_lsn FROM pg_stat_subscription;
無効な場合、サブスクリプションを有効化:
ALTER SUBSCRIPTION my_sub ENABLE;
エラーの確認:
SELECT * FROM pg_stat_subscription;
問題2: レプリケーションラグが増大
遅いテーブルの特定:
SELECT schemaname, tablename, n_tup_ins, n_tup_upd, n_tup_del
FROM pg_stat_user_tables
ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC;
ワーカープロセスの増加:
# サブスクライバーのpostgresql.conf
max_logical_replication_workers = 20
max_worker_processes = 30
問題3: レプリケーションスロットの肥大化
スロット使用状況の確認:
SELECT
slot_name,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
非アクティブなスロットの削除:
SELECT pg_drop_replication_slot('inactive_slot_name');
問題4: 初期同期の失敗
初期同期の再実行:
-- サブスクリプションを削除して再作成
DROP SUBSCRIPTION my_sub;
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (copy_data = true);
パフォーマンス最適化
1. 適切なレプリカIDの使用
-- デフォルト: PRIMARY KEYのみ
ALTER TABLE my_table REPLICA IDENTITY DEFAULT;
-- フル: すべてのカラム(高オーバーヘッド)
ALTER TABLE my_table REPLICA IDENTITY FULL;
-- インデックス: 特定のユニークインデックスを使用
CREATE UNIQUE INDEX replica_idx ON my_table(col1, col2);
ALTER TABLE my_table REPLICA IDENTITY USING INDEX replica_idx;
2. 大規模トランザクションの並列適用
CREATE SUBSCRIPTION parallel_sub
CONNECTION 'host=publisher dbname=mydb user=repuser'
PUBLICATION my_pub
WITH (streaming = parallel);
ベストプラクティス
- 常にレプリカIDを使用: テーブルにPRIMARY KEYまたはUNIQUE制約があることを確認
- レプリケーションラグを監視: リカバリ要件とWAL保持予算に基づいてアラートを設定
- 行フィルターを慎重に使用: フィルターはパブリッシャーで評価され、パフォーマンスに影響
- 競合に備える: 双方向書き込みを試みる前に競合検出を実装
- フェイルオーバー手順をテスト: データベース間の切り替えを練習
- 定期的なメンテナンス: 古いレプリケーションスロットをクリーンアップ
まとめ
単一方向の論理レプリケーションから始めてください。選択的テーブル同期、レポートコピー、多くのアップグレードワークフローに信頼性があります。双方向レプリケーションは、チェックボックスではなく高度な設計問題として扱ってください:両側に書き込む前に、行の所有権、キー生成、競合処理を定義してください。