フェイルオーバーが安全でないとき: Kubernetesベースの高可用性PostgreSQLの構築
(datadoghq.com)- k8sベースの PostgreSQL クラスタでネットワーク障害時に レプリケーション遅延(replication lag) が蓄積し、安全なフェイルオーバーが不可能になる構造的な弱点を解決した方法
- 従来の構成は 可用性(availability) を 耐久性(durability) より優先しており、プライマリが書き込みを受け続ける間にレプリカが遅れ、データ損失なしに昇格できる候補がなくなっていた
- 解決策として、フェイルオーバー候補に 同期レプリケーション(synchronous replication) を適用し、オープンソースの高可用性マネージャー Patroni で調整
- リーダープールのスタンバイのみが同期レプリケーションに参加し、読み取りレプリカは非同期を維持する ハイブリッドレプリケーションモデル により、耐久性とレイテンシのバランスを確保
remote_applyモードの適用時に書き込みレイテンシが 53% 増加するなどの性能コストがある一方、5つの障害シナリオの検証を通じて安全な自動フェイルオーバーを実現
ゲームデイで明らかになった問題
- Datadog はシステムとプロセスの隙を事前に見つけるため ゲームデイ を定期的に実施し、プラットフォームに意図的な負荷をかけて実際の条件下での反応を学習
- あるゲームデイでステージング環境に アベイラビリティゾーン(AZ) 障害 をシミュレーションし、ネットワーク遅延を発生させたことで PostgreSQL アーキテクチャの弱点が露呈
- 複数の Kubernetes ベース PostgreSQL クラスタのプライマリ/ライターノードが、影響を受けた AZ で動作中だった
- ネットワーク遅延の急増により、プライマリがレプリカと安定して通信できなくなり、レプリケーション遅延が増加 → 書き込み停滞 → アプリケーションが古いデータを提供
- 十分に最新状態のレプリカがなく、フェイルオーバーは安全ではなく、クラスタは事実上停止
- この構成は正常時にはうまく機能していたが、特定のネットワーク障害状況では 可用性を耐久性より優先 していたため、安全な復旧経路がなかった
- プライマリはレプリケーションが遅延している間も書き込みを受け続け、レプリケーション遅延が拡大してレプリカはさらに遅れた
- 結果として、データ損失リスクなしにフェイルオーバー候補を昇格できず、遅延が収まりレプリカが追いつくのを待つことが唯一の選択肢だった
- 目標は PostgreSQL の性能特性を必要以上に損なうことなく、フェイルオーバーを 自動かつ安全 にすること
基準アーキテクチャ: Kubernetes 上の PostgreSQL
- Kubernetes ベースの PostgreSQL クラスタは leader pool と read replica pool の 2 つのプールで構成され、PostgreSQL は単一ライター(single-writer) システム
- 読み書き分離によりリーダーに負荷をかけずに読み取りを拡張でき、書き込みレイテンシは予測可能かつ安定して維持される
- leader pool は、すべての書き込みを処理する単一のアクティブライターノード 1 台と、スタンバイノード 2 台で構成
- スタンバイはアプリケーショントラフィックを処理しないが、リーダー障害時には昇格可能
- read replica pool は読み取り専用トラフィックを処理する多数のノードで構成され、読み取りスケーラビリティとクエリ分離に最適化されており、フェイルオーバー対象からは意図的に除外
Patroni と ZooKeeper の役割
- Patroni がレプリケーション、フェイルオーバー、リーダー選出を管理し、分散設定ストア(DCS) として ZooKeeper を使用
- ZooKeeper は現在のリーダーキー/ロック、クラスタ構成、各メンバーのレプリケーション状態(最新 LSN など)のメタデータを保存
- Patroni はこの情報を基に昇格・降格を保守的に判断し、積極的なフェイルオーバーよりデータ整合性を優先
- 新しいノードがクラスタに参加すると、まず ZooKeeper でリーダーの存在有無を確認
- リーダーがいなければ一時 znode を作成してリーダーキーの取得を試み、ZooKeeper は単一ノードのみがキーを取得できることを保証するため 多重プライマリ形成を防止
- すでにリーダーがいれば、自身をレプリカとして構成してストリーミングレプリケーションを開始
- ネットワーク分断(network partition) 時にリーダーまたは ZooKeeper との接続を失ったレプリカはクラスタ状態を確認できず、Patroni は当該ノードを一時停止または降格
- リーダーが接続を失うと、Patroni は ZooKeeper と連携して適格なスタンバイ 1 台だけがリーダーロックを取得するようにし、部分的なネットワーク障害でも制御されたフェイルオーバーを保証
- 接続回復後、元のリーダーはリーダーロックの再取得に失敗すると自らスタンバイへ降格し、split brain を防止
安全なフェイルオーバーが不可能だった理由
- 単一ライターモデルでは、障害時に Patroni が正常なスタンバイの中から新しいリーダーを選出
- データ損失防止のため、Patroni は昇格前に安全確認を実施し、核心となるのは レプリケーション遅延 が
maximum_lag_on_failoverの閾値以内かどうか- スタンバイがリーダーより遅れていれば、昇格時にデータ欠落や不整合が発生する可能性がある
- ゲームデイではプライマリが接続を失うと、すべてのスタンバイのレプリケーション遅延が閾値を超え、Patroni は正しくフェイルオーバーを拒否
- クラスタに安全に書き込み可能なプライマリが残らなかったのは Patroni のせいではなく、安全に昇格できる候補がなかったため
ストリーミングレプリケーションの 2 つのモード
- ストリーミングレプリケーションでは、リーダーはすべての変更を含む write-ahead log(WAL) をレプリカへ継続的に送信し、レプリカは WAL をローカルに適用して同期を維持
-
非同期レプリケーション (デフォルト)
- リーダーはトランザクションをコミットする前にレプリカの確認を待たない
- 書き込みレイテンシを最小化し、高いスループットを支援
- ただし、リーダー障害時にはプライマリでコミット済みだがまだ複製されていないトランザクションが昇格時に失われる可能性がある
-
同期レプリケーション
- リーダーはクライアントに応答を返す前に、少なくとも 1 つのレプリカからの確認を待機
- 少なくとも 1 台のレプリカが大きく遅れるリスクを大幅に減らし、コミット済みトランザクションが他ノードにも存在することを確認してから応答するため、より強い耐久性を保証
- フェイルオーバー候補が最新状態である可能性が高くなり、データ分岐のリスクなしに昇格可能
ハイブリッドレプリケーション設定
- 耐久性・レイテンシ・スループットのバランスを取るため、ハイブリッドレプリケーションモデルを採用
- leader pool のスタンバイノード は同期レプリケーションに参加し、リーダーは指定された同期スタンバイの確認後に書き込みをコミット
- read replica は非同期レプリケーションを維持 し、読み取り専用トラフィックのみを処理してフェイルオーバー対象ではないため、リーダープールのレプリケーション負荷を限定
- これにより、読み取りレプリカに同じレイテンシコストを課すことなく、フェイルオーバー候補にのみ厳格な耐久性保証を適用
安全なフェイルオーバーのための PostgreSQL・Patroni チューニング
- 同期レプリケーションを有効にするため、PostgreSQL と Patroni の両方でパラメータを調整
-
調整した主要パラメータ
synchronous_mode: Patroni の同期レプリケーションを有効化。trueの場合、synchronous_node_countに従って同期スタンバイの確認後にコミット (デフォルト false → true、Patroni 管理、必須)synchronous_node_count: 同期スタンバイノード数。synchronous_standby_namesのリスト生成に使用 (デフォルト 1 → 1、Patroni 管理、任意)synchronous_mode_strict: 厳格な同期モードを強制。trueで利用可能なレプリカがない場合、非同期への切り替えではなく書き込みをブロック (デフォルト false → true、Patroni 管理、任意)synchronous_commit: PostgreSQL のコミット耐久性設定 (デフォルト on → remote_apply、PostgreSQL 管理、任意)
- 適用後、リーダーは同期スタンバイがデータの受信・適用を確認した後にのみ、クライアントへトランザクション応答を返す
耐久性とレイテンシのバランス
- 同期レプリケーションは耐久性を改善する一方、リーダーが同期スタンバイの確認を待つため書き込みレイテンシが増加し、継続的な負荷下ではスループットにも影響する可能性がある
- 性能への影響は、同期スタンバイ数(
synchronous_node_count) とsynchronous_commitで設定した耐久性レベルに左右される -
synchronous_commitの耐久性レベルごとのトレードオフremote_apply: レプリカが WAL を書き込み、フラッシュし、再生するまで待機。最も強い整合性・最も高いレイテンシon(内部的には remote_flush): レプリカが WAL をディスクにフラッシュするまで待機。強い耐久性だが、スタンバイではまだ読み取り不可remote_write: WAL がレプリカの OS キャッシュ(ディスクではない)に到達するまで待機。低レイテンシだが OS クラッシュに弱いlocal: スタンバイ待機なしでローカルディスクにフラッシュ後コミット。ノード間の耐久性保証なしoff: ローカル WAL フラッシュ前にコミット。最も低レイテンシで、データ損失リスクは最大
同期レプリケーションの性能ベンチマーク
- 各コミットが 1 台以上のスタンバイからの確認を待つため、同期レプリケーションはレイテンシを追加する。影響を定量化するため、PostgreSQL 標準の負荷テストツール pgbench でベンチマークを実施 (Patroni バージョン 3.2.1)
- 単純な読み書き混在をシミュレートする TPC-B トランザクションスイートを使用し、2 つの指標を測定
- 平均レイテンシ: トランザクションあたりの平均処理時間(ms)
- 1 秒あたりのトランザクション数(tps): 接続設定時間を除いた完了トランザクション数
-
テストパラメータ
- スケールファクタ(データベースサイズ)、クライアント数(同時ユーザートラフィック)、スレッド数(CPU・並列性)、トランザクション数(ワークロード強度)を変化させ、本番に近い条件を模擬
- クォーラムコミットモードの同期レプリケーションは今回のベンチマークでは未テスト
-
ベンチマーク結果
- 書き込みレイテンシ増加率:
remote_apply53%、on46%、remote_write38%、local32% - スループット(tps) 減少率:
remote_apply34%、on31%、remote_write27%、local23% remote_applyはレプリカでの WAL 再生・適用まで待機するため、一貫して最も高いレイテンシと最も低いスループットを示したが、最も強い整合性により安全なフェイルオーバーに適していた
- 書き込みレイテンシ増加率:
-
本番適用
- ベンチマーク後、複数の高書き込みクラスタに
remote_applyを導入し、継続的な本番負荷下でもアプリケーションレベルの書き込みレイテンシやスループットに有意な影響はなかった - 性能リスクを緩和するため、データセンターとワークロード階層ごとに段階的ロールアウトを行い、各段階の間にベイクイン期間と継続監視を適用
- 例: 高スループットのリソース処理ワークロードは、同期レプリケーション有効化後も DB 書き込みレイテンシの増加にもかかわらず、処理遅延や下流バックログなしで継続稼働
synchronous_commitはpatronictl edit-configでダウンタイムなしに即時調整でき、超高スループットワークロードのコミット耐久性を迅速に下げられる柔軟性を提供
- ベンチマーク後、複数の高書き込みクラスタに
障害シナリオによるフェイルオーバー検証
- 同期レプリケーションと厳格なフェイルオーバー制御が、データ整合性の保護、split-brain の防止、自動復旧を保証するかを検証
-
シナリオ 1: 同期スタンバイ 1 台の喪失
- 同期スタンバイを失うと、Patroni は別の適格スタンバイを割り当てて同期レプリケーションの維持を試みる
- リーダーノード上の Patroni は
pg_stat_replicationで切断・停滞・遅延したストリーミング接続を検知し、ZooKeeper でレプリカのメンバーシップを追跡 - 正常で適格なストリーミングレプリカの一覧を再計算し、
synchronous_node_countに応じてsynchronous_standby_namesを更新して、同期レプリケーション有効状態のまま動作を継続
-
シナリオ 2: すべての同期スタンバイの喪失
- 動作は
synchronous_mode_strictの値に左右される -
非厳格モード: 書き込み可用性を優先
- Patroni は
synchronous_standby_namesを空にして同期レプリケーションを一時的に無効化し、正常なレプリカが再参加するまでリーダーを非同期に切り替えて書き込みを継続許可
- Patroni は
-
厳格モード: 安全のため書き込みをブロック
- Patroni は
synchronous_standby_namesを*に設定し、明示的な同期スタンバイがなくても PostgreSQL は書き込みトランザクションを受け付けてローカルコミットするが、レプリカが WAL を確認するまでクライアント応答をブロック - 同期レプリケーションに参加可能な適格レプリカが再接続すると、Patroni がそのレプリカに同期スタンバイの役割を割り当てる
- Patroni は
- 動作は
-
シナリオ 3: すべてのスタンバイ・レプリカノードが利用不可
- すべてのレプリカが利用不可で
synchronous_mode_strict = trueの場合、PostgreSQL は少なくとも 1 台の適格レプリカが復帰するまでトランザクション確認を保留 - データ整合性は維持されるが、アプリケーションレベルでは一時的に書き込み不能となる
- すべてのレプリカが利用不可で
-
シナリオ 4: 同期コミット中のリーダー障害
- リーダーが同期スタンバイの確認を待っている途中で、確認受信前に停止するエッジケース
- よくある原因: コミット中のクライアントによるトランザクション取消、リーダー PostgreSQL プロセスのクラッシュ・終了、コミット段階中のネットワーク分断
- PostgreSQL が WAL をローカルにフラッシュしたがスタンバイへの複製に失敗した場合、確認がないためトランザクションはレプリカに現れない
- リーダーが同期スタンバイへ WAL を複製する前にクラッシュし、そのスタンバイが昇格すると、トランザクション損失が起こり得て、元のリーダーと新しいプライマリの履歴が分岐
- 元のリーダーは pg_rewind によりタイムライン分岐点を特定し、データディレクトリを新しいプライマリのタイムラインに合わせて巻き戻して、複製されなかったローカル変更を破棄した上でスタンバイとして再参加
- この動作は Patroni ではなく PostgreSQL の同期コミット内部処理の結果であり、クォーラム設定と確認メカニズムの慎重なチューニング・監視の必要性を浮き彫りにした
-
シナリオ 5: ZooKeeper 利用不可
- ZooKeeper が利用不可になると、Patroni はリーダーシップの確認や新規選出ができず、データ不整合防止のため保守的な動作に切り替える
-
failsafe モード無効時
- ZooKeeper に到達できなくてもリーダーが到達可能で全ノードが正常なら書き込みは継続されるが、リーダーロック TTL の期限までに限られる
- リーダーキー更新ループ時間が過ぎてもロック更新できない場合、Patroni はリーダーを降格してクラスタを読み取り専用に切り替える
-
failsafe モード有効時
- リーダーが ZooKeeper 接続を失うと、Patroni は REST API で全クラスタメンバーへの到達可能性を継続的に確認
- すべてのメンバーにアクセス可能な場合にのみ書き込みを継続し、そうでなければ読み取り専用へ降格
同期レプリケーション下での手動フェイルオーバー・スイッチオーバー
- Patroni はヘルスチェックと ZooKeeper 調整に基づく自動フェイルオーバー・スイッチオーバーに加えて、
patronictlコマンドによる手動操作もサポートするが、同期レプリケーション有効時はすべてのスタンバイが有効候補ではないため、データ整合性保護のためのガードレールが適用される -
非同期スタンバイへのフェイルオーバー
patronictl failover: 選択ノードが非同期なら失敗patronictl switchover: 選択ノードが非同期なら失敗- 同期レプリケーション有効中に非同期ノードへのフェイルオーバーを強制すると、耐久性保証を迂回してデータ損失が起こり得る
-
同期スタンバイを対象とする場合
patronictl failover: 成功し、リーダーが同期スタンバイへ切り替わるpatronictl switchover: 成功し、リーダーと同期スタンバイの間でスムーズなハンドオフを実行
- さまざまな
synchronous_commitモードの動作と Patroni のガードレールを検証した後、高書き込み・高読み取り・混合ワークロードの本番クラスタで同期レプリケーションを有効化し、レイテンシやスループットへの測定可能な影響はなかった - 問題が発生した場合は、
synchronous_mode: false設定によりダウンタイムなしで非同期レプリケーションへ安全に戻すことが可能
DRBD を選ばなかった理由
- 高可用性評価では、ブロックレベル複製システム DRBD(Distributed Replicated Block Device) も検討した。これは PostgreSQL データディレクトリと WAL ファイルを含むボリューム全体をサーバー間でミラーリングし、ほぼリアルタイムのスタンバイレプリカを生成する
- DRBD は PostgreSQL 組み込みのストリーミングレプリケーションより低レイテンシを提供できる可能性があるが、新たなインフラ、監視、運用プレイブックを含む大きなアーキテクチャ変更が必要
- 成熟した Kubernetes ベース構成と PostgreSQL 同期レプリケーションのきめ細かな制御を踏まえ、可視性・柔軟性・運用信頼性に優れるデータベースレベルのレプリケーションを選択
同期レプリケーションの監視
- 同期レプリケーション有効化後は、レプリケーション状態とフェイルオーバー準備状況を綿密に監視しており、とくに 2 つのシグナルが大規模な安定性維持に寄与
-
SyncRep 待機イベント
- プライマリがコミット完了と状態返却の前に同期スタンバイの確認を待つ際に発生し、一部は正常だが、長時間または頻繁な待機はレプリカ性能問題やノード間ネットワーク遅延を示唆
- 重要な理由: 長期待機は書き込みレイテンシ増加とスループット低下を引き起こす
- 追跡対象:
SyncRep・WalSenderWaitForReply待機イベントの継続時間・頻度、およびwait_event:SyncRepタグでフィルタしたpostgresql.activity.waitsDatadog メトリクス(内部的にはpg_stat_activityテーブルのクエリ)から収集
-
同期スタンバイ未検出
- Patroni が正常な同期スタンバイを長時間検出できない場合、クラスタは安全なフェイルオーバー能力を失う
- 重要な理由: 同期スタンバイがなければ、フェイルオーバー時にデータ損失へ脆弱になる
- アラート基準:
patroni_sync_standbyが空の状態が継続すると高可用性(HA) ヘルスアラートを発報。OpenMetrics データから収集し、ネイティブな Datadog 統合はない
- 同期レプリケーションは耐久性を改善する一方、レプリカが異常または到達不能な場合には可用性と性能が低下するため、待機時間とスタンバイ可用性の監視は負荷状況下で可用性と性能を維持するうえで不可欠
設計段階から安全なフェイルオーバーを
- シミュレートされた AZ 障害により PostgreSQL アーキテクチャの致命的な弱点が露呈し、レプリカがリーダーより遅れたことで、ネットワーク障害の回復待ちかデータ分岐を受け入れるかという二者択一を迫られ、本番では受け入れがたいトレードオフとなった
- Patroni ベースの同期レプリケーション採用と耐久性・レイテンシのチューニングにより、劣化したネットワーク条件下でもフェイルオーバーを可能かつ安全に実装し、ベンチマークと反復的な障害シミュレーションを通じて、大規模な性能劣化なしに予測可能な復旧を確認
- 同期レプリケーション障害中に書き込みをブロックすることで、失敗を上流サービスに明示的に露出させ、非同期レプリケーションのように書き込みが静かに失われることなく、再試行やキューイングなどで対処可能になり、障害モードがより可視化され復旧しやすくなる
- 今後はクォーラムベースのコミットモードと、レプリケーション状態に対するより深い可観測性を探っている
まだコメントはありません。