キューは時間を買う道具だ
一言でいうと
キューは処理速度を上げません。リクエストを受け取る速度と処理する速度を切り離して、 瞬間的な急増を時間で吸収します。
なぜ必要なのか
画像アップロードAPIがあるとしましょう。原本を保存し、サムネイルを3つ作り、 メタデータを抽出し、検索インデックスに入れます。すべてを同期で行うと、応答に4秒かかります。 普段は耐えられます。ところがプロモーションの日にアップロードが10倍に増えると、4秒かかるリクエストが 10倍積み上がってワーカースレッドがすべて塞がり、ヘルスチェックまで失敗します。
キューを入れると、APIは原本だけを保存して「受け付けました」と答えます。200msです。残りは ワーカーが自分の速度で処理します。急増が来てもAPIは変わらず200msで答え、キューの深さだけが 増えます。増えた深さは処理の遅延になります。4秒かかっていたものが3分かかることも あります。その代わり、システムは死にません。
この交換がキューのすべてです。遅延を受け入れて、可用性を買います。
安定条件は不等式1つ
キューを使うのに向いた条件は3つです。1つ目、作業がすぐに終わらなくてもよいか。2つ目、リクエストの 速度が処理の速度より瞬間的に速くなりうるか。3つ目、失敗したとき、あとでやり直して もよいか。3つとも「はい」なら、キューが合っています。
逆に、キューを入れてはいけない場合もはっきりしています。ユーザーが結果をすぐ見なければならない参照、 そして平均の流入が平均の処理能力より大きい場合です。後者を誤解している人が 多いです。キューはバーストを吸収しますが、慢性的な過負荷は解決できません。
λ = 초당 유입 건수 μ = 워커 하나의 초당 처리 건수 × 워커 수
λ < μ → 큐 깊이가 0 근처로 돌아온다 (안정)
λ = μ → 깊이가 무작위로 떠돈다 (경계 — 운영하면 안 되는 지점)
λ > μ → 깊이가 선형으로 자란다 (터진다)
数値で見ると明確です。μ = 100/sで、λが普段は60/sなのに、30分間90/sに 上がったとしましょう。この区間で毎秒なくなる余裕は10件です。30分でキューに残るものは なく、うまく吸収されます。ところがλが110/sに上がると、毎秒10件ずつ溜まって30分で18,000件に なります。この状態が終わらなければ、メモリやディスクを食い尽くして壊れます。
λ > μが続くなら、必要なのはキューではなくワーカーの増設か流入制限です。
遅延は利用率に比例しない
ここで、人々がよく見落とすことがあります。待ち時間は利用率(ρ = λ/μ)に比例して 増えるのではなく、非線形に爆発します。 おおまかな感覚は、こうです。
| 利用率 ρ | 待ち時間の倍率(1/(1−ρ)) | 意味 |
|---|---|---|
| 0.5 | 2倍 | 余裕がある |
| 0.8 | 5倍 | そろそろ目立ってくる |
| 0.9 | 10倍 | もう少し上がると急激に悪化する |
| 0.95 | 20倍 | 運用が難しい |
| 0.99 | 100倍 | 事実上の障害 |
そのため、ワーカーの容量を「平均の流入にぴったり合わせて」設定してはいけません。利用率70–80%を目標に 設定し、それを超える分は自動スケーリングや流入制限が受け止めます。
現場での姿
キューを入れると、新しい運用メトリクスが生まれます。キューの深さとコンシューマーラグ(consumer lag) です。この2つをダッシュボードに載せなければ、キューは静かなブラックホールになります。 ユーザーは 「アップロードしたのに出てきません」と報告し、APIのダッシュボードはすべて緑です。問題は APIの後ろに溜まっています。
深さだけを見るのでは不十分です。深さ1,000が深刻かどうかは、処理レートによって 違います。深さ÷処理レート=消化時間も一緒に見ないと判断できません。毎秒100件を 処理するなら10秒分で、毎秒1件なら17分分です。アラートもこの値で設定します。
アラートをもう1つ。深さが0から動かないのも異常のサインです。 コンシューマーがすべて 死んで誰も取り出さなければ、流入だけが溜まるはずなのに、プロデューサーまで死んでいると、深さが0で 平らになります。処理完了件数が0のまま深さが0なら、パイプライン全体が止まっているのです。
そして、キューを入れると、ユーザー体験の設計も一緒に必要になります。「受付済み」の状態をどう 見せるか、どれだけ待てばよいか、失敗したらどう知らせるか。これを決めずにキューだけを 入れると、ユーザーは、ただ消えたリクエストを見ることになります。最低でも3つは決める必要があります。
- ジョブIDをすぐに返す: ユーザーがあとで状態を尋ねられる必要があります。
- 状態を取得する経路を作る:
GET /jobs/{id}で、pending・running・failedを見ます。 - 失敗を知らせる: リトライを使い切ったジョブはデッドレターキューに送り、人が見られるようにします。
キューを選ぶ前に答えるべき4つのこと
道具を選ぶのは最後です。その前に4つの性質を先に決める必要があり、これを 決めずにRedisやKafkaを選ぶと、あとですべて作り直すことになります。
配信保証。 少なくとも1回(at-least-once)が基本です。ちょうど1回は、キューが与えてくれる ものではなく、コンシューマーを冪等にして得るものです。処理したジョブIDを記録して おき、同じものがまた来たら飛ばします。
順序。 グローバルな順序を守るキューは、並列処理ができません。実務では、たいていキー単位の 順序で十分です。同じユーザーのジョブだけが順番どおりであればよく、ほかのユーザーとの順序は 関係ありません。パーティションキーをユーザーIDにすると、これが得られます。
リトライと諦め。 何回リトライし、間隔をどう延ばし、いつ諦めるかを決めます。 指数バックオフにジッターを混ぜないと、失敗したジョブが同じ時刻に集中してリトライします。
デッドレターキュー(DLQ)。 リトライを使い切ったジョブの行き先です。DLQがなければ、そのジョブは 永遠に循環するか、静かに消えます。DLQに溜まった件数には、必ずアラートをかけます。 ここに溜まるのは、人が見なければならないものだからです。
次のクイズで確認すること
このモジュールは概念だけを扱います。次のモジュールから、Redisのリストとストリームでキューを 実際に作り、順序・重複・消失がどこで生じるかを1つずつ再現します。