PostgreSQLにおける高可用性のための同期レプリケーションの設定

同期ストリーミングレプリケーションを使用して、データ損失ゼロ(RPO=0)のPostgreSQL高可用性を構成する方法を学びます。このステップバイステップのチュートリアルでは、プライマリサーバーとスタンバイサーバーで`wal_level`、レプリケーションスロット、`pg_basebackup`、および`synchronous_commit`パラメータの正しい設定について説明し、重要な環境でのトランザクションの永続性を保証します。

PostgreSQLにおける高可用性のための同期レプリケーションの設定

PostgreSQLの高可用性(HA)を構成する際、通常は難しい質問から始まります。プライマリサーバーがコミット直後に消失した場合、どれだけのデータ損失を許容できますか?通常の非同期ストリーミングレプリケーションでは、答えは「多少の損失はあり得る」です。プライマリは、スタンバイがWALレコードを受信または再生する前に、トランザクションがコミットされたことをアプリケーションに通知できます。そのわずかな間にプライマリが障害を起こすと、昇格したスタンバイには最後にコミットされたトランザクションが含まれていない可能性があります。

同期ストリーミングレプリケーションは、そのトレードオフを変えます。PostgreSQLは、コミット成功を報告する前に、1つ以上の名前付きスタンバイを待機します。synchronous_commitレベルに応じて、スタンバイはWALをオペレーティングシステムに書き込むだけでよい場合も、永続ストレージにフラッシュする必要がある場合も、スタンバイでクエリが表示できるように再生する必要がある場合もあります。これにより、コミットされたトランザクションのRPOをゼロにできますが、書き込みパスがネットワークとスタンバイの健全性に依存することを意味します。

そのトレードオフは重要です。同期レプリケーションは、承認されたトランザクションが1つでも失われることが許されない少数のデータ(支払い、アカウント残高、在庫予約、注文状態、監査証跡)に適しています。大量のイベントログ、クリックストリームデータ、メトリクス、またはノード間の完全な耐久性よりも可用性とレイテンシが重要なワークロードには、多くの場合適していません。グローバルに有効にする前に、実際に必要なワークロードの部分を決定してください。

前提条件

開始する前に、同一のメジャーバージョンのPostgreSQLを実行する2台のPostgreSQLサーバー(プライマリとスタンバイ)がセットアップされていることを確認してください。両方のサーバーはネットワーク接続が可能である必要があります。このガイドでは、以下を想定しています。

  • プライマリホスト名/IP: pg_primary
  • スタンバイホスト名/IP: pg_standby
  • レプリケーションユーザー: repl_user
  • データベース名: mydb

また、初期ベースバックアップのための動作するバックアップとメンテナンスウィンドウが必要です。例では、スタンバイモードがstandby.signalで制御され、接続設定が通常pg_basebackup -Rによって書き込まれるPostgreSQL 12以降を想定しています。

ステップ1: プライマリサーバーの構成

プライマリサーバーでは、ストリーミングレプリケーションを有効にし、同期コミットに必要なWrite-Ahead Log(WAL)を管理するための特定の設定が必要です。

A. プライマリのpostgresql.confの調整

プライマリサーバーのpostgresql.confファイルを編集します。以下のパラメータはストリーミングレプリケーションに必須です。

# --- レプリケーションに必須 ---
listen_addresses = '*'         # スタンバイからの接続を許可
wal_level = replica            # 'replica'以上(例:'logical')である必要があります
max_wal_senders = 10           # スタンバイからの最大同時接続数
max_replication_slots = 10     # 永続的なレプリケーションストリームに必要なスロット

# --- 同期コミットに必須 ---
synchronous_standby_names = 'FIRST 1 (standby1)' # application_nameで必要なスタンバイを指定

# --- オプションだが推奨 ---
wal_log_hints = on             # より安全なレプリケーションに推奨されますが、WALボリュームが増加します
shared_preload_libraries = 'pg_stat_statements' # 監視を使用する場合

