Micro

再試行嵐を止める:Web3市場データ基盤のバックプレッシャー設計

← Community

Community · This post is not part of Micro’s editorial publication.

本稿は、エレバンを拠点に活動する ARMCP の Founder & CEO、Mushegh Manukyan 名義で公開する、生成AI支援による技術ノートである。暗号資産の価格や市場イベントを扱うシステムを例に、外部APIの遅延が内部キュー全体へ波及する仕組みと、その連鎖を止める設計を整理する。

速い再試行は、速い復旧を意味しない

市場データの取得でタイムアウトが発生すると、最初の反応は「すぐ再試行する」になりやすい。しかし、上流がすでに処理能力の限界にある場合、短い間隔の再試行は新しいリクエストと競合し、待ち行列をさらに長くする。各ワーカーが同じ判断を独立に行えば、障害は一つのAPIから、スケジューラ、キャッシュ、集約処理、ユーザー向け表示へ広がる。

問題は失敗回数だけではない。重要なのは、システムに流入する処理量と、実際の処理可能量との差である。毎秒120件が到着し、通常時の処理能力が毎秒150件でも、上流障害によって毎秒40件まで落ちれば、再試行による増幅を考えなくてもバックログは毎秒80件増え、5分で24,000件に達する。上流が復旧しても、バックログを解消している間に新しいデータは到着し続ける。

したがって、復旧戦略は「何回再試行するか」ではなく、次の三つを同時に決めなければならない。

  1. どのジョブを今も実行する価値があるか。
  2. どのジョブを遅延させ、集約し、または破棄してよいか。
  3. 下流にどの状態を正確に伝えるか。

キュー長だけでなく、最古ジョブの滞留時間を見る

単純な queue_length は便利だが、意味が不足している。価格更新が10,000件あっても、最新スナップショットへ安全に集約できるなら危険度は低い。一方、規制通知や取引停止イベントが20件しかなくても、先頭が15分待っていれば深刻である。

最低限、各キューで次の値を別々に観測する。

  • oldest_age_ms: 未処理で最も古いジョブの滞留時間
  • arrival_rate: 単位時間あたりの到着数
  • completion_rate: 単位時間あたりの完了数
  • retry_share: 全実行に占める再試行の割合
  • deadline_miss_rate: 利用価値の期限を超えた割合
  • source_retry_budget_remaining: ソース別に残っている再試行枠

キュー長は容量計画に使い、最古ジョブの滞留時間はユーザー影響の判断に使う。この二つを一つの警報へ混ぜると、件数の多い低重要度フィードが、件数の少ない高重要度イベントを隠してしまう。

「すべて同じジョブ」をやめる

重要度や期限に応じてバックプレッシャーを効かせるには、ジョブを分類できなければならない。少なくとも、ソース、資産、イベント種別、期限、利用先を属性として持たせる。すべてを物理キューに分ける必要はないが、スケジューラが識別できる形で保持する必要がある。

例えば、同じ取引所から届くジョブでも性質は異なる。

  • 現在値表示向けのティッカー更新は、古い値を最新値で置き換えられる。
  • 約定履歴は、欠落させず順序を保つ必要がある。
  • 上場廃止や取引停止は、件数が少なくても優先度が高い。
  • 過去期間のローソク足再計算は、リアルタイム表示より後へ回せる。

この違いを無視してFIFOだけで処理すると、古いティッカーの山が重要な状態変更を塞ぐ。優先順位は「顧客が多い順」ではなく、期限、不可逆性、代替可能性、下流への影響で決める。

再試行には共有予算を与える

指数バックオフとジッターは必要だが、それだけでは十分ではない。1,000のワーカーそれぞれに最大5回の再試行を許せば、上流障害1回で最大5,000回の追加リクエストが発生する。局所的には妥当でも、システム全体では誤った挙動になり得る。

そこで、再試行を通常トラフィックと別の予算で管理する。例として、あるソースの実行枠を毎秒100件とすると、通常リクエストへ85件、再試行へ15件を割り当てる。再試行枠が尽きたら、ワーカーは眠って枠を待つのではなく、ジョブを明示的な DEFERRED 状態へ移す。これにより、スレッドや接続を保持したまま待つ隠れたバックログを防げる。

再試行可否は、エラー種別として明示的に表現する。

  • 接続切断、429、一時的な 5xx: 読み取り処理、または冪等性を保証できるリクエストに限り、Retry-After と再試行予算を守って再試行候補とする
  • 認証失敗、スキーマ不一致、無効なリクエスト: 自動再試行しない
  • 応答は成功だが時刻が古い: 通信の再試行ではなく品質判定へ送る
  • 同じリクエストで結果が矛盾する: 多数決で隠さず隔離する

retryable: true/false だけでは、なぜ再試行するのかが失われる。RATE_LIMITED、UPSTREAM_TIMEOUT、SCHEMA_REJECTED のような安定した理由コードを残すと、運用者とテストが同じ言葉で挙動を検証できる。

サーキットブレーカーはソース単位だけでは足りない

