APIタイムアウトの「魔物」を飼いならせ ― 現場で語られることのない、接続と遮断の最適解
ネットワークエンジニアとして数多の障害現場を渡り歩いてきた私だが、APIのタイムアウト設定ほど「なんとなく設定されている」場所はない。開発者が「とりあえず長めに設定しておこう」と書き込んだ 60s という数字が、のちに大規模なシステム障害の引き金になる瞬間を、私は何度も目撃してきた。
今日は、Web APIにおけるタイムアウト設計の深淵に触れ、クライアントとサーバーが握手から切断に至るまで、どのような「駆け引き」を行うべきかについて解説しよう。
1. なぜ「タイムアウト」は設計の鬼門なのか
タイムアウトの設計が難しい理由はただ一つ。「正解がない」からだ。ネットワークの遅延(レイテンシ)は変動するし、バックエンドの処理負荷も一定ではない。
APIにおけるタイムアウトは、大きく分けて以下の3つのフェーズで捉える必要がある。
1. Connect Timeout: TCPの3ウェイ・ハンドシェイクが完了するまでの時間。
2. Read Timeout: リクエストを送信し、サーバーから最初のレスポンスデータが届くまでの時間。
3. Write Timeout: クライアントがデータを送信し終えるまでの時間(アップロード系で重要)。
これらが不適切だと、コネクションプールが枯渇したり、サーバー側で「ゾンビ」化した処理が延々とリソースを食いつぶすことになる。
2. タイムアウト設計の黄金律:クライアントとサーバーの序列
多くのエンジニアが犯すミスは、サーバー側のタイムアウト値をクライアントよりも短く設定してしまうことだ。
基本方針はこうだ。「サーバー側のタイムアウト > クライアント側のタイムアウト」。
これには明確な理由がある。クライアントが先にタイムアウトして接続を放棄しても、サーバー側で処理が継続していれば、サーバーは「結果」を生成し続ける。この「幽霊のようなリクエスト」が、インフラのリソースを食い尽くす。
実践的な設定例:Python (requests) と Nginx
クライアントサイド(Python requests)とサーバーサイド(nginx)の設定を見てみよう。
クライアント側(Python: requests)
import requests
# 接続に3秒、データ受信(読み取り)に10秒の制限をかける
timeout_config = (3.0, 10.0)
try:
response = requests.get(
"https://api.example.com/data",
timeout=timeout_config
)
response.raise_for_status()
except requests.exceptions.Timeout:
# タイムアウト発生時のハンドリング。ここでリトライロジックを検討する
print("接続または読み取りがタイムアウトしました")
サーバー側(Nginx)
# クライアントとの通信設定
keepalive_timeout 65; # 持続接続の維持時間
client_body_timeout 12s; # リクエストボディの読み取り制限
client_header_timeout 12s; # リクエストヘッダーの読み取り制限
send_timeout 10s; # クライアントへデータを送信する際のタイムアウト
3. リクエストの不整合を回避する「冪等性」と「リトライ」
タイムアウトが発生したとき、最も恐ろしいのは「リクエストはサーバーに届いて処理されたのか、それとも届く前に切れたのか」という不確実性だ。
ここで絶対に守るべき原則が冪等性(Idempotency)だ。
GET,PUT,DELETEは本質的に冪等であるべきだ。何度投げても結果が変わらないなら、タイムアウト後にリトライしても怖くない。- 問題は
POSTだ。タイムアウトした際に自動リトライを行うと、同じ注文が2回飛ぶような悲劇が起きる。
これを回避するために、APIには必ず Idempotency-Key ヘッダーを導入してほしい。
# クライアントからのリクエスト例
POST /api/v1/orders HTTP/1.1
Host: api.example.com
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
サーバー側でこのキーをRedis等に保存し、同じキーで2回目のリクエストが来た場合は、処理を実行せずに前回の結果(または「処理中」というステータス)を返す。これが、大規模システムにおける「安全なリトライ」の作法だ。
4. 現場のシニアが教える「デバッグの極意」
もし、あなたがAPIのタイムアウトと格闘しているなら、まずは curl を使って、通信のどこで詰まっているのかを可視化することをお勧めする。
# 接続時間や読み取り時間を詳細に表示する
curl -w "DNS: %{time_namelookup}s\nConnect: %{time_connect}s\nStartTransfer: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
-o /dev/null -s https://api.example.com
time_namelookup: 名前解決に時間がかかっていないか?time_connect: ネットワーク経路上のファイアウォールで遮断されていないか?time_starttransfer: サーバーのアプリケーション処理(DBクエリなど)が遅延していないか?
これらを見るだけで、問題が「ネットワーク層」にあるのか「アプリケーション層」にあるのかが一撃で判別できる。
最後に:完璧な設定など存在しない
技術者として一つだけ忠告がある。どんなに優れたタイムアウト設定も、ネットワークの不確定性を完全には消し去れない。
だからこそ、タイムアウトを「障害」として隠蔽するのではなく、「システムの一部」として設計に組み込むことが重要だ。タイムアウトした際に何を返し、クライアントにどう再試行させるか。その「ハンドリングの美しさ」こそが、一流のエンジニアとそうでないエンジニアを分かつ境界線となる。
さあ、あなたのシステムのタイムアウト値を見直す準備はできたかな?まずは、本番環境のログから、504 Gateway Timeout がどれだけ頻発しているかを確認するところから始めてみてほしい。
コメント