主要パラメータの説明:

  • wal_level = replica: これにより、スタンバイサーバーがデータベース状態を再構築できるように、十分な情報がWALに書き込まれることが保証されます。同期コミットの場合、このレベルが最小要件です。
  • synchronous_standby_names: これは、どのスタンバイが書き込みを承認する必要があるかを定義するためのコア設定です。ここでの名前は、レプリケーションスロット名ではなく、レプリケーション接続のapplication_name値です。FIRST 1 (standby1)は、PostgreSQLがそのリストから最初に利用可能な同期スタンバイを待つことを意味します。ANY 1 (standby1, standby2)は、リストされたスタンバイのいずれか1つがコミットを満たすことができることを意味します。

B. ホストベース認証の構成(pg_hba.conf

プライマリサーバーは、レプリケーション目的でスタンバイサーバーからのレプリケーションユーザーの接続を許可する必要があります。

プライマリのpg_hba.confにエントリを追加します。

# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    replication     repl_user       pg_standby/32           scram-sha-256

pg_standby/32をスタンバイサーバーの実際のIPアドレスまたはサブネットに置き換えます。

C. レプリケーションスロットとユーザーの作成

プライマリサーバーでPostgreSQLに接続し、必要なユーザーとレプリケーションスロットを作成します。

1. レプリケーションユーザーの作成:

CREATE ROLE repl_user WITH REPLICATION LOGIN PASSWORD 'a_strong_password';

2. レプリケーションスロットの作成:

このスロットは、スタンバイが受信を確認するまでWALセグメントが保持されることを保証し、スタンバイが新しいベースバックアップを必要とするほど遅れるのを防ぎます。スロットは便利ですが、スタンバイが長時間ダウンしているとディスクを満たす可能性があるため、保持されているWALを監視してください。

SELECT pg_create_physical_replication_slot('standby1_slot');

スロット名はsynchronous_standby_namesと一致する必要はありません。この例では、standby1は同期スタンバイ選択に使用されるapplication_nameであり、standby1_slotはWAL保持に使用される物理レプリケーションスロットです。

D. プライマリの再起動

プライマリサーバーでPostgreSQLサービスを再起動して、すべての構成変更を適用します。

sudo systemctl restart postgresql

ステップ2: スタンバイサーバーの構成

スタンバイサーバーは、リカバリ構成を使用してプライマリからWALレコードをストリーミングするように構成されます。

A. ベースバックアップ

ストリーミングを開始する前に、スタンバイにはプライマリのデータディレクトリの完全なコピーが必要です。最初にスタンバイでPostgreSQLを停止します。

sudo systemctl stop postgresql

pg_basebackupを使用してベースバックアップを取得します。必要に応じてパスと接続の詳細を置き換えます。

# pg_basebackupユーティリティを使用した例
pg_basebackup -h pg_primary -D /var/lib/postgresql/15/main/ -U repl_user -P -Xs -R -W
  • -D: スタンバイのターゲットデータディレクトリ。
  • -U: レプリケーションユーザー。
  • -P: 進行状況を表示。
  • -Xs: ベースバックアップ中に必要なWALファイルを含めます。
  • -R: standby.signalファイルを自動的に作成し、postgresql.auto.conf(またはリカバリ構成)に必要な接続設定を生成します。

B. スタンバイのpostgresql.confの構成

スタンバイで、PostgreSQLがプライマリに接続する方法を認識していることを確認します。同期レプリケーションの重要な詳細はapplication_nameです。これはsynchronous_standby_namesにリストされている名前と一致する必要があります。

# --- スタンバイに必須 ---
primary_conninfo = 'host=pg_primary port=5432 user=repl_user password=a_strong_password application_name=standby1'
primary_slot_name = 'standby1_slot'
hot_standby = on          # リカバリ/スタンバイモード中に読み取りクエリを許可

C. スタンバイの起動

スタンバイサーバーでPostgreSQLサービスを起動します。

sudo systemctl start postgresql

ステップ3: 同期コミットの確認とテスト

両方のサーバーが実行されたら、接続を確認し、同期動作をテストします。

A. レプリケーションステータスの確認

プライマリデータベースに接続し、pg_stat_replicationビューを確認します。

SELECT client_addr, application_name, state, sync_state FROM pg_stat_replication;

sync_statesyncstandby1のエントリが表示されるはずです。potentialと表示される場合、スタンバイは接続されていますが、現在同期コミットを満たしているものではありません。asyncと表示される場合、PostgreSQLはそれを同期スタンバイとして扱っていません。application_namesynchronous_standby_namesのスペルを確認してください。

B. 同期コミットのテスト

PostgreSQLが待機する度合いを決定するグローバルパラメータはsynchronous_commitです。RPO=0の場合、同期を強制する値を使用する必要があります。

1. グローバル動作の設定

ステップ1で示したようにプライマリでsynchronous_standby_namesを構成した場合、デフォルトのsynchronous_commit = onは、同期スタンバイがWALレコードを永続ストレージにフラッシュするまで待機します。remote_writeは、スタンバイがWALレコードをオペレーティングシステムに書き込むまで待機します。これは通常高速ですが、スタンバイホストがフラッシュ前にクラッシュした場合の保証は弱くなります。remote_applyは、スタンバイがトランザクションを再生するまで待機します。これは、プライマリに書き込んだ直後にアプリケーションがスタンバイから読み取る場合に便利です。

ほとんどのデータ損失ゼロHAセットアップでは、onが実用的な開始点です。remote_applyは、スタンバイでの読み取り後書き込み動作が、追加のレイテンシを正当化するほど重要な場合にのみ使用してください。

# プライマリのpostgresql.conf内
synchronous_commit = on

警告: 同期コミットは、非同期モード(offまたはlocal)と比較して、書き込みレイテンシを著しく増加させる可能性があります。追加されるレイテンシは、ネットワークラウンドトリップ、スタンバイWAL書き込み速度、およびremote_applyの場合は再生速度に起因します。

2. トランザクション内でのテスト

トランザクション内でテストするには(グローバル構成変更を必要とせずに)、セッションまたはトランザクションごとに設定できます。

-- プライマリに接続

BEGIN;
SET LOCAL synchronous_commit = on;

INSERT INTO sales (item, amount) VALUES ('Widget A', 100);
-- このINSERTは、同期スタンバイによって承認される必要があるWALを準備します。

COMMIT;
-- COMMITは、スタンバイがWAL書き込みを承認した後にのみ成功します。

コミット時に構成された同期スタンバイが利用できない場合、コミットは待機します。これがこの機能のポイントですが、障害時にチームを驚かせる可能性があります。プライマリはまだ稼働している可能性がありますが、PostgreSQLが同期確認を待っているため、書き込みがフリーズしているように見えます。同期スタンバイが消失したときに自動的に非同期コミットに「フォールバック」する一般的なPostgreSQL設定はありません。その動作が必要な場合、HAツールはsynchronous_standby_namesを変更する必要があるか、可用性がデータ損失ゼロよりも重要であると判断した後に手動で変更するためのランブックが必要です。

より安全な2スタンバイパターン

単一の同期スタンバイは、すべてが正常な間は強力な耐久性を提供しますが、書き込み可用性の単一障害点も作成します。そのスタンバイがダウンしているか、遅いか、プライマリから分離されている場合、コミットは待機します。本番環境では、少なくとも2つのスタンバイを実行し、1つの同期確認を要求するのが一般的なパターンです。

synchronous_standby_names = 'ANY 1 (standby1, standby2)'

この設定では、どちらかのスタンバイがコミットを満たすことができます。standby1が再起動している場合、standby2が書き込みを承認できます。両方のレプリカを監視する必要があります。1つのスタンバイの長時間の障害により、そのレプリケーションスロットが大量のWALを保持する可能性がありますが、単一のスタンバイ障害によってプライマリが停止する可能性は低くなります。

2つの確認を要求することも可能です。

synchronous_standby_names = 'ANY 2 (standby1, standby2, standby3)'

これはより厳格な耐久性の選択です。通常、非常に低レイテンシのリンクがあり、コミット前に複数のリモートコピーを必要とする明確な理由がある環境のために予約されています。多くのアプリケーションデータベースでは、「近くの2つのスタンバイのいずれか1つ」がより良いバランスです。

有効化後に監視すべきこと

「スタンバイが接続した」で止まらないでください。同期レプリケーションは技術的に機能していても、ユーザー向けのレイテンシが悪化する可能性があります。ロールアウト後はこれらのシグナルを監視してください。

SELECT
    application_name,
    client_addr,
    state,
    sync_state,
    write_lag,
    flush_lag,
    replay_lag
FROM pg_stat_replication;

プライマリで、sync_state = 'sync'は、現在どのスタンバイが同期しているかを示します。write_lagflush_lagreplay_lagは、時間がどこで費やされているかを理解するのに役立ちます。write_lagが高い場合は、ネットワークまたはスタンバイのWAL書き込みプレッシャーが疑われます。flush_lagが高い場合は、ストレージが疑われます。replay_lagのみが高い場合、スタンバイはWALを受信しているが、I/O、CPU、ロック、またはスタンバイでの長時間実行クエリのために適用が遅い可能性があります。

また、スロットの保持も監視します。

SELECT
    slot_name,
    active,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;

レプリケーションスロットはスタンバイを保護しますが、ディスクを保護するわけではありません。スロットが非アクティブで、保持されているWALが増え続ける場合は、スタンバイを迅速に修正するか、不要になったことを確認した後にスロットを削除します。

実用的なロールアウト計画

ビジーな本番システムの場合、同期レプリケーションを1行の設定変更としてではなく、段階的な変更として扱います。

まず、スタンバイを非同期で構築し、しばらく実行させます。ピーク書き込み期間中に追いつくことができることを確認します。非同期で遅れると、同期になったときにコミットレイテンシに悪影響を及ぼします。

次に、application_nameを設定し、プライマリがpg_stat_replicationで期待どおりにスタンバイを認識していることを確認します。synchronous_standby_namesはホスト名やスロットではなく、ランタイムのapplication_nameと一致するため、スペルミスがよくあります。

第三に、低トラフィックウィンドウ中に同期レプリケーションを有効にし、アプリケーション側からコミットレイテンシを監視します。トランザクションが接続を少し長く保持するようになったため、PostgreSQLメトリクスは問題なく見えても、アプリケーション接続プールがバックアップされる可能性があります。

最後に、障害時の判断を文書化します。同期スタンバイが消失し、プライマリがコミットを待機している場合、誰がsynchronous_standby_namesを緩和することを許可されますか?どのような条件下で?ノードを再結合する前に、古いプライマリと古いスタンバイのどちらに最新のデータが含まれているかをどのように確認しますか?これらは運用上の決定であり、単なるデータベース設定ではありません。

同期HAのベストプラクティス

  • 専用スタンバイを使用する: プライマリに物理的に近い(低レイテンシ)スタンバイのみを同期レプリケーションリストに割り当てます。高レイテンシはコミット時間に直接現れます。
  • レプリケーションラグを監視する: 同期モードでも、スタンバイラグを監視します。技術的には「同期」していてもWALの処理に時間がかかりすぎている遅いスタンバイは、ユーザーエクスペリエンスに影響を与える可能性があります。
  • 可用性のトレードオフを計画する: インシデント中にオペレーターが一時的に欠落しているスタンバイをsynchronous_standby_namesから削除できるかどうかを事前に決定します。
  • 複数のスタンバイを使用する: 書き込み可用性を向上させるには、synchronous_standby_names = 'ANY 1 (standby1, standby2)'を構成して、どちらかのスタンバイがコミットを承認できるようにします。