バックプレッシャー - 過負荷制御とフロー制御のパターン

中級 | 10分 で読める | 2026.04.24

公式ドキュメント

バックプレッシャー(Backpressure)は、受信側(consumer)が処理できる量に合わせて、送信側(producer)へ速度を落とす必要を伝える仕組みです。

速いproducerと遅いconsumerの間に無制限キューを置くと、遅延とメモリ使用量が増え続けます。 待つ、上限を設ける、古いデータを捨てる、要求を拒否する、という選択を要件に合わせて行います。

過負荷が連鎖する流れ

producerが毎秒100件を送り、consumerが毎秒60件しか処理できない場合、差分の40件が毎秒キューへ残ります。短いburstなら吸収できますが、継続すればキュー待ち時間とメモリが増えます。

producer 100件/秒 -> queue +40件/秒 -> consumer 60件/秒

キューの上限がなければ、いずれメモリ不足、タイムアウト、再試行の増加が起きます。再試行が新しい負荷を作り、依存サービスまで遅くなることもあります。

速いproducerと遅いconsumerの間に上限付きbufferを置き、consumerの処理能力をfeedbackしてpause、drop、rejectなどを要件で選ぶ図

基本戦略

戦略向く状況失うもの・注意点
producerを待たせる一件も失えず、送信側が待てる待ち時間と上流の占有が増える
上限付きキュー短いburstを吸収したい上限到達時の動作が別に必要
batch化まとめて処理すると効率が上がる一件目の待ち時間が増える
古い/新しい項目をdrop最新値だけ重要な監視データなど欠落を許せる契約が必要
rejectするこれ以上受けると全体が壊れる429/503と再試行方針が必要

すべてを保存する要件なら、dropではなく永続キュー、送信側の停止、処理能力の増強を組み合わせます。メモリ上のキューを大きくするだけでは耐久性は得られません。

Node.js Streamsの最小例

Node.jsのWritable.write()は、内部バッファがしきい値へ達するとfalseを返します。その後も書き続けることはできますが、メモリへ蓄積し続けるため、drainを待ちます。

import { once } from "node:events";

async function writeAll(writable, chunks) {
  for await (const chunk of chunks) {
    if (!writable.write(chunk)) {
      await once(writable, "drain");
    }
  }

  writable.end();
  await once(writable, "finish");
}

ファイルや変換処理をつなぐ場合は、可能ならstream.pipeline()を使うと、バックプレッシャーとエラー伝播をまとめて扱えます。highWaterMarkは「最大メモリ量」そのものではなく、読み書きを止める判断に使うしきい値です。

Reactive Streamsの要求量

Reactive Streamsでは、subscriberがrequest(n)で受け取れる個数を示し、publisherは要求された数を超えて送らない契約を中心にしています。ライブラリを使う時は、演算子がbuffer、drop、最新値のみ保持のどれを行うか確認します。

バックプレッシャー対応をうたうAPIでも、途中で通常の配列へ全件展開したり、無制限キューへ渡したりすれば制御は切れます。producerから最終consumerまで、境界ごとの上限を追います。

HTTP APIでの過負荷制御

同期HTTPでは、下流が飽和した時に要求を無制限に保持せず、早めに429 Too Many Requestsまたは503 Service Unavailableを返す設計があります。Retry-Afterを付けられる場合でも、全クライアントが同時に再試行しないよう指数バックオフとjitterを使います。

if queue.isFull():
  return 503 with Retry-After

queue.enqueue(job)
return 202 with operationId

202 Acceptedを返すなら、処理の永続性、状態確認、失敗通知、重複要求の扱いをAPI契約へ含めます。受付後にメモリから消える構成では「受理済み」を保証できません。

ジョブキューでの設計

非同期ジョブでは、producerが受付API、consumerがworkerになります。キューに空きがあるだけで受け付け続けると、待ち時間がサービスの期限を超える場合があります。件数上限に加えて「今受けた仕事が期限内に終わるか」を判断します。

優先度がある場合、低優先度ジョブがキューを埋めて重要ジョブを妨げないよう、キューや同時実行数を分けます。失敗ジョブは無限に先頭へ戻さず、再試行回数、backoff、dead-letter queue、手動再処理を設計します。

consumer数を増やす時も、DB接続数、外部APIのquota、ロック競合を上限として扱います。workerだけ増やして下流を飽和させると、全体の処理率がかえって下がることがあります。

