🔋
概念 #データ設計 #バックプレッシャー #サーキットブレーカー #信頼性 #分散システム #DDIA 📚 データ志向アプリケーション設計(DDIA)

バックプレッシャーとサーキットブレーカー

システムが過負荷になったとき何が起きるか。バックプレッシャーによるフロー制御、Circuit BreakerとBulkheadパターンで障害の連鎖を防ぐ設計を理解する

定義

バックプレッシャー(Backpressure):処理できる速度を超えたリクエストが来たとき、上流に「遅くしてほしい」というシグナルを伝える仕組み。バッファが無限ではないため、何もしなければメモリ枯渇やレイテンシ無限増大が起きる。

サーキットブレーカー(Circuit Breaker):依存するサービスが不健全なとき、早期に失敗を返してシステム全体の崩壊を防ぐパターン。電気回路のブレーカーに例えた名前。

バックプレッシャーなしの問題

Producerが1000件/秒でKafkaにメッセージを送る
Consumerが100件/秒しか処理できない

何も制御しない場合:
  1. Kafkaのキューが積み上がる(ラグが増大)
  2. Consumerがどんどん遅れる
  3. DBへのクエリが積み上がる
  4. DBコネクションプールが枯渇
  5. 全リクエストがタイムアウト → カスケード障害

バックプレッシャーの実装

プルベースのバックプレッシャー

ConsumerがProducerから「自分が処理できる分だけ」取得する。

Kafkaの場合:
  ConsumerはPollでメッセージを引き取る(Pushではない)
  max.poll.records で1回に取得する件数を制御
  処理が遅ければPollingが減る → 自然にバックプレッシャーになる

HTTPの場合:
  Streaming APIで1件ずつ受け取る
  → サーバーはクライアントが受け取るまで次を送らない(TCP流量制御)

キューの深さ制限

// 有界キュー(Bounded Queue)でバックプレッシャー
import { Queue } from 'bull';

const queue = new Queue('email', {
  defaultJobOptions: {
    attempts: 3,
    backoff: { type: 'exponential', delay: 1000 },
  },
  settings: {
    maxStalledCount: 1,
  },
});

// キューが満杯ならエラーを返す(積み込みすぎない)
async function enqueueEmail(data: EmailData) {
  const waiting = await queue.getWaitingCount();
  if (waiting > 10000) {
    throw new Error('Queue is full, please try later');
    // → 503 Service Unavailable をクライアントに返す
  }
  await queue.add(data);
}

レート制限(Rate Limiting)

import Bottleneck from 'bottleneck';

// 外部APIへのリクエストを制限
const limiter = new Bottleneck({
  maxConcurrent: 5,     // 同時実行数
  minTime: 100,         // リクエスト間の最小間隔(ms)
  reservoir: 100,       // トークンバケット: 100リクエスト/秒
  reservoirRefreshAmount: 100,
  reservoirRefreshInterval: 1000,
});

const result = await limiter.schedule(() => externalApi.call());

サーキットブレーカーパターン

3つの状態:
  Closed(通常): リクエストを通す。エラー率を監視
  Open(遮断):  エラー率が閾値超え → 即座に失敗を返す(DBにアクセスしない)
  Half-Open(半開): 一定時間後に試験的にリクエストを通す

状態遷移:
  Closed → Open:     エラー率が閾値(例: 50%)を超えた
  Open → Half-Open:  タイムアウト(例: 30秒)後
  Half-Open → Closed: 試験リクエストが成功
  Half-Open → Open:  試験リクエストが失敗
import CircuitBreaker from 'opossum';

const options = {
  timeout: 3000,          // 3秒でタイムアウト
  errorThresholdPercentage: 50,  // エラー率50%でOpen
  resetTimeout: 30000,    // 30秒後にHalf-Openに移行
};

const breaker = new CircuitBreaker(callDatabase, options);

breaker.on('open', () => {
  console.log('Circuit breaker OPEN: DB calls blocked');
  metrics.increment('circuit_breaker.open');
});

breaker.on('halfOpen', () => {
  console.log('Circuit breaker HALF-OPEN: testing...');
});

breaker.on('close', () => {
  console.log('Circuit breaker CLOSED: normal operation');
});

// フォールバックを定義
breaker.fallback(() => getCachedData());  // キャッシュから返す

const result = await breaker.fire(queryId);

Bulkheadパターン

船の隔壁(Bulkhead)から来た名前。一部が浸水しても他の区画が影響を受けない設計。

問題:
  APIサーバーが外部サービスA, B, Cを呼ぶ
  サービスAが遅くなる → スレッドが詰まる
  → サービスB, Cへのリクエストも詰まる(共有スレッドプール)
  → サービス全体が応答不能に

