多くのレプリケーション図は、プライマリ、2 つのレプリカ、その間の矢印を描いています。その図は正しいものの、障害対応ではほとんど役に立ちません。いまどのノードが書き込みを受けているのか、どのレプリカが古いデータを返しうるのか、プライマリが応答しなくなったら何が起きるのかがわからないからです。
役に立つレプリケーション図は、4 つの問いに答えます。このガイドでは、PostgreSQL、Patroni、etcd を使ってその問いを順に考え、結果を図にします。同じ問いは、MySQL、ほかのフェイルオーバーマネージャー、マネージドサービスにも当てはまります。
レプリケーション図が答えるべき 4 つの問い
- 書き込みはどこへ行くのか。 プライマリはちょうど 1 つでなければならず、クライアントにはフェイルオーバー後にそれを見つける手段が必要です。
- 読み取りはどれだけ古くなりうるのか。 各レプリカは同期か非同期のどちらかで、非同期のレプリカは遅れます。
- フェイルオーバーは誰が決めるのか。 何かが障害を検知し、レプリカを選び、昇格させる必要があります。
- プライマリが 2 つになるのを何が防ぐのか。 切り離されたプライマリは、別のノードが昇格する前に書き込みの受け付けを止めなければなりません。
同期レプリケーションと非同期レプリケーション
PostgreSQL のストリーミングレプリケーションは、デフォルトでは非同期です。プライマリはスタンバイがデータを受け取る前にコミットを確定させるので、クラッシュすると、まだ届いていなかったトランザクションが失われることがあります。PostgreSQL のドキュメントもはっきり書いています。失われるデータの量は、フェイルオーバー時点のレプリケーション遅延に比例します。
同期レプリケーションでは、コミットが synchronous_standby_names に列挙した 1 つ以上のスタンバイを待ちます。どこまで待つかは synchronous_commit で決まります。
synchronous_commit |
同期スタンバイが次の状態になったらコミットが返る |
|---|---|
remote_apply |
適用済み。スタンバイ上のクエリからも見える |
on(デフォルト) |
永続ストレージにフラッシュ済み |
remote_write |
OS に書き込み済み。まだフラッシュはしていない |
local |
スタンバイは待たない。ローカルのフラッシュだけを待つ |
off |
何も待たない。ローカルディスクも待たない |
代償はレイテンシです。すべてのコミットにスタンバイとの往復が含まれるので、同期スタンバイは別のリージョンではなく、近いゾーンに置きましょう。また、唯一の同期スタンバイが消えると、コミットはいつまでも待ち続けることがあります。候補を複数指定するか、設定の変更をフェイルオーバーマネージャーに任せましょう。
読み取りレプリカと読み取りのルーティング
非同期レプリカは、データが遅れうることをコードが理解している限り、レポートや読み取りの多いエンドポイントに適しています。プライマリでは、pg_stat_replication ビューが各スタンバイの write_lag、flush_lag、replay_lag を表示します。replay_lag は、コミットがレプリカで見えるようになるまでのおおよその時間です。
古いデータを読んでよいかどうかは、クエリごとに決めましょう。フォームを保存した直後にページを再読み込みしたユーザーは、プライマリから読むべきです。ダッシュボードならレプリカから読めます。
フェイルオーバー:誰が決めるのか
PostgreSQL は、プライマリの障害を自分で検知することも、スタンバイを自分で昇格させることもしません。ドキュメントはそれを外部ツールに任せています。よく選ばれるのが Patroni です。各ノードで Patroni エージェントが動き、エージェント同士は etcd のような分散設定ストアを通じて状態を共有します。
- プライマリは TTL 付きのリーダーキーを持ち、ループごとに更新します(
loop_wait、デフォルトは 10 秒)。 ttl秒(デフォルトは 30 秒)のあいだキーを更新できないとキーは失効し、条件を満たすレプリカがリーダーの座を争います。- 最も適した候補がキーを取って自らを昇格させ、ほかのレプリカはそれに追従します。
クライアントは、プロキシを通じて新しいプライマリを見つけます。Patroni の REST API は、リーダーキーを持つノードでだけ GET /primary に HTTP 200 を返すので、HAProxy は「はい」と答えたノードに書き込みを送れます。
listen primary
bind *:5000
option httpchk OPTIONS /primary
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server pg-a 10.0.1.10:5432 check port 8008
server pg-b 10.0.2.10:5432 check port 8008
server pg-c 10.0.3.10:5432 check port 8008
ポート 5001 の 2 つ目のリスナーは /replica?lag=16MB を確認し、WAL が 16 MB 以上遅れているレプリカを外します。HAProxy は複数のマシンで動かしましょう。そうしないと、なくそうとしていた単一障害点に HAProxy 自身がなってしまいます。
スプリットブレイン:プライマリが 2 つになるのを防ぐもの
書き込みを受け付けるプライマリが 2 つあれば、履歴が 2 つに分かれ、片方は捨てることになります。PostgreSQL のドキュメントは対策を STONITH と呼んでいます。古いプライマリに、もうプライマリではないと伝えなければならないということです。Patroni は何重にも防御を重ねます。
- リース。 リーダーキーを更新できないプライマリは書き込みの受け付けを止めます。Patroni が PostgreSQL を停止するか降格させます。ストア自体に
retry_timeoutを超えて到達できない場合も、DCS フェイルセーフモードが有効でない限り、Patroni はプライマリを降格させます。 - ウォッチドッグ。 Patroni のプロセスがハングすると、Linux のウォッチドッグがマシンをリセットします。Patroni は、TTL が切れる 5 秒前に発動するようにウォッチドッグを設定します。
- クォーラム。 etcd が書き込みを受け付けるには、メンバーの過半数、つまり 3 台中 2 台が必要です。3 つのゾーンに 3 台を置けば 1 ゾーンの喪失に耐えられ、分断された少数派は誰も選出できません。
- 管理するプロセスは 1 つだけ。 PostgreSQL を起動してよいのは Patroni だけです。クラッシュ後に Postgres を勝手に再起動するシステムサービスがあると、2 つ目のプライマリが戻ってくることがあります。
古いプライマリが戻ってきたら、レプリカとして再参加させる必要があります。pg_rewind は、変更のないデータブロックをコピーせずに、データディレクトリを新しいプライマリと再同期します。use_pg_rewind が有効なら、Patroni がこれを実行します。
# Patroni dynamic configuration (patronictl edit-config)
ttl: 30
loop_wait: 10
retry_timeout: 10
synchronous_mode: on # promote only a node that has every acknowledged commit
postgresql:
use_pg_rewind: true
parameters:
wal_log_hints: "on"
# Local patroni.yml tags
# pg-a: noloadbalance: true (failover target, no reads)
# pg-c: nosync: true, nofailover: true (reads only)
synchronous_mode を有効にすると、Patroni は、成功として報告されたすべてのコミットを持っていると確認できたスタンバイだけを昇格させます。代償は可用性です。同期スタンバイが 1 台も接続していないときにプライマリが落ちると、Patroni は誰も昇格させず、誰かが介入するまで書き込みは止まったままになります。
レプリケーションはバックアップではない
DROP TABLE は数ミリ秒ですべてのレプリカに伝わります。継続的な WAL アーカイブと定期的なベースバックアップを用意しましょう。これがあれば、ベースバックアップ以降の任意の時点へのポイントインタイムリカバリができます。リストアは定期的にテストしてください。
図
ダイアグラムの内容
- Order service
- HAProxy
- AZ a
- AZ b
- AZ c
- pg-a · sync standby
- pg-b · primary
- pg-c · async replica
- etcd
- WAL archive + base backups
- SQL
- writes :5000
- reads :5001
- WAL (sync)
- WAL (async)
- leader key, TTL 30 s
- races if key expires
- archived WAL
図の読み方は次のとおりです。
- 書き込み。 注文サービスは HAProxy に接続します。ポート 5000 は、
/primaryに 200 を返す唯一のノードである pg-b に向かいます。 - 読み取り。 ポート 5001 は、別のゾーンにある非同期レプリカの pg-c に向かいます。pg-a は読み取りを受けません。
- レプリケーション。 pg-b は WAL を pg-a に同期で(実線)、pg-c に非同期で(破線)送ります。
- フェイルオーバー。 pg-b は etcd のリーダーキーを更新します。キーが失効すると、同期スタンバイの pg-a がそれを取りに行きます。pg-c には
nofailoverのタグが付いています。 - リカバリー。 pg-b は WAL とベースバックアップをオブジェクトストレージにアーカイブします。
- 目標値。 左上のメモには、RPO、RTO、遅延の許容量、バックアップの保持期間が書かれています。
レプリケーション図に書くべきラベル
- 各レプリケーションの矢印のモード(同期か非同期か)と、非同期レプリカの遅延の許容量。
- 各レプリカの役割: フェイルオーバー候補、読み取りレプリカ、またはその両方。
- リーダーロックの置き場所と TTL、あるいはその役割を担うマネージドサービス。
- クライアントがプライマリを見つける方法: プロキシ、DNS 名、仮想 IP など。
- 障害ドメイン: コンセンサスのメンバーがどこで動いているかも含め、アベイラビリティーゾーンをフレームで示します。
- RPO と RTO の目標。レビュアーが設計をこの目標と照らし合わせられるようにします。
マネージドサービスが肩代わりしてくれること
マネージドデータベースも同じ問いに答えますが、答えの一部は固定されています。Amazon RDS の Multi-AZ DB インスタンスでは、スタンバイは同期で、別のゾーンにあり、読み取りには使えません。フェイルオーバーは DNS レコードをスタンバイに切り替え、通常 60〜120 秒かかります。Multi-AZ DB クラスターには読み取り可能なスタンバイが 2 つあり、準同期レプリケーションを使い、通常 35 秒未満でフェイルオーバーします。図の上では、エンドポイントが HAProxy の代わりになり、AWS が Patroni と etcd の代わりになります。それでも、遅延、読み取りのルーティング、目標値のラベルは自分で書きます。
フェイルオーバーをテストする
意図的にプライマリを切り替えましょう。たとえば、メンテナンスウィンドウのたびに patronictl switchover を実行します。PostgreSQL のドキュメントは定期的な切り替えを勧めています。それがフェイルオーバーの仕組みのテストにもなるからです。書き込みが失敗する時間を測り、HAProxy が追従することを確かめ、確定済みのコミットが欠けていないことを確認します。リージョンをまたぐフェイルオーバーについては、AWS マルチリージョンアーキテクチャ図を参照してください。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- アベイラビリティーゾーン用に基本パックの フレーム を 3 つ追加し、それぞれにデータパックの Postgres ノードを置きます。pg-a(同期スタンバイ)、pg-b(プライマリ)、pg-c(非同期レプリカ)です。
- ゾーンの上に、注文サービス用として C4 モデルパックの コンテナ を、HAProxy 用としてネットワークパックの ロードバランサー を追加します。
- ゾーンの下に、etcd 用として C4 モデルパックの コンテナ を、WAL アーカイブ用としてデータパックの オブジェクトストア を追加します。
- エッジにラベルを付けます。「writes :5000」「reads :5001」、実線の「WAL (sync)」、破線の「WAL (async)」、pg-b から etcd への「leader key, TTL 30 s」、そしてリーダー争い用に pg-a から etcd への破線のエッジです。
- RPO、RTO、遅延の許容量、バックアップの保持期間を書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 11 個で、Free プランの範囲に収まります。クラスターの変化に合わせて図を最新に保つ方法は腐らない構成図を、一般的な手順はシステム設計図の例を参照してください。
よくある質問
読み取りレプリカはバックアップになりますか? なりません。削除や誤ったマイグレーションもレプリケートされます。WAL アーカイブ付きのバックアップがあれば、ミスの前の時点に戻せます。
同期レプリケーションならデータ損失はゼロですか? 同期スタンバイが確認したコミットについては、フェイルオーバーでそのスタンバイだけを昇格させる限りゼロです。同期スタンバイが 1 台も接続していないあいだに受け付けたコミットは、プライマリにしか存在しません。
なぜ etcd は 2 台ではなく 3 台なのですか? etcd が書き込みを受け付けるには過半数が必要です。2 台ではどちらも失えませんが、3 台なら 1 台を失っても動き続け、ネットワーク分断時にも片側で過半数を保てます。
プライマリを見つけるのに、プロキシの代わりに DNS を使えますか? 使えます。マネージドサービスはそうしています。DNS の TTL を短くし、クライアントが名前解決の結果を長くキャッシュしないようにしましょう。AWS は、JVM の DNS キャッシュの TTL を 60 秒以下にすることを推奨しています。
フェイルオーバーはどのくらいの頻度でテストすべきですか? チームが信頼できる程度に定期的に行います。たとえば、メンテナンスウィンドウのたびに計画的なスイッチオーバーを行い、それに加えてステージングで計画外の障害テストを行います。