非同期処理とキュー — 「受け付けました」のあとで、何が起きているか
メール送信をキューに移したら、同じメールが4通届く人と、1通も届かない人が出た。キューは「必ず1回」を約束しない。重複と消失は、処理を書く側が引き受ける。
この記事の進み方
What — 受け付けと処理を分ける
同期処理では、リクエストを受けた API がその場で仕事を終えてから返します。非同期処理では、仕事をキューに置いて先に返し、別のワーカーが後で処理します。
分ける理由は3つです。
- 遅い仕事で応答を待たせない。 メール送信、画像の変換、外部 API の呼び出し
- 急な増加を吸収する。 仕事はキューに溜まり、ワーカーは自分の速さで処理する
- 失敗を利用者の応答から切り離す。 一時的な失敗なら、ワーカーが後で試し直せる
その代わり、「受け付けました」と返した時点では、仕事はまだ終わっていません。 終わったかどうかを、利用者も API も知らない状態が生まれます。
キューは「少なくとも1回」配る
多くのキューは、ワーカーが仕事を受け取った瞬間に消すのではなく、しばらく他のワーカーから見えなくするだけです。ワーカーが処理を終えて「消して」と伝えたら消えます。伝えないまま時間が過ぎたら、ワーカーが落ちたとみなして、また見えるように戻します。
待機キューどのワーカーでも受け取れる
見えないキューあるワーカーが受け取った。一定時間、他には配らない
削除キューワーカーが処理を終えて、消すよう伝えた
待機—ワーカー A が受け取る→見えない — 仕事は消えない。30秒間、他のワーカーから見えなくなるだけ。この30秒を可視性タイムアウトと呼ぶ。「見えない」は「処理中」の推測でしかない。キューはワーカーの中で何が起きているかを知らず、時間で判断する。遅いだけのワーカーも、落ちたワーカーと区別できない。
- 可視性タイムアウト
- 受け取られた仕事を、他のワーカーから見えなくしておく時間。過ぎても消されなければ、また配られる。
- At-least-once
- 少なくとも1回は配る、という配信の約束。重複はありうるが、消えにくい。
- デッドレターキュー
- 何度配っても処理に成功しない仕事を隔離する、別のキュー。
- 冪等なワーカー
- 同じ仕事を2回処理しても、結果が1回と変わらないワーカー。
可視性タイムアウトが30秒のキューです。ワーカーが仕事を受け取り、処理に40秒かかりました。何が起きますか?
Why — なぜ「必ず1回」にならないのか
「必ず1回だけ処理する」は、キューにとって約束できない性質です。
キューが仕事を配ったあと、ワーカーから返事が来ないとき、キューには2つの可能性を区別する手段がありません。
- ワーカーは処理を終えたが、消す直前に落ちた
- ワーカーは処理を始める前に落ちた
前者なら配り直すと重複し、後者なら配り直さないと消えます。どちらかを選ぶしかなく、多くのキューは「消えない」側を選びます。 これが At-least-once です。
消してから処理すると、消える
重複を嫌って、受け取った直後に消してから処理する書き方があります。
受け取る → 消す → メールを送る ← 送る前に落ちると、仕事ごと消える
受け取る → メールを送る → 消す ← 消す前に落ちると、もう一度配られる
前者は重複しませんが、落ちた仕事は二度と戻りません。確認メールが届かないことに、誰も気づけません。消すのは、処理が終わったあとです。
重複を避けるため、ワーカーは仕事を受け取った直後にキューから消し、それからメールを送る作りにしました。どんな失敗が起こりえますか?
DB とキューに、同時には書けない
冒頭の「メールが1通も届かない」は、キューの内側ではなく、キューに入れる手前で起きています。
注文の保存は DB、仕事の投入はキューです。2つの別のシステムに、1つのトランザクションで書くことはできません。 どちらを先にしても、間で落ちると食い違います。
仕事だけがあり、注文が無い
queue.send(job) → BEGIN → INSERT order → COMMIT- 間で落ちると
- **存在しない注文のメールが送られる**
- ロールバックしたとき
- **仕事は取り消せない**
- ワーカーの対策
- 注文が見つからなければ捨てる
- 見つけやすさ
- 注文が無いので、ワーカーで気づける
- 発生する場面
- —
- 注文と仕事の食い違い
- —
- 重複
- —
- 必要なもの
- —
- –在庫不足などで保存が失敗しても、仕事はすでにキューにある
- –ワーカーが「注文が無い」と判断するまでの時間差にも注意
- –コミット前に投入すると、ワーカーのほうが先に読みに来ることもある
DB とキューは別のシステムなので、2つをまとめて確定させることはできない。どちらかを先にすると、間で落ちたときにどちらかが残る。
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
注文確認メールの送信を、どう組み直しますか?
- 注文は RDB に保存する。キューは別のサービスを使っている
- キューは At-least-once で、可視性タイムアウトは30秒
- メール配信サービスは普段2秒、遅い日は40秒かかる
- 同じ確認メールが複数届くのも、届かないのも避けたい
- デプロイは1日に数回。そのたびに API のプロセスが入れ替わる
- · 注文の保存と仕事の投入が、間で落ちたときに食い違わないか
- · 同じ仕事が2回配られたとき、メールが2通になるか
- · 何度やっても失敗する仕事を、どこで止めるか
「キューに入れたら、確実に1回だけ処理されると思っていた」と言うチームメンバーに、何が違うのかを説明してください
- 相手は同期処理の API を多く書いてきた
- 今回の重複メールの事故の対応に一緒に入っている
- 次に別の非同期処理を書くときに、自分で気づけるようにしたい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。