仕組みから学ぶ Web
応用読了目安 15#キュー#非同期処理#At-least-once#アウトボックス#分散システム

非同期処理とキュー — 「受け付けました」のあとで、何が起きているか

メール送信をキューに移したら、同じメールが4通届く人と、1通も届かない人が出た。キューは「必ず1回」を約束しない。重複と消失は、処理を書く側が引き受ける。

この記事の進み方

What — 受け付けと処理を分ける

同期処理では、リクエストを受けた API がその場で仕事を終えてから返します。非同期処理では、仕事をキューに置いて先に返し、別のワーカーが後で処理します。

分ける理由は3つです。

  • 遅い仕事で応答を待たせない。 メール送信、画像の変換、外部 API の呼び出し
  • 急な増加を吸収する。 仕事はキューに溜まり、ワーカーは自分の速さで処理する
  • 失敗を利用者の応答から切り離す。 一時的な失敗なら、ワーカーが後で試し直せる

その代わり、「受け付けました」と返した時点では、仕事はまだ終わっていません。 終わったかどうかを、利用者も API も知らない状態が生まれます。

キューは「少なくとも1回」配る

多くのキューは、ワーカーが仕事を受け取った瞬間に消すのではなく、しばらく他のワーカーから見えなくするだけです。ワーカーが処理を終えて「消して」と伝えたら消えます。伝えないまま時間が過ぎたら、ワーカーが落ちたとみなして、また見えるように戻します

Figureキューの中の仕事
待機キュー

どのワーカーでも受け取れる

見えないキュー

あるワーカーが受け取った。一定時間、他には配らない

削除キュー

ワーカーが処理を終えて、消すよう伝えた

1/4
待機ワーカー A が受け取る見えない仕事は消えない。30秒間、他のワーカーから見えなくなるだけ。この30秒を可視性タイムアウトと呼ぶ。

「見えない」は「処理中」の推測でしかない。キューはワーカーの中で何が起きているかを知らず、時間で判断する。遅いだけのワーカーも、落ちたワーカーと区別できない。

可視性タイムアウト
受け取られた仕事を、他のワーカーから見えなくしておく時間。過ぎても消されなければ、また配られる。
At-least-once
少なくとも1回は配る、という配信の約束。重複はありうるが、消えにくい。
デッドレターキュー
何度配っても処理に成功しない仕事を隔離する、別のキュー。
冪等なワーカー
同じ仕事を2回処理しても、結果が1回と変わらないワーカー。
確認 — ここまで読めたか

可視性タイムアウトが30秒のキューです。ワーカーが仕事を受け取り、処理に40秒かかりました。何が起きますか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜ「必ず1回」にならないのか

「必ず1回だけ処理する」は、キューにとって約束できない性質です。

キューが仕事を配ったあと、ワーカーから返事が来ないとき、キューには2つの可能性を区別する手段がありません。

  • ワーカーは処理を終えたが、消す直前に落ちた
  • ワーカーは処理を始める前に落ちた

前者なら配り直すと重複し、後者なら配り直さないと消えます。どちらかを選ぶしかなく、多くのキューは「消えない」側を選びます。 これが At-least-once です。

消してから処理すると、消える

重複を嫌って、受け取った直後に消してから処理する書き方があります。

受け取る → 消す → メールを送る   ← 送る前に落ちると、仕事ごと消える
受け取る → メールを送る → 消す   ← 消す前に落ちると、もう一度配られる

前者は重複しませんが、落ちた仕事は二度と戻りません。確認メールが届かないことに、誰も気づけません。消すのは、処理が終わったあとです。

確認 — ここまで読めたか

重複を避けるため、ワーカーは仕事を受け取った直後にキューから消し、それからメールを送る作りにしました。どんな失敗が起こりえますか?

まず選ぶ(解答例は a〜d の記号で説明します)

DB とキューに、同時には書けない

冒頭の「メールが1通も届かない」は、キューの内側ではなく、キューに入れる手前で起きています。

注文の保存は DB、仕事の投入はキューです。2つの別のシステムに、1つのトランザクションで書くことはできません。 どちらを先にしても、間で落ちると食い違います。

Compareキューに入れるタイミング

仕事だけがあり、注文が無い

queue.send(job) → BEGIN → INSERT order → COMMIT
間で落ちると
**存在しない注文のメールが送られる**
ロールバックしたとき
**仕事は取り消せない**
ワーカーの対策
注文が見つからなければ捨てる
見つけやすさ
注文が無いので、ワーカーで気づける
発生する場面
注文と仕事の食い違い
重複
必要なもの
  • 在庫不足などで保存が失敗しても、仕事はすでにキューにある
  • ワーカーが「注文が無い」と判断するまでの時間差にも注意
  • コミット前に投入すると、ワーカーのほうが先に読みに来ることもある

DB とキューは別のシステムなので、2つをまとめて確定させることはできない。どちらかを先にすると、間で落ちたときにどちらかが残る。

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

注文確認メールの送信を、どう組み直しますか?

与えられた条件
  • 注文は RDB に保存する。キューは別のサービスを使っている
  • キューは At-least-once で、可視性タイムアウトは30秒
  • メール配信サービスは普段2秒、遅い日は40秒かかる
  • 同じ確認メールが複数届くのも、届かないのも避けたい
  • デプロイは1日に数回。そのたびに API のプロセスが入れ替わる
この軸で考える
  • · 注文の保存と仕事の投入が、間で落ちたときに食い違わないか
  • · 同じ仕事が2回配られたとき、メールが2通になるか
  • · 何度やっても失敗する仕事を、どこで止めるか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「キューに入れたら、確実に1回だけ処理されると思っていた」と言うチームメンバーに、何が違うのかを説明してください

与えられた条件
  • 相手は同期処理の API を多く書いてきた
  • 今回の重複メールの事故の対応に一緒に入っている
  • 次に別の非同期処理を書くときに、自分で気づけるようにしたい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。

この記事を前提にしている記事