信頼性の高い計画外フェイルオーバーの設計
Admin はプライマリが本当に死んだのか、それとも見えないだけなのかを判断できない。そこで Neki はレプリカに問い合わせ、その場で凍結し、最も多く WAL を持つものを昇格させる。
日本語
コピー

計画的スイッチオーバーを中断なしで行うための設計では、参加者全員が健全なときに Neki が Postgres プライマリをどう移行するかを扱った。コントロールプレーンの Admin はプライマリがいつ交代するかを事前に把握している。だからシャードを検査し、ターゲットを選び、中断が起きる前に引き継ぎを準備できる。これが「計画的スイッチオーバー」の「計画的」たる所以だ。
計画外のフェイルオーバーには、最初からこうした利点がない。予期しない障害モード(ネットワークリンクの切断やサーバーハードウェアの故障など)で発生する。ただし、適切に対処できる範囲の障害ではある。
Neki の Admin システムには、プライマリが本当に故障したのか、それとも Admin の位置から到達できないだけなのかを判別する手段がない。昇格先は事前に選ばれておらず、プライマリに最終的な WAL 位置を問い合わせることもできない。その間、アプリケーションは確実に書き込めない。
このシナリオでは答えるべき問いが2つある。
- プライマリは実際に利用不能になっているのか?
- どのレプリカなら安全に引き継げるのか?
プライマリが死んだとみなすのはいつか
1つの Neki シャードは Postgres プライマリと複数のレプリカで構成される。プライマリが書き込みを受け付け、各レプリカがプライマリの WAL を継続的に受信して再生する。プライマリが故障したら、いずれかのレプリカが引き継ぐ必要がある。
シャードのプライマリが P、レプリカが R1、R2、R3 だとしよう。Admin が P にヘルスチェックを実行する。結果は……応答なし!
ここで、そろそろレプリカを昇格させるべきではと疑問に思うかもしれない。答えはまだだ。P はクラッシュしているかもしれないし、ネットワークから隔離されているかもしれない。あるいは障害が Admin と P の間にしかなく、アプリケーションとレプリカの通信は正常ということもありうる。
よくある手法は、Admin がプライマリへのヘルスチェックを複数回繰り返すことだ。何度か試してもプライマリが応答しなければ、死亡と判定する。繰り返しは一時的なパケットロスには効くが、Admin がネットワーク分断の隔離側にいる場合、分断が続く限り何度繰り返しても無駄だ。
他の高可用システムはリースを使う。プライマリ上のエージェントが分散ストレージに定期的に更新するキーを保持し、キーが期限切れになったらプライマリ故障とみなす。成熟した設計だ。Neki にも分散トポロジサービスはあるが、プライマリの健全性判定にリースは使っていない。
Neki はレプリカに尋ねる。
各レプリカは、pg_stat_wal_receiver から見た物理レプリケーションの状態を報告する。これでいくつかの状況を切り分けられる。
- Admin が
Pに接続できなくても、レプリカがそこからストリーミングレプリケーションを受け続けているなら、その証拠だけではフェイルオーバーを起こすには足りない。 - Admin が
Pに接続できず、到達可能なレプリカが少なくとも1つレプリケーションの途絶を報告し、それに反する報告をどの到達可能なレプリカも出していなければ、Neki はPが死亡したと判定して復旧を試みる。 - Admin が
Pにもどのレプリカにも接続できない場合、Neki はフェイルオーバーを実行しない。裏付けもなく、昇格できる候補も見えないからだ。
最後のケースで Admin にできるのは待つことだけだ。1回の失敗したヘルスチェックだけでレプリカを昇格させると、書き込み可能なプライマリが2つできてしまうおそれがある。
レプリカ側の視点が重要な理由
Neki のデフォルトの同期永続化ポリシーは Postgres を次のように設定する。
synchronous_standby_names = 'ANY 1 (R1, R2, R3)'
synchronous_commit = on では、P は条件を満たすレプリカが少なくとも1つ WAL を永続化してフラッシュするまでコミットを確認しない。確認を担当するレプリカはコミットごとに変わりうる。R1 が T でコミットを確認し、次のコミットは R3 が T+1 で確認する、といった具合だ。
フェイルオーバーが起きる状況を見よう。Admin は P に接続できず、どのレプリカも接続できない。P はクラッシュしているかもしれないし、動いていて Admin ノードから到達できないだけかもしれない。どちらにせよ、ACK を返せるレプリカがないため P は同期コミットを完了できない。P が生きていても、書き込みは実質的にブロックされている。レプリカを昇格させれば、書き込めなくなったこのプライマリを置き換えることになる。
一部のレプリカも到達不能な場合、それらの視点は不明であり、そのうちの1つ(たとえば R1)がまだ P に接続している可能性がある。したがって P が書き込みを受け付けていないとは断定できない。
両者が正常に協調したまま、Admin や他のレプリカから隔離されているだけかもしれない。これは ANY 1 の契約が許容する範囲内だ。緩和策として、Neki はアベイラビリティゾーンをまたぐレプリカからのレプリケーション確認だけを受け付けるようにし、両者が同時にネットワーク分断される可能性を下げている。
到達可能なレプリカが少なくとも1つレプリケーションの途絶を報告し、反する報告をどのレプリカも出していなければ、この状況でも Neki はフェイルオーバーを実行する。
Neki は非同期永続化もサポートする。非同期モードでは P はレプリカの書き込み確認を待たないため、書き込み受付を停止したと推論することはできない。プライマリが到達不能で、すべてのレプリカが切断されているのは非常に強いシグナルだが、保証ではない。
障害の検出
検出の仕組みと速度は障害の種類によって変わる。よくあるシナリオをいくつか見ていく。
Postgres プロセスが何らかの理由でクラッシュし、エラーで終了したとしよう。Neki は Postgres を PostgresManager 経由で実行する。このコンポーネントが Postgres プロセスの起動、停止、設定、その他の制御を担う。goroutine が Postgres プロセスを待っており、プライマリ P 上のプロセスが終了すると PostgresManager は即座にそれを知る。Admin に PROCESS_EXITED イベントを送り、Admin は P を到達不能とマークしてシャード分析を開始し、P のレプリカと突き合わせて検証する。この時点では検出はまだ不完全で、レプリカの確認を待つ必要がある。
別のよくあるシナリオは接続拒否だ。ファイアウォールが P とシャードの残りの間に立ち、そのトラフィックを能動的に拒否しているのかもしれない。この場合は単純で、Admin のヘルスチェックもレプリカのレプリケーション接続も即座にエラーで失敗する。
パケットロスはもっと厄介だ。P が動いているホストがダウンした場合や、ネットワークがそのトラフィックを黙って捨てている場合、誰もエラーを受け取らない。接続は、相手側がとうに存在しなくなった後でも、確立済みに見え続けることがある。
そして Postgres プロセスのフリーズは、おそらく最も難しいケースだ。サーバーのカーネルは TCP レベルでは依然として応答しているので、Postgres が何もしていなくても接続は健全に見える。
レプリカはこれらの状況をどう見分けるのか。見分ける必要はない。Neki は Postgres のレプリケーション設定を調整し、どの状況も、ここに挙げていない多くの組み合わせも、同じ結末を迎えるようにしている。レプリカがレプリケーションの中断を報告するのだ。
- レプリケーション接続における短めの TCP keepalive 設定と
tcp_user_timeoutがパケットロスを捕捉する。 wal_receiver_timeoutはフリーズしたプロセスを捕捉する。これは TCP だけではできない。Pが数秒間何もデータを送らなければ、レプリカは接続を閉じ、レプリケーションの中断を報告する。
フェイルオーバーの実行を決める
Neki が P は死んだと判断すると、そのシャードには新しいプライマリが必要になる。適切な候補レプリカを選んでプライマリに昇格させ、同時に残りのレプリカを再設定して新しいプライマリを受け入れさせる。ただし、ここには問題が二つある。
Pはいつでも「生存」状態に戻りうる。- レプリケーションが中断するまでに、各レプリカが受け取った WAL の量はそれぞれ異なりうる。
レプリカを切断する
レプリケーションはすでに中断しているのに、なぜわざわざレプリカを切断するのか。P が戻ってくるかもしれないからだ。Admin がフェイルオーバーの実行を決める前に戻ってきたのなら害はない。レプリカは再接続し、短い揺らぎの後でシャードは健全な状態に戻る。問題は、Admin がフェイルオーバーを決めた後に P が戻ってきた場合だ。その時点で Admin はシャードを新しいプライマリへ収束させようとしているが、切れた接続は依然として再接続できてしまう。WAL 位置が再び動き始め、P はまた、そのコミットを確認できるレプリカを持つことになる。
MySQL には、レプリケーションを完全に止めずにプライマリからの変更受信を管理者が停止できるコマンドがある。STOP REPLICA IO_THREAD だ。Postgres の物理レプリケーションには対応するコマンドがない。Neki は、到達可能な各レプリカの primary_conninfo に小さく可逆な変更を加え、P のホスト名の前に // を付ける。レプリカは P を見つけられなくなり、再接続もできなくなる。
計画的スイッチオーバーでは、引き継ぎの前にレプリカの WAL 位置を凍結するが、これも同じ仕組みを使っている。
レプリカの切断には二つの効果がある。
第一に、P が不意に健全な状態へ戻るのを防ぐ。P が戻ってきても、切断済みのレプリカはそれに再接続しない。ANY 1 の下では、P はレプリカの確認なしにコミットを完了できないため、書き込みを一切受け付けられなくなる。
第二に、レプリカの WAL 位置を凍結する。どのレプリカも P からそれ以上 WAL を受け取れなくなれば、各レプリカが報告する位置が最終値になる。これらの位置は WAL の現在のビューを記録した安定したスナップショットであり、これをもとに新しいプライマリを選ぶ。
WAL 位置から選ぶ
計画的スイッチオーバーでは、Admin は旧プライマリがまだ健全なうちに、新しいプライマリをあらかじめ選んでおく。計画外のフェイルオーバーにはその余裕がなく、生き残った WAL の中から選ぶしかない。
レプリカが次の位置で止まっているとしよう。
R1 receive: 120 replay: 120
R2 receive: 125 replay: 122
R3 receive: 123 replay: 123
受け取った位置の最大値は 125 で、R2 にある。Admin はまず、125 まで再生済みの昇格可能なレプリカを探す。このスナップショットには存在しないので、少し待ってからもう一度調べる。R2 がローカルストレージの WAL を 125 まで再生すれば、それが候補になる。
待機ウィンドウ内に追いつくレプリカがなければ、Admin は receive_lsn が最大の昇格可能なレプリカを選び、次の判断材料として replay_lsn を使う。
Admin がレプリカ間で最大値を取れるのは、WAL が順序を持つからだ。位置 125 を受け取ったレプリカは、それより前の WAL も必ず受け取っている。Neki は、個々のコミットが R1、R2、R3 のどれで確認されたかを知る必要はない。確認済みの履歴を WAL がカバーしている、生き残った候補が一つあればよい。
最新の WAL が、昇格できないレプリカ上にあることもある。ANY 1 が保証するのは、確認済みの各コミットがいずれかのレプリカに届いたことであって、そのレプリカがまだ存在することではない。P がそのレプリカと一緒に故障すれば、そのコミットは失われる。アベイラビリティゾーンをまたいだ確認を要求すれば、この可能性は大幅に下がる。代償はコミットのレイテンシだ。
昇格時に何が起きるか
Admin は新しいプライマリ上に他のレプリカ用のレプリケーションスロットを作成し、Postgres でそれをプライマリに昇格させる。続いてそれをシャードのプライマリとしてマークし、Neki の Router が書き込みをそこへ送り始める。最後に、Admin は他のレプリカを新しいプライマリに向け直す。
受け取った位置が最大のものを選ぶことには、ここでもう一つ見返りがある。他のすべてのレプリカは新プライマリの WAL のプレフィックスを保持しているので、そのまま新プライマリを追って新しいタイムラインに入れる。例外は、候補より位置が進んでいるのに昇格できないレプリカ、たとえばメンテナンス中のものだ。Neki はそれが追従を続けられるようにする前に、まずロールバックするか再構築しなければならない。
書き込みはすぐには戻らない。新しいプライマリは ANY 1 で動作するため、最初のいくつかのコミットは、レプリカが接続して確認するまで完了しない。
P の場合
Admin は P を TEARDOWN としてマークする。TEARDOWN ノードはトラフィックを担わず、レプリケーションにも参加せず、Admin も監視しなくなる。シャードにとって P はもはや存在しない。たとえ後で再び動き出したとしても。
計画的スイッチオーバーが完了すると、Admin は P を新しいプライマリのレプリカにする。計画外のフェイルオーバーではそうしない。P には新しいプライマリが持っていない書き込みが残っている可能性があり、再び到達可能になったというだけでは信頼したくないからだ。
だからといって、すぐに始末しなければならないわけではない。落ちたプライマリは、問題を突き止めるための最良の証拠であることが多い。ログ、最後の WAL、ディスクの状態だ。障害の振り返りのために残しておきたいこともある。破棄や回収、あるいは明示的な判断を経てレプリカとしてシャードに戻すのは、その後でいい。
まとめ
計画外のフェイルオーバーは不確実さから始まる。Admin には、P がすでに死んでいるのか、それとも自分の視界から外れただけなのかがわからない。Neki は Admin の判断だけで動かない。各レプリカに問い合わせ、こちらから到達できるレプリカがそろって P はもういないと認めたときにだけ、フェイルオーバーを実行する。
次に、P については最悪を想定する。いつ戻ってきてもおかしくない。レプリカを切り離しておけば、戻ってきた P がこれらのレプリカを取り戻すのを防ぎ、同時にレプリカの WAL を凍結できる。後継者には最も高い WAL 位置を選ぶ。これで昇格可能なレプリカ上の確認済みコミットをすべて守れ、残りのレプリカも追従しやすくなる。
自動復旧の範囲を超える障害もある。Admin は到達できないレプリカを昇格できないし、それを保持していたすべてのマシンとともに消えた WAL も復元できない。
Admin 自身が故障することもあるが、それはまた別の記事の話だ。