TT Lab
はじめる
学ぶ 学習パス コース

キューと非同期API

λ、μ、そしてキューの深さ

TT Labで続きを見る

一言でいうと

安定条件はλ < μの1つだけです。流入率が処理率を超えた瞬間、キューの深さは線形ではなく爆発的に増えます。

なぜ必要なのか

キューの深さのグラフを見ると、奇妙な点があります。負荷がゆっくり上がっているのに、キューの深さはしばらく0付近に張り付いたまま、ある地点を越えると、急に垂直に跳ね上がります。これはバグではなく、待ち行列の性質です。

流入率λが処理率μより小さければ、キューはたいてい空です。両者が近づくほど、瞬間的な変動を吸収するために平均の深さが大きくなり、λがμを超えると、キューは際限なく伸びます。利用率ρ = λ/μが0.7のときと0.95のときの待ち時間の差は、2倍ではなく、はるかに大きいです。

そのため、ワーカーの容量を流入のちょうど100%に合わせてはいけません。70–80%の利用率を目標にして、余裕を残すのが実務の慣行です。

どう動くのか

キューの深さを遅延に翻訳する道具が、リトルの法則です。L = λ x W。システム内の平均の項目数は、流入率かける平均の滞留時間です。逆にするとW = L / λです。キューの深さが3,000で、処理率が毎秒50件なら、いま入ってきたメッセージは60秒後に処理されます。この計算ができると、「キューが少し溜まっているな」が「いま受け付けているユーザーは、1分待つ」に変わります。

バックプレッシャーは、この状況でシステムが自分自身を守る方法です。3つの層があります。

1つ目、キューの上限。キューに最大の長さを置き、超過したらプロデューサーに429を返します。無限に受け取っておいてあとでできなくなるより、いまは受けられないと言うほうが誠実です。

2つ目、ロードシェディング(load shedding)。優先度の低いリクエストから捨てます。すべてのリクエストを抱え込んで遅く処理し、全員で一緒に死ぬより、一部を素早く拒否して残りを生かすほうが、全体の可用性に有利です。

3つ目、コンシューマーの並行性の調整。μを上げます。ただし、際限なく上げることはできません。ワーカーを増やすと、その後ろのDBや外部APIが新しいボトルネックになります。ボトルネックを移しただけなのか、なくしたのかを、毎回確認する必要があります。

現場での姿

429を返すときにRetry-Afterヘッダーも一緒に付けるとよいです。察しのよいクライアントは、このシグナルを尊重して、不要な早期リトライを控えます。これを付けないと、クライアントは自分なりのバックオフでリトライし、それが互いに重なります。

そして、キューの深さのアラートは、絶対値よりも傾向で設定するほうがよいです。深さ1,000が正常なシステムもあれば、10が異常なシステムもあります。「5分間増え続けている」のほうが、はるかに信頼できるシグナルです。

バックプレッシャーをどこにかけるのか

キューが伸び始めたとき、選択肢は3つしかなく、何も選ばなければ4つ目が 起きます。メモリを使い切って死にます。

方法 何を諦めるか 向いている場所
プロデューサーを止める(blocking) 応答時間 内部パイプライン、バッチ
新しいリクエストを拒否する(429) 一部のリクエスト 公開API
古いものから捨てる 古いデータ メトリクス・ログ・リアルタイムの相場

3つ目が、意外によく正解になります。毎秒更新される相場を、5分後に処理することには、 何の価値もありません。古いものに価値がないデータなら、捨てることが正解です。 逆に、決済イベントは絶対に捨ててはいけないので、1行目か2行目を使います。

有界キューが基本

無制限のキューは、問題を遅らせるだけで、なくしません。上限を置けば、問題がキューではなく プロデューサーの側で表に出ます。 そのほうがはるかに早く見え、対応できます。

# ❌ 무제한 — 메모리가 다 찰 때까지 아무 신호가 없다
q = asyncio.Queue()

# ✅ 상한을 두면 put 이 기다리고, 그 대기가 곧 신호다
q = asyncio.Queue(maxsize=1000)
await asyncio.wait_for(q.put(item), timeout=0.5)   # 못 넣으면 거절한다

上限値は消化時間で決めます。処理率が毎秒100件で、最大10秒まで 遅延を許容するなら、1,000が上限です。こう決めると、キューの深さがそのまま遅延バジェットに なります。

並行数の制限のほうが正確なとき

キューの深さよりも、進行中の作業数を制限するほうがよい場合が多くあります。特に、 下流が脆弱なときにそうです。

sem = asyncio.Semaphore(20)      # 하류에 동시에 20개까지만

async def handle(item):
    async with sem:
        await downstream.call(item)

キューをどれだけ大きくしても、下流が同時20個しか耐えられないなら、それ以上を送ることは、 下流を崩す行為です。キューはバッファーで、セマフォは防護壁です。2つは 一緒に使います。

何をダッシュボードに載せるのか

큐 깊이                       ← 지금 얼마나 밀렸나
소진 시간 = 깊이 ÷ 처리율      ← 이것이 경보 기준
생산율 λ 와 소비율 μ           ← 둘의 차이가 추세를 만든다
거절·폐기 건수                ← 역압이 실제로 작동했는가
가장 오래된 항목의 나이        ← 지연의 최대치

最後の行が特に有用です。深さが同じでも、古いものが押されたままになっていないか (先入れ先出しが崩れていないか)を教えてくれます。

次のラボですること

負荷ジェネレーターでキューを埋めながら、深さを時系列で記録し、リトルの法則で待ち時間を推定し、キューの上限と429を付け、ワーカーの並行数を上げて処理率の改善を測ったあと、安定条件をドキュメントにまとめます。