Bulkhead:
  サービスA用スレッドプール: 20スレッド
  サービスB用スレッドプール: 20スレッド
  サービスC用スレッドプール: 10スレッド
  
  Aが詰まっても B, C のプールは影響を受けない
// DBのコネクションプールを用途別に分ける
const oltp_pool = new Pool({ max: 20 }); // トランザクション用
const olap_pool = new Pool({ max: 5  }); // 分析クエリ用(長時間クエリが詰まっても本線に影響しない)
const background_pool = new Pool({ max: 3 }); // バックグラウンドジョブ用

カスケード障害のパターン

依存関係:
  API → 注文サービス → 在庫サービス → DB

在庫DBが遅くなる:
  1. 在庫サービスのスレッドが詰まる(タイムアウト待ち)
  2. 注文サービスが在庫サービスを待つスレッドが詰まる
  3. APIが注文サービスを待つリクエストが詰まる
  4. 全体が応答不能に(カスケード障害)

防止策:
  - 各層でタイムアウトを設定(無限に待たない)
  - サーキットブレーカーで障害を隔離
  - Bulkheadでリソースを分離
  - キャッシュでフォールバック

リトライとジッター

// ❌ 単純なリトライ(サンダーリングハード問題)
async function retry(fn, times) {
  for (let i = 0; i < times; i++) {
    try { return await fn(); }
    catch { await sleep(1000); }  // 全クライアントが同時にリトライ → 同時スパイク
  }
}

// ✅ 指数バックオフ + ジッター(ランダム分散)
async function retryWithJitter(fn, maxAttempts = 3) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === maxAttempts - 1) throw err;
      const base = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      const jitter = Math.random() * base; // ランダムに分散
      await sleep(base + jitter);
    }
  }
}

ストリーム処理でのバックプレッシャー

ストリーム処理でもバックプレッシャーは重要なテーマ。

Project Reactor / RxJS のバックプレッシャー:
  Publisher(データ生成)
    ↓ request(N)
  Subscriber(処理できる量だけリクエスト)

Kafka Streams:
  Consumer Lag = Producerがどれだけ先行しているか
  Lagが増大 → ConsumerのCPU・メモリを増やすか、Producer側を制限する

観測指標

監視すべき指標:
  - キューの深さ(増え続けていないか)
  - Consumer Lag(Kafkaのラグ)
  - Circuit BreakerのOpen状態の回数・時間
  - スレッドプールの利用率
  - タイムアウトの発生率
  - p99レイテンシ(平均より外れ値が重要)

関連概念

出典・参考文献

  • Martin Fowler, “CircuitBreaker” — martinfowler.com/bliki/CircuitBreaker.html
  • Michael Nygard, Release It! (2018) — Stability Patterns
  • Martin Kleppmann, Designing Data-Intensive Applications (2017) Chapter 1, 11
  • Reactive Streams Specification — reactive-streams.org
  1. 1. 🗄️データ志向アプリケーション設計:概要
  2. 2. 🧩データモデルとクエリ言語
  3. 3. 💾ストレージエンジンとインデックス
  4. 4. 🔁レプリケーション
  5. 5. 🍕パーティショニング(シャーディング)
  6. 6. 🔒トランザクションとACID
  7. 7. 分散システムの本質的な問題
  8. 8. 🤝一貫性と分散合意
  9. 9. 📦バッチ処理
  10. 10. 🌊ストリーム処理
  11. 11. 📋エンコーディングとスキーマ進化
  12. 12. 🔗Sagaパターンと分散トランザクション
  13. 13. 🏗️データシステムの統合設計
  14. 14. 📸MVCC(多版型同時実行制御)
  15. 15. 📊列指向ストレージとOLAP設計
  16. 16. 🕰️ベクタークロックと因果順序
  17. 17. 🔀CRDT(競合なし複製データ型)
  18. 18. 🔍クエリオプティマイザーと実行計画
  19. 19. キャッシュ戦略とRedis設計
  20. 20. 🔎全文検索と転置インデックス
  21. 21. 🌐NewSQL(分散ACIDデータベース)
  22. 22. 📝WALと論理レプリケーション
  23. 23. 🔌コネクションプーリング
  24. 24. 🚧ゼロダウンタイムマイグレーション
  25. 25. 🆔分散ID生成
  26. 26. 🔄N+1問題とDataLoaderパターン
  27. 27. 📈タイムシリーズDB
  28. 28. 🛡️Row Level Security(行レベルセキュリティ)
  29. 29. 📤Outboxパターン(トランザクショナルアウトボックス)
  30. 30. 💾DBバックアップとPITR
  31. 31. ⚠️データベース設計アンチパターン
  32. 32. 🕸️グラフDB深掘り
  33. 33. 🔋バックプレッシャーとサーキットブレーカー

出典: Martin Kleppmann, 'Designing Data-Intensive Applications' (2017) Chapter 1, 11