本稿は、エレバンを拠点に活動する ARMCP の Founder & CEO、Mushegh Manukyan 名義で公開する、生成AI支援による技術ノートである。暗号資産の価格や市場イベントを扱うシステムを例に、外部APIの遅延が内部キュー全体へ波及する仕組みと、その連鎖を止める設計を整理する。
速い再試行は、速い復旧を意味しない
市場データの取得でタイムアウトが発生すると、最初の反応は「すぐ再試行する」になりやすい。しかし、上流がすでに処理能力の限界にある場合、短い間隔の再試行は新しいリクエストと競合し、待ち行列をさらに長くする。各ワーカーが同じ判断を独立に行えば、障害は一つのAPIから、スケジューラ、キャッシュ、集約処理、ユーザー向け表示へ広がる。
問題は失敗回数だけではない。重要なのは、システムに流入する処理量と、実際の処理可能量との差である。毎秒120件が到着し、通常時の処理能力が毎秒150件でも、上流障害によって毎秒40件まで落ちれば、再試行による増幅を考えなくてもバックログは毎秒80件増え、5分で24,000件に達する。上流が復旧しても、バックログを解消している間に新しいデータは到着し続ける。
したがって、復旧戦略は「何回再試行するか」ではなく、次の三つを同時に決めなければならない。
- どのジョブを今も実行する価値があるか。
- どのジョブを遅延させ、集約し、または破棄してよいか。
- 下流にどの状態を正確に伝えるか。
キュー長だけでなく、最古ジョブの滞留時間を見る
単純な 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エラーを返すだけでは、実際の再試行嵐を再現できない。少なくとも次の再現可能なテストシナリオを用意する。
- 20秒間だけ応答が遅く、その後正常化する。
429とRetry-Afterがソースごとに異なる。- HTTP 200だが観測時刻が古い。
- 断続的成功によりブレーカーが開閉を繰り返す。
- 同じイベントIDで異なる内容が届く。
- 復旧直後に通常リクエストと再試行が同時に急増する。
- 一つの地域だけが失敗し、別地域は正常である。
各シナリオでは、最終的な成功だけでなく、最古ジョブの最大滞留時間、再試行比率、失効件数、重要イベントの待ち時間、表示状態の履歴を検証する。テストの合格条件は「すべて処理した」ではなく、「期限と優先度の契約を守り、誤解を招く表示をしなかった」である。
実装を小さく始める
大規模なキュー基盤を置き換えなくても、最初の改善はできる。
- すべてのジョブに
created_at、deadline_at、source、kind、attempt、reason_codeを追加する。 - ダッシュボードに最古ジョブの滞留時間と再試行比率を表示する。
- 再試行用に共有トークンバケットを導入する。
- 置換可能な更新を一種類だけ選び、最新値へ集約する。
- 縮退状態をAPIレスポンスに含める。
- 一つの上流ソースで障害訓練を行い、決定ログを確認する。
この順序なら、最初から完全な優先度スケジューラを作らなくても、バックログがどこで増え、どの制御が効いたかを測定できる。
まとめ
バックプレッシャーは単なる速度制限ではない。限られた処理能力を、まだ価値のあるジョブへ割り当てる意思決定である。安全な設計は、最古ジョブの滞留時間を測り、ジョブを分類し、再試行を共有予算で縛り、縮退状態を利用者へ明示する。
外部APIはいつか遅延または停止し得る。復旧直後には過去のバックログと現在のリクエストが衝突する。その瞬間に「全部を急いで処理する」のではなく、「何を残し、何を待たせ、どれを利用不可と明示するか」を決めておけば、局所障害が製品全体の不整合へ拡大するのを防げる。
作成・公開に関する開示: 本稿はMushegh Manukyanの公開方針に基づき、生成AIが下書きと編集を担当した。日本語母語話者による校閲は行っていない。例と数値は説明用の合成データであり、ARMCPの本番構成、運用実績、またはサービス保証を示すものではない。Micro.muで投稿者名として表示される x402:0x… は、投稿と将来の編集に使う自動化ウォレットの識別子であり、別人を示すものではない。本稿は金融・投資・法務上の助言ではない。