WebSocketや画面更新

リアルタイム画面でproducerのイベントをすべて描画すると、ブラウザの描画が追いつかない場合があります。株価や監視値のように最新状態が重要なら、中間値を間引き、一定間隔で最新値だけ描画できます。

一方、チャットや監査イベントのように欠落できないデータはdropせず、永続化した履歴と再開位置を用意します。同じ「ストリーム」でも、イベントの意味によって戦略が変わります。

キャンセルとエラーを上流へ返す

consumerが処理を中止したのにproducerが作業を続ければ、不要なCPU、I/O、メモリを使います。接続切断、タイムアウト、処理失敗をキャンセル信号として上流へ伝えます。

Node.jsでは複数のstreamを手作業でpipe()するより、Promise版のpipeline()を使うと、完了とエラーを一つの処理として待てます。

import { pipeline } from "node:stream/promises";
import { createReadStream, createWriteStream } from "node:fs";
import { createGzip } from "node:zlib";

await pipeline(
  createReadStream("input.log"),
  createGzip(),
  createWriteStream("input.log.gz"),
);

途中のtransformが全データを配列へためる実装なら、streamを使っていても上限は失われます。各段が一件ずつ、または決めたbatch単位で処理することを確認します。

順序、公平性、重複

並行consumerを増やすと、完了順が入力順と変わることがあります。順序が必要なら、partition keyごとに直列化する、sequence番号で並べ直す、並行数を制限する、といった設計が必要です。

一つの大きなジョブがキューを占有すると、小さなジョブが待ち続けるhead-of-line blockingも起きます。サイズ別キューや公平なschedulerを検討します。

タイムアウト後に再試行されると、最初の処理も完了して重複する可能性があります。consumerはidempotency keyを使い、同じジョブを二度適用しても副作用が重複しないようにします。

似た仕組みとの違い

  • Rate limiting:利用者や期間ごとの到着量を制限する。consumerの現在容量を直接返すとは限りません。
  • Load shedding:過負荷時に一部の仕事を捨て、重要な処理を守ります。
  • Circuit breaker:失敗中の依存先への呼び出しを一時的に止め、連鎖障害を防ぎます。
  • Autoscaling:処理能力を増やしますが、起動までの遅れがあるため、上限付きキューの代わりにはなりません。

これらは競合する仕組みではなく、入口の制限、内部のフロー制御、依存先の保護として組み合わせます。

上限を決める

キュー件数だけでなく、キュー内の総バイト数と待ち時間を見ます。画像一件と小さなイベント一件ではメモリ量が違うからです。

確認する指標は次です。

  • queue depthと最古メッセージの待ち時間
  • producerの入力率とconsumerの処理率
  • メモリ、CPU、外部I/O
  • drop/reject件数
  • 処理時間のpercentileとタイムアウト
  • 再試行数とdead-letter queue

上限到達後の動作をテストしないと、普段の負荷では見えないデータ欠落や停止が本番で起きます。短いburst、継続的な過負荷、consumer停止、復旧後の滞留解消を別々に試します。

上限から逆算する

たとえば処理期限が30秒、consumerが安定して毎秒50件を処理できるなら、単純計算で1500件を超える待ち行列は期限を守れません。実際には処理時間のばらつきと失敗分を見込み、さらに小さい上限にします。

この計算は固定の性能値ではありません。負荷試験で処理率とpercentileを測り、デプロイや依存先の変化に合わせて更新します。

よくある誤解

  • キューを大きくすれば解決する:故障までの時間を延ばすだけで、継続的な速度差は残ります。
  • dropは必ず悪い:最新値だけ必要なデータでは、古い値を捨てる方が正しい場合があります。
  • 再試行すれば安全:同時再試行は負荷を増やすため、上限、backoff、jitterが必要です。
  • consumerを増やせばよい:DBや外部APIが新しいボトルネックになる場合があります。

まとめ

バックプレッシャーは、consumerの処理能力をproducer側の動作へ反映する仕組みです。上限付きキューを基準に、待機、batch、drop、rejectをデータの重要度に応じて選びます。キュー件数だけでなく待ち時間とバイト数を観測し、過負荷と復旧の両方を試します。

参考リソース

関連記事

← 一覧に戻る
PR
PR
PR
PR