決めていないと、何が起きるか
APIを呼ぶコードを書く時、「いつまで待つか」「失敗したら、何回やり直すか」を、決めていないことがあります。すると、次のような問題が起きます。
| 決めていないと | 起きること |
|---|---|
| タイムアウトを、設定しない | 相手が応答しない時に、いつまでも待ち続け、自分側のシステムが固まる |
| 失敗したら、すぐ無限に再試行 | 相手が不調の時に、さらに負荷をかけ、アクセス制限を受ける |
| 再試行で、二重処理 | 決済や登録が、二重に実行される |
これらは、運用で障害が起きた時に、初めて表面化しやすい問題です。
タイムアウトの種類
「タイムアウト」には、いくつか種類があります。
| 種類 | 意味 |
|---|---|
| 接続のタイムアウト | 相手に、接続できるまでに待つ時間の上限 |
| 読み取り(応答)のタイムアウト | 接続した後、応答が返ってくるまでに待つ時間の上限 |
| 全体のタイムアウト | リクエストの開始から終わりまでの、合計の時間の上限 |
使うライブラリによって、設定できる項目は異なります。必ず、何らかのタイムアウトを設定してください。初期値が「無制限」になっているライブラリも、あります。
値の決め方
| 観点 | 考え方 |
|---|---|
| 相手の通常の応答時間 | 通常、どれくらいで返ってくるかを、観察する |
| 自分の許容できる待ち時間 | 利用者を待たせてよい上限から、逆算する |
| 公式の推奨 | 相手のAPIの説明に、推奨値があれば、参考にする |
| 処理の重さ | 重い処理(大量のデータの取得など)は、長めにする |
短すぎると、正常な処理が、失敗扱いになります。長すぎると、障害の時に、自分側が固まります。
再試行してよい失敗・いけない失敗
すべての失敗を、再試行すればよいわけではありません。
| 失敗の種類 | 再試行 | 理由 |
|---|---|---|
| ネットワークの一時的なエラー、タイムアウト | してよい | 一時的な問題の可能性がある |
| 429(リクエストが多すぎる) | 待ってから、してよい | 制限が解除されるのを待つ(レートリミットとは) |
| 500番台(相手側の問題)、503 | 間隔をあけて、してよい | 相手の復旧を待つ |
| 400(リクエストが不正) | しない | 内容が同じなら、何度やっても失敗する |
| 401・403(認証・権限) | しない | 認証情報や権限を、直す必要がある |
| 404(見つからない) | しない | 宛先やIDが、誤っている可能性が高い |
サービスによって、ステータスの使い方に違いがあるため、公式の説明も確認してください(API連携が「両方正常なのに繋がらない」時の確認5項目)。
間隔の決め方:指数バックオフとジッター
再試行の間隔は、失敗するたびに、延ばしていくのが基本です。これを指数バックオフと言います。たとえば、1秒、2秒、4秒、8秒、のように、倍々に延ばします。
さらに、**ジッター(ばらつき)**を加えます。多くのクライアントが、同じタイミングで再試行すると、相手に、再び負荷が集中するためです。間隔に、ランダムな幅を持たせて、再試行のタイミングを、ずらします。
import random
import time
def wait_time(attempt, base=1.0, cap=30.0):
# 指数バックオフ:1, 2, 4, 8... 秒(上限 cap)
delay = min(cap, base * (2 ** attempt))
# ジッター:0〜delay の間で、ばらつかせる
return random.uniform(0, delay)
for attempt in range(4):
# ... リクエストを送り、再試行すべき失敗なら:
time.sleep(wait_time(attempt))
回数と、全体の時間に、上限を設ける
再試行には、必ず上限を設けます。
- 回数の上限:たとえば、3〜5回まで(目安)
- 全体の時間の上限:再試行を含めて、合計で何秒まで待つか(利用者を、待たせすぎないため)
- 上限に達したら、失敗として扱い、エラーを返すか、後で改めて実行する
相手が長時間不調の場合に、延々と再試行を続けるのは、避けてください。相手の復旧を、かえって妨げることがあります。