【実務・中級編】 APIゲートウェイでのリクエストタイムアウトと再試行(Retry)戦略 – Web APIアーキテクチャ・データ連携実践ガイド

APIゲートウェイの「死の淵」を歩く:タイムアウトとリトライ戦略の流儀

インフラエンジニアとして現場に立っていると、必ずと言っていいほど「APIがたまにタイムアウトする」「リクエストが重複してデータが壊れた」という怪奇現象に遭遇します。

特にマイクロサービス化が進んだ現代のアーキテクチャにおいて、APIゲートウェイは「信頼の最後の砦」です。しかし、ここの設定を甘く見ると、バックエンドが遅延した瞬間にゲートウェイ自体がリソース枯渇を起こし、連鎖的にシステム全体が共倒れする「カスケード故障」を招きます。

今日は、RFC 7231やHTTPのセマンティクスを紐解きつつ、現場で泥臭く戦うための「タイムアウトとリトライ」の設計術を伝授しましょう。

—

1. なぜ「タイムアウト設定」は複雑なのか

REST APIにおいて、クライアントとサーバーの間にゲートウェイが介在する場合、タイムアウト設定には大きく分けて2つの視点が必要です。

  • Connect Timeout: TCPハンドシェイクが完了するまでの時間。
  • Read/Write Timeout: 接続後、サーバーがヘッダーを返し、ボディを全て送信し終えるまでの時間。

多くのエンジニアがやりがちなミスは、これらを一律に「5秒」といった固定値にすることです。実際には、サービスごとに「どの程度のレスポンス速度を期待するか」というSLA(Service Level Agreement)に基づき、ゲートウェイのタイムアウト値を設定しなければなりません。

設定の勘所

ゲートウェイの設定(例えばNginxの proxy_read_timeout など)は、「最長の期待レスポンス時間+バッファ」にするのが鉄則です。

# Nginx設定例:バックエンドの遅延に対する防御
location /api/v1/ {
    # TCP接続確立までの許容時間
    proxy_connect_timeout 2s;
    # レスポンスヘッダー受信までの許容時間
    proxy_read_timeout 5s;
    # データを書き込むまでの許容時間
    proxy_send_timeout 5s;
    
    # タイムアウト時に別サーバーへフェイルオーバーさせる(冪等性に注意!)
    proxy_next_upstream error timeout http_500 http_502 http_503;
}

—

2. リトライの地獄と「冪等性(Idempotency)」の絶対法則

さて、タイムアウトしたリクエストをどう再送するか。ここでエンジニアの知識が試されます。最も重要なルールは、「冪等性(Idempotency)が保証されないリクエストは、自動リトライしてはならない」ということです。

HTTPメソッドにおける冪等性の定義(RFC 7231)を再確認しましょう。

  • 冪等である (GET, PUT, DELETE, HEAD, OPTIONS): 何度実行してもサーバー側の状態が変わらないもの。
  • 冪等でない (POST): 実行するたびにリソースが新規作成される可能性があるもの。

POST リクエストを安易にリトライすると、サーバー側では処理が成功しているのに、ゲートウェイ側でタイムアウトして「失敗」と判定された場合、「二重決済」や「二重投稿」が発生します。

実装のヒント:Idempotency-Key

これを解決するのが Idempotency-Key ヘッダーです。クライアント側でユニークなUUIDを生成し、ヘッダーに乗せて送信します。サーバー側ではこのキーをRedis等でチェックし、既知のキーであれば「前回の結果」を返します。

—

3. 指数バックオフ(Exponential Backoff)の適用

APIが過負荷状態のときに、追い打ちをかけるようにリトライを繰り返すのは「DDOS攻撃」と変わりません。ここで導入すべきが「指数バックオフ」と「ジッター(揺らぎ)」です。

リトライ間隔を 2^n * base_delay のように増やし、さらにランダムな数値を加算することで、リクエストのタイミングを分散させます。

Pythonでの実装例

import time
import random

def execute_request_with_retry(api_call, max_retries=3):
    base_delay = 1.0  # 基本待機時間
    
    for attempt in range(max_retries):
        try:
            return api_call()
        except TimeoutError:
            # 2の乗数で待機し、ジッター(0.1秒)を加える
            delay = (base_delay * (2 ** attempt)) + random.uniform(0, 0.1)
            print(f"Retrying in {delay:.2f} seconds...")
            time.sleep(delay)
    
    raise Exception("Max retries exceeded")

—

4. 現場で生き残るための運用Tips

最後にもう一つ。デバッグの際に最も重要なのは、「ゲートウェイが出したタイムアウトなのか、バックエンドが出したタイムアウトなのか」をログで識別することです。

  • ゲートウェイ側のエラー: 504 Gateway Timeout
  • バックエンド側のエラー: 500 Internal Server Error や 502 Bad Gateway

通信フローを確認する際は、必ず X-Request-ID ヘッダーを全システムで付与してください。これがないと、巨大なログの海の中から、特定のタイムアウトしたパケットを追いかけることは不可能です。

まとめ:美しいAPI運用のために

1. タイムアウトはサービスごとに最適化せよ: 全体一律は悪手。
2. 冪等性のないリクエストを自動リトライするな: POST には Idempotency-Key を。
3. 指数バックオフでサーバーを休ませろ: 攻撃者になってはいけない。

ネットワークの深淵は常に「想定外」に満ちています。しかし、こうした理論に基づいた堅牢な設計こそが、障害発生時にエンジニアを「夜中の呼び出し」から解放してくれる唯一の手段なのです。

次に皆さんが設計するAPIエンドポイントが、どんな状況下でも健やかに振る舞えることを祈っています。それでは、良いエンジニアリングを!

コメント

タイトルとURLをコピーしました