2つの信頼性の課題の間には、チームがしばしば混同してしまう明確な境界線がある。リトライは不良なリクエストを処理するもので、1回の呼び出しが失敗したら再試行する。フェイルオーバーは不良な依存関係を処理するもので、コンポーネント全体が動作を停止したら、それを迂回する。load balancing and retry architectureの記事では前者を扱った。作業がどのようにアイデンティティにマッピングされ、リトライ層が個々の失敗をどう分類し復旧させるかについてだ。今回は後者を扱う。これは別の問題で、リトライループが助けにならない状況、つまりリトライ先の対象そのものがダウンしているときにパイプラインが何をすべきかという話だ。
複数の市場で規模を持って収集を行うデータエンジニアリングチームにとって、これはインシデント中に段階的に劣化するパイプラインと、丸一日にわたって欠損または誤ったデータを黙って生成し続けるパイプラインとの違いになる。レジデンシャルプロキシのゲートウェイ上ではIPを自分で管理しないため、フェイルオーバーはアドレスの入れ替えの話ではなく、個々のリクエストより上位のレベルで発生する失敗に対する設計の話になる。
まず、失敗ドメインに名前をつける
実際に何が失敗するのかを列挙しなければフェイルオーバーは構築できない。プロキシを利用したパイプラインは6つの異なるレベルで失敗し、そのうち最初の2つだけがすでに対処済みだ。
| 失敗 | 誰が対処するか |
|---|---|
| 単一のリクエスト(タイムアウト、一時的なエラー) | リトライ層 |
| 単一の出口IPが不調になる | ゲートウェイ(サーバー側のローテーション) |
| ターゲット全体があなたのパターンに対してブロックの波を起こす | あなた |
| ジオ/市場の劣化(1つの国で品質や可用性が低下する) | あなた |
| ゲートウェイ/プロバイダー(エンドポイントに到達不能、認証失敗、インシデント) | あなた |
| 自分のインフラ(リージョン、ワーカー、キューが停止する) | あなた |
よくある誤りは、リトライが最上段より広い範囲をカバーすると仮定することだ。あなたのトラフィックパターン全体をブロックの波で叩いているターゲットに対してより強くリトライしても、復旧するのではなく悪化させるだけだ。インシデントが発生しているゲートウェイに対してリトライを続けても、時間を浪費するだけだ。フェイルオーバーは3行目から6行目までのための設計だ。
あなたが実際に制御できる信頼性の基本要素
IP管理はゲートウェイ側にあるため、フェイルオーバーのツールキットは適切な粒度で適用される少数の基本要素にすぎない。
失敗ドメインごとのヘルスシグナル。 成功率、レイテンシ、ブロック率を、全体だけでなくターゲットごと、ジオごと、プロバイダーごとに追跡する。全体の成功率95%という集計値は、1つの市場が20%に落ち込んでいることを隠してしまうことがある。失敗を見ることができるものだけをフェイルオーバーさせることができる。測定方法についてはhow to test proxy speed, success rate, and location accuracyを参照してほしい。
失敗ドメインごとのサーキットブレーカー。 load balancingの記事ではホストごとのブレーカーを紹介した。フェイルオーバーはこれを一般化する。すなわち、ターゲットごと、ジオごと、プロバイダーごとのブレーカーだ。あるドメインのヘルスが崩壊したら、すでに拒否している依存関係を叩き続けるのではなく、そこへの攻撃を止めてフォールバックへ切り替える。
セカンダリパス。 フェイルオーバーは、フェイルオーバー先がなければ意味がない。フォールバックのジオ、フォールバックのプロバイダー、あるいは明示的な劣化モードだ。失敗に対する唯一の応答が「同じものを再試行する」であれば、それはフェイルオーバーではなくビジーループだ。
耐久性のあるキュー。 障害発生時に処理中だった作業は、その障害を乗り越えて存続しなければならない。インシデントによって作業項目が失われるのであれば、フェイルオーバーはその損失を隠蔽することで状況を悪化させたことになる。
マルチリージョントポロジー: 失敗を広げず、閉じ込める
中核となる構造的な考え方は、各市場はそれ自体が独立した失敗ドメインであり、トポロジーはそれを維持すべきだというものだ。ドイツのトラフィックに対するブロックの波が、米国での収集を停滞させてはならない。
つまり:
- 市場ごとにワーカーを分割する。 ジオごとに独立したワーカープール(少なくとも独立したキューとリミッター)を用意し、1つのリージョンの劣化が他のリージョンのキャパシティを消費できないようにする。これはload balancingの記事で述べたホストごとの分離の原則を、1段階上のリージョンレベルに適用したものだ。
- リージョンスコープのサーキットブレーカーと同時実行数。 各市場は独自のブレーカーと独自の同時実行予算を持つ。
deがトリップしても、usとgbは影響を受けずに動作を続ける。 - グローバルな結合をしない。 リージョン間で共有される単一のレートリミッターや単一の共有リトライ予算は、独立した失敗ドメインを結合してしまう。これはまさに避けるべきアンチパターンだ。ある市場のインシデントは他の市場には見えないようにすべきだ。
その見返りは、部分的な失敗が部分的なままにとどまることだ。「パイプラインがダウンしている」ではなく、「ドイツの収集は劣化してフェイルオーバー中だが、他はすべて動作している」という状態になる。これは深夜3時のページ通知ではなく、運用可能な状態だ。
プロバイダーレベルのフェイルオーバー、正直に言うと
本格的な信頼性レビューが問う気の重い質問がここにある。ゲートウェイそのものにインシデントが起きたらどうするか。認証が失敗し始め、エンドポイントに到達できなくなり、品質がネットワーク全体で崩壊する。ジオの分離はこの場合、何の助けにもならない。すべてのリージョンが同じプロバイダーを経由しているからだ。
正直な答えは2つの部分に分かれる。
大半のチームはマルチプロバイダーのフェイルオーバーを必要とせず、早まって追加すべきではない。 2番目のプロバイダーは統合の表面積、課金、一貫性の問題(ジオとセッションのセマンティクスはプロバイダー間で異なるため、フェイルオーバーしたリクエストは微妙に異なる動作をする可能性がある)を倍増させる。大多数のパイプラインでは、品質の高い単一のresidential proxyプロバイダーと、健全な内部フェイルオーバー(ヘルスベースのブレーカー、劣化モード、耐久性のあるキュー)を組み合わせれば、実際のリスクをカバーできる。まれに発生するプロバイダーのインシデントに備えて2番目のプロバイダーを追加し、日常的な複雑さを持ち込むのは、多くの場合割に合わない取引だ。
信頼性目標が本当にそれを正当化する場合、つまり厳格な鮮度SLAを持つ高価値のパイプラインでは、これを正しく行う。プロバイダーを薄いインターフェースの背後に抽象化し、フェイルオーバーをコードの書き換えではなく設定変更にする。
class ProxyProvider: def proxy_url(self, geo: str, session: str | None) -> str: ... def healthy(self) -> bool: ...
class Gateway: def __init__(self, primary: ProxyProvider, secondary: ProxyProvider | None = None): self.primary, self.secondary = primary, secondary
def resolve(self, geo, session=None): # Prefer primary; fail over only when its breaker is open. if self.secondary and not self.primary.healthy(): return self.secondary.proxy_url(geo, session) return self.primary.proxy_url(geo, session)重要なのはコードそのものではなく、その形だ。プロバイダーはインターフェースの背後にある交換可能な依存関係であり、ヘルスによってゲートされ、プライマリのブレーカーが開くまでフォールバックは休止したままだ。今日単一のプロバイダーを運用していても、このインターフェースに沿って構築するコストはわずかであり、後でセカンダリを追加する際にパイプラインの書き換えではなく設定変更になる。必要になる前に2番目のプロバイダーを購入しないこと、しかし扉は開けておくこと。
段階的な劣化: 誤っているが正直であることは、欠損よりも優れている
本当に新しいデータを収集できない場合、何もない状態へフェイルオーバーすることが最良の選択肢であることはまれだ。意図的に劣化させる。
- 最後に確認された正常な値を、stale(古い)としてマークして提供する。 多くの用途では、
stale: trueとフラグ付けされた昨日の価格の方が、欠損よりも有用だ。ただし下流がそれが古いことを認識している必要がある。古いデータを現在のものとして黙って渡してはならない。それはデータ品質の失敗であり、データが人に関する意思決定を左右する場合は正確性の失敗でもある。 - 優先度に応じて負荷を落とす。 部分的な障害でキャパシティが制約されている場合は、すべてを均等に失敗させるのではなく、高価値のターゲットを収集し、ロングテールを後回しにする。
- ウィンドウを広げる。 カバレッジを落とすのではなく、一時的に鮮度要件を緩和する。少し古い完全なデータセットは、新しいが部分的なデータセットよりもしばしば優れている。
劣化は事故ではなく、第一級のモードだ。各データセットにおいて「劣化」が何を意味するかを事前に決めておき、それを明示的で観測可能な状態にする。
耐久性とバックプレッシャー
フェイルオーバーは、処理中の作業が障害を乗り越えて存続する場合にのみ機能する。2つの規則がある。
何も失われない。 作業項目は耐久性のあるキューに存在する。失敗した項目はリトライキューまたはデッドレターに送られ、依存関係が復旧したときにドレインされる。無に帰すことはない。deがフェイルオーバーすると、その保留中の作業は待機し、ブレーカーが閉じたときに再実行される。
リプレイは安全である。 作業項目を冪等にし、復旧後に再実行しても二重カウントや破損が起きないようにする。これはload balancingの記事が依拠している冪等性と同じものであり、これがフェイルオーバーからの復旧をきれいなものにし、突合作業の悪夢にしない要因だ。
フェイルオーバーをテストせよ、そうでなければそれは存在しない
実際のインシデント中に初めて実行されるフェイルオーバーのパスは、フェイルオーバーのパスではない。それは善意で作られた負債だ。ステージング環境で意図的に障害を注入する。
- あるリージョンのプロバイダーをデッドなエンドポイントに向け、そのブレーカーがトリップし、フェイルオーバーが発生し、他のリージョンが影響を受けないことを確認する。
- ターゲットがブロックを返すことをシミュレートし、ターゲットごとのブレーカーが開き、劣化モードが起動することを確認する。
- 実行中にワーカーやキューを停止し、復旧時に作業が失われていないことを確認する。
パイプラインが依存関係を失って、それでも持ちこたえるところを見たことがなければ、それができるかどうかはわからない。
失敗のために構築された可観測性、単なるヘルスではなく
集計スループットしか表示しないダッシュボードは、1つの市場が静かに死んでいる間も緑色のままだろう。失敗ドメインのために計測する。
- 鮮度を含むSLO、成功率だけでなく。データの遅延は、静かに停止したリージョンを捕まえる指標だ。
- ドメインごとのヘルスに対するアラート、ターゲットごと、ジオごと、プロバイダーごとに。単一の失敗している市場が、丸一日のデータ欠損になる前にあなたにページ通知を送るようにする。
- フェイルオーバーイベントを第一級のテレメトリとして扱う。 ブレーカーがトリップしたときやリージョンが劣化したとき、それは記録し、アラートし、レビューすべきイベントであり、静かな内部状態ではない。
FAQ
リトライとフェイルオーバーの違いは何ですか? リトライは、同じ依存関係に対して失敗した単一のリクエストを再試行する。フェイルオーバーは、それ自体が失敗した依存関係、ブロックの波を起こしているターゲット、劣化したジオ、プロバイダーのインシデントを迂回する。リトライは壊れた依存関係を修復できない。フェイルオーバーは、リトライ先の対象がダウンしているときのための設計だ。
フェイルオーバーのために2番目のプロキシプロバイダーが必要ですか? 通常は必要ない。品質の高い単一のプロバイダーと内部のフェイルオーバー(ヘルスベースのサーキットブレーカー、劣化モード、耐久性のあるキュー)を組み合わせれば、大半のリスクをカバーできる。2番目のプロバイダーは実際のコストと一貫性の複雑さを増やす。厳格な信頼性/鮮度SLAがそれを正当化する場合にのみマルチプロバイダーを追加し、その場合はプロバイダーをインターフェースの背後に抽象化して、設定変更で済むようにする。
1つの市場の失敗がパイプライン全体をダウンさせないようにするにはどうすればよいですか? 各市場をそれ自体の失敗ドメインとして扱う。ジオごとに独立したワーカープール、キュー、同時実行予算、サーキットブレーカーを用意し、それらを結合するグローバルなレートリミッターや共有予算を持たない。そうすれば、ある国でのブロックの波はその国を劣化させフェイルオーバーさせるが、他は正常に動作し続ける。
新しいデータを収集できない場合、何が起きるべきですか? 静かに失敗するのではなく、意図的に劣化させる。stale(古い)とマークした最後に確認された正常な値を提供する、優先度の高いターゲットに負荷を落とす、あるいは鮮度のウィンドウを広げる。データセットごとに「劣化」が何を意味するかを事前に決めておき、それを観測可能な状態にする。古いデータを現在のものとして渡してはならない。
自分のフェイルオーバーが実際に機能することをどうやって知るのですか? テストすることだ。ステージング環境でプロバイダー、ターゲット、インフラの障害を注入し、ブレーカーがトリップし、フォールバックが起動し、他のドメインが動作を続け、作業が失われないことを確認する。実際のインシデント中にしか実行されたことのないフェイルオーバーのパスは、壊れていると仮定すべきだ。
結論
リトライとフェイルオーバーは異なる問題を解決するものであり、これらを混同することが、堅牢に見えるパイプラインが実際のインシデント中に崩れ落ちる原因だ。リトライは個々のリクエストを復旧させる。フェイルオーバーは、ターゲット、市場、プロバイダー、あるいは自分自身のインフラといった失敗ドメイン全体がダウンしたときのためのアーキテクチャだ。これらのドメインに名前をつけ、各市場を分離して失敗を閉じ込め、ヘルスベースのサーキットブレーカーの背後に依存関係をゲートし、静かに失敗するのではなく意図的に劣化させ、処理中の作業を耐久性があり冪等な状態に保ち、そして何よりも、インシデントがあなたの失敗パスをテストする前に、自分でテストすることによって、これを構築する。
プロバイダーの問題については、本当のSLAがそれを要求するまで2番目のプロバイダーを追加することに抵抗し、薄いプロバイダーインターフェースに沿って構築することで、その選択肢を安価なままにしておく。ジオとセッションの動作が一貫した品質の高いresidential proxy networkこそが、ブロックの波やジオの劣化といった一般的な失敗モードをそもそも復旧可能にするものであり、プールの品質(IP reputation)が、そもそもこれらのパスをどれだけ頻繁に実行することになるかを決定する。pricing pageには、これを自分たちのマルチリージョンのワークロードに対して構築しテストするためのGBごとのプランが掲載されている。