サービス間REST呼び出しにタイムアウトとリトライを入れる
目標
不安定なダウンストリームを相手に、タイムアウト、指数バックオフ、ジッター、リトライ禁止の条件、フォールバックを自分で実装して、同期呼び出しの基本を完成させます。
なぜ重要なのか
同期呼び出しでの事故は、たいてい「タイムアウトがなかったから」または「リトライを間違えたから」起きます。タイムアウトがないと、ダウンストリームが遅くなったときにアップストリームのコネクションプールが先に枯渇し、ダウンストリームと無関係なリクエストまで失敗します。逆に、固定の間隔で無制限にリトライすると、ダウンストリームが回復しようとする瞬間ごとに、同じ波が押し寄せます。実際の事例で、注文インスタンス20台が毎秒100件を処理しながらmaxAttempts 5でリトライしたところ、決済サービスが毎秒10,000件を受けました。リトライは負荷を足すのではなく、掛け算します。そのため、このラボでは値を入れる順序を重視します。先にタイムアウト、次にリトライの条件、次にバックオフとジッター、最後にフォールバックです。
ステップ
/opt/app/flaky.pyを127.0.0.1:8110で起動してください。GET /fastが200でなければなりません。/root/rest/call.pyを作成して/fastを呼び出し、レスポンスの本文を/root/rest/fast.outに保存してください。/root/rest/timeout.pyを作成して、/slowを1.0秒のタイムアウトで呼び出してください。/root/rest/timeout.outの1行目にTIMEOUT、2行目にelapsed=<초>を書いてください(プレースホルダーは秒数です)。経過時間は2.0秒未満でなければなりません。/root/rest/retry.pyを作成して、/flaky?key=lab&fail=2を指数バックオフでリトライして、最終的に成功させてください。/root/rest/retry.logに、attempt=<n> wait=<초>の形式で、試行ごとに1行ずつ残して、合計3行にしてください(プレースホルダーは秒数です)。retry.pyの待ち時間の計算を、full jitterに変更してください。試行番号2の待ち時間を20回取り出して、/root/rest/jitter.txtに1行ずつ書いてください。互いに異なる値が15個以上なければなりません。/root/rest/retry400.pyで/badを呼び出してください。400はリトライしないので、/root/rest/retry400.logはちょうど1行でなければなりません。/root/rest/budget.txtに、instances=20、rps=100、max_attempts=5、worst_rps=10000の4行を書いてください。/root/rest/gateway.pyを127.0.0.1:8111で起動してください。GET /orderは、ダウンストリームの/always500を呼び出しますが、タイムアウトとリトライを適用し、最終的に失敗したときは、200と{"degraded":true}を返します。
参考
- タイムアウトの値は、相手のp99から出発します。p99の10倍にすると、ないのと同じです。
- full jitter:
wait = random.uniform(0, min(cap, base * 2 ** attempt)) - よくあるミス1: リトライのログに成功した試行だけを残すことです。失敗した試行も残さなければ、バジェットを計算できません。
- よくあるミス2: タイムアウトを例外としてキャッチせず、スタックトレースだけが残って、経過時間を測れないことです。
不安定なダウンストリームを起動する
/opt/app/flaky.pyを127.0.0.1:8110で起動してください。GET /fastが200でなければなりません。
/opt/app/flaky.pyを実行すると、8110で起動します。/fast、/slow、/flaky、/badの4つのパスの動作を、まず自分の目で確認してください。
正常な呼び出しで基準線を取る
/root/rest/call.pyを作成して/fastを呼び出し、レスポンスの本文を/root/rest/fast.outに保存してください。
httpxやurllibで/fastを呼び出して、本文をファイルに残します。まだタイムアウトは気にしなくて構いません。
遅いレスポンスをタイムアウトで切る
/root/rest/timeout.pyを作成して、/slowを1.0秒のタイムアウトで呼び出してください。/root/rest/timeout.outの1行目にTIMEOUT、2行目にelapsed=<초>を書いてください(プレースホルダーは秒数です)。経過時間は2.0秒未満でなければなりません。
/slowは3秒後に答えます。クライアントのタイムアウトを1秒にすると、例外が発生します。例外をキャッチして、結果と経過時間を一緒に記録してください。
指数バックオフのリトライを実装する
/root/rest/retry.pyを作成して、/flaky?key=lab&fail=2を指数バックオフでリトライして、最終的に成功させてください。/root/rest/retry.logに、attempt=<n> wait=<초>の形式で、試行ごとに1行ずつ残して、合計3行にしてください(プレースホルダーは秒数です)。
間隔を、試行のたびに2倍に増やします。各試行の番号と待ち時間を、ログに1行ずつ残さなければ採点されません。
ジッターでリトライのタイミングを散らす
retry.pyの待ち時間の計算を、full jitterに変更してください。試行番号2の待ち時間を20回取り出して、/root/rest/jitter.txtに1行ずつ書いてください。互いに異なる値が15個以上なければなりません。
full jitterは、0から計算されたバックオフまでの間の乱数だけ待ちます。同じ試行番号でも、値が毎回異なる必要があります。
確定的なエラーはリトライしない
/root/rest/retry400.pyで/badを呼び出してください。400はリトライしないので、/root/rest/retry400.logはちょうど1行でなければなりません。
400は、何回送っても400です。ステータスコードでリトライするかどうかを分ける分岐を入れてください。
リトライバジェットを計算する
/root/rest/budget.txtに、instances=20、rps=100、max_attempts=5、worst_rps=10000の4行を書いてください。
インスタンス数×毎秒のリクエスト数×最大試行回数が、ダウンストリームが受ける最悪の毎秒リクエスト数です。3つの値と結果を、それぞれ書いてください。
フォールバックのあるゲートウェイとして総合する
/root/rest/gateway.pyを127.0.0.1:8111で起動してください。GET /orderは、ダウンストリームの/always500を呼び出しますが、タイムアウトとリトライを適用し、最終的に失敗したときは、200と{"degraded":true}を返します。
ダウンストリームが落ちても、ユーザーには200を返しますが、レスポンスに品質低下の表示を残します。前のステップのタイムアウトとリトライを、併せて使います。