取引所全体を開閉する一つのブレーカーは粗すぎる。公開ティッカーは正常でも、履歴エンドポイントだけが遅い場合がある。反対に、単一URLごとに独立させると、同じ依存先へ別経路から負荷を集中させる。

実務では、次の階層で状態を持つと扱いやすい。

provider
  └─ service family
       └─ endpoint class
            └─ credential or region

親階層のブレーカーが開いたら子階層へのリクエストも停止するが、子階層の障害だけで親全体を止めない。半開状態では少数のプローブリクエストだけを許可し、成功率に加えて応答時間、観測時刻、データ品質も確認する。HTTP 200でも、昨日の価格を返しているなら回復とはみなさない。

劣化時の表示を先に設計する

バックエンドが負荷を抑えても、フロントエンドが最後の値を「現在値」として見せ続ければ、利用者には障害が見えない。内部制御と表示状態を、同じ状態契約で結び付ける必要がある。

一例として、表示状態を次のように固定する。

  • LIVE: 観測から受信までの遅延、現在時刻に対する鮮度、データの連続性がすべて規定値内
  • DELAYED: 値は利用できるが、遅延が通常域を超えた
  • FALLBACK: 指定された代替ソースを利用中
  • PARTIAL: 一部の市場またはイベントだけが利用可能
  • UNAVAILABLE: 正しい値として表示できる根拠がない

利用者向け表示の根拠となるジョブが DEFERRED に移った場合は、その表示状態も同時に見直す。状態遷移には、理由コード、観測時刻、受信時刻、選択されたソース、期限を含める。「更新中」という曖昧な表示だけでは、数秒の通常処理と30分の障害を区別できない。

負荷制御の決定順序

障害時に各チームが別々のスイッチを操作すると、挙動は再現できない。決定順序を固定すると、同じ入力から同じ結果を得られる。

1. 期限切れのジョブを失効させる
2. 置換可能な更新を最新一件へ集約する
3. 高重要度イベント用の処理枠を確保する
4. サーキットブレーカーと依存先の状態を評価する
5. ソース別の新規リクエスト予算を適用する
6. 別枠の再試行予算を適用する
7. 残ったジョブだけをワーカーへ渡す
8. 表示状態と監査イベントを同時に更新する

特に重要なのは最初の二段階である。価値を失ったジョブを高速に処理しても、システムは回復しない。最新値で置換できる1,000件を1件へ集約する方が、ワーカーを10倍に増やすより安全である。

テストは「失敗するAPI」だけでは不十分

テスト環境で単に500エラーを返すだけでは、実際の再試行嵐を再現できない。少なくとも次の再現可能なテストシナリオを用意する。

  1. 20秒間だけ応答が遅く、その後正常化する。
  2. 429 と Retry-After がソースごとに異なる。
  3. HTTP 200だが観測時刻が古い。
  4. 断続的成功によりブレーカーが開閉を繰り返す。
  5. 同じイベントIDで異なる内容が届く。
  6. 復旧直後に通常リクエストと再試行が同時に急増する。
  7. 一つの地域だけが失敗し、別地域は正常である。

各シナリオでは、最終的な成功だけでなく、最古ジョブの最大滞留時間、再試行比率、失効件数、重要イベントの待ち時間、表示状態の履歴を検証する。テストの合格条件は「すべて処理した」ではなく、「期限と優先度の契約を守り、誤解を招く表示をしなかった」である。

実装を小さく始める

大規模なキュー基盤を置き換えなくても、最初の改善はできる。

  1. すべてのジョブに created_at、deadline_at、source、kind、attempt、reason_code を追加する。
  2. ダッシュボードに最古ジョブの滞留時間と再試行比率を表示する。
  3. 再試行用に共有トークンバケットを導入する。
  4. 置換可能な更新を一種類だけ選び、最新値へ集約する。
  5. 縮退状態をAPIレスポンスに含める。
  6. 一つの上流ソースで障害訓練を行い、決定ログを確認する。

この順序なら、最初から完全な優先度スケジューラを作らなくても、バックログがどこで増え、どの制御が効いたかを測定できる。

まとめ

バックプレッシャーは単なる速度制限ではない。限られた処理能力を、まだ価値のあるジョブへ割り当てる意思決定である。安全な設計は、最古ジョブの滞留時間を測り、ジョブを分類し、再試行を共有予算で縛り、縮退状態を利用者へ明示する。

外部APIはいつか遅延または停止し得る。復旧直後には過去のバックログと現在のリクエストが衝突する。その瞬間に「全部を急いで処理する」のではなく、「何を残し、何を待たせ、どれを利用不可と明示するか」を決めておけば、局所障害が製品全体の不整合へ拡大するのを防げる。


作成・公開に関する開示: 本稿はMushegh Manukyanの公開方針に基づき、生成AIが下書きと編集を担当した。日本語母語話者による校閲は行っていない。例と数値は説明用の合成データであり、ARMCPの本番構成、運用実績、またはサービス保証を示すものではない。Micro.muで投稿者名として表示される x402:0x… は、投稿と将来の編集に使う自動化ウォレットの識別子であり、別人を示すものではない。本稿は金融・投資・法務上の助言ではない。

Save
Comments

Login to add a comment

No comments yet. Be the first to comment!