指数バックオフとDLQを実装する
目標
リトライの4つの要素(指数バックオフ、ジッター、上限、諦めたあとのDLQ)をすべて実装し、DLQに入った情報だけで原因を突き止めて、再処理まで完走します。
なぜ重要なのか
リトライは最も簡単に見えて、最も頻繁に事故を起こすコードです。固定間隔の無限リトライは、回復しようとするサーバーを、周期的にまた倒します。指数バックオフだけを入れると、1万のクライアントが依然として同じ瞬間にリトライして、波を作ります。ジッターまで入れても上限がなければ、10回目の試行で17分待ちます。そして、諦める条件がなければ、間違ったメッセージ1つがキュー全体を永遠に塞ぎます。これをポイズンメッセージと呼びます。DLQは、そのメッセージを隔離して残りを流すための仕組みであり、同時に調査対象の一覧です。そのため、メッセージだけを入れても役に立ちません。原因と試行回数と最初の時刻が一緒にあって初めて、原因を直して再処理できます。
ステップ
/root/qr/worker.pyでq:tasksを消費するワーカーを作ります。/opt/app/taskproc.pyのprocess(msg)は、msg["kind"]がbadなら例外を投げます。- 失敗したメッセージを、試行回数を1増やしてキューに戻します。
/root/qr/requeue.outにattempts=2が見えている必要があります。 /root/qr/schedule.txtに、base 1秒、試行0–5の指数バックオフの値をattempt=<n> wait=<초>の形式で6行書きます(プレースホルダーは秒数です)。1、2、4、8、16、32です。/root/qr/jitter.txtに、試行番号3のフルジッターの待ち時間を20個、1行ずつ書きます。すべて0以上8以下で、互いに異なる値が15個以上である必要があります。- 上限30秒を適用して、
/root/qr/capped.txtに試行0–8の値を書きます。最後の3つの値は、すべて30である必要があります。 - 5回失敗したメッセージは、
q:tasks:dlqに送ります。q:tasksは空で、DLQの長さが1である必要があります。 - DLQメッセージのJSONに、
payload、error、attempts、first_seen_atの4つのキーがすべてあり、attemptsは5です。 /root/qr/replay.pyは、DLQから取り出してattemptsを0に戻し、q:tasksに入れます。実行後、DLQは空で、q:tasksに1件がある必要があります。
参考
- フルジッター:
wait = random.uniform(0, min(cap, base * 2 ** attempt)) - リトライするエラー: タイムアウト、503、429。リトライしないもの: スキーマ違反、存在しない参照。
- よくある間違い1は、DLQのコンシューマーを作って自動で再処理することです。原因がそのままなら、無限ループになります。
- よくある間違い2は、DLQの深さにアラートをかけないことです。DLQは静かなので、数週間後に発見されます。
キューを消費するワーカーを作る
/root/qr/worker.pyでq:tasksを消費するワーカーを作ってください。/opt/app/taskproc.pyのprocess(msg)は、msg["kind"]がbadなら例外を投げます。
メッセージを1つずつ取り出して、処理関数に渡します。処理関数は、わざと失敗できるように作っておいてください。
失敗したメッセージを再キューする
失敗したメッセージを、試行回数を1増やしてキューに戻してください。/root/qr/requeue.outにattempts=2が見えている必要があります。
試行回数をメッセージの中に入れておけば、次の消費のときに続けて数えられます。
指数バックオフのスケジュールを計算する
/root/qr/schedule.txtに、base 1秒、試行0–5の指数バックオフの値をattempt=<n> wait=<초>の形式で6行書いてください(プレースホルダーは秒数です)。1、2、4、8、16、32です。
試行番号を指数にします。計算だけを先にして、ファイルに残して確認してから、コードに入れてください。
フルジッターを適用する
/root/qr/jitter.txtに、試行番号3のフルジッターの待ち時間を20個、1行ずつ書いてください。すべて0以上8以下で、互いに異なる値が15個以上である必要があります。
0から計算された値までの間の乱数です。同じ試行番号でも、値が毎回変わる必要があります。
待ち時間に上限をかける
上限30秒を適用して、/root/qr/capped.txtに試行0–8の値を書いてください。最後の3つの値は、すべて30である必要があります。
指数はすぐ大きくなります。上限がなければ、10回目の試行で17分待つことになります。
5回失敗したらDLQに送る
5回失敗したメッセージは、q:tasks:dlqに送ってください。q:tasksは空で、DLQの長さが1である必要があります。
諦める条件があって初めてリトライです。元のキューからは消え、DLQにだけあるようにします。
DLQのメッセージに調査用の情報を入れる
DLQメッセージのJSONに、payload、error、attempts、first_seen_atの4つのキーがすべてあり、attemptsが5になるようにしてください。
元のメッセージ、失敗の原因、試行回数、最初に受信した時刻の4つがあって初めて、あとで調査ができます。
原因を取り除いてから再処理する
/root/qr/replay.pyは、DLQから取り出してattemptsを0に戻し、q:tasksに入れてください。実行後、DLQは空で、q:tasksに1件がある必要があります。
DLQから取り出して元のキューに戻し、試行回数を初期化します。自動のループではなく、明示的な実行である必要があります。