目次を表示する

共通基盤の設計軸 2026 ─ 抽象・責務・非機能要件を設計する 15 章

障害伝播の抑制 ─ Bulkhead / Circuit Breaker / Backpressure

障害伝播の抑制 ─ Bulkhead / Circuit Breaker / Backpressure

共通基盤は 多くの利用者を抱える。1 利用者が変な挙動をしたら、それが他利用者に伝染しない仕組みが必要。1 つの依存先が壊れたら、それが自分のサービスを倒さない仕組みが必要。

この章では Resilience pattern 5 つを整理する。Netflix が Hystrix で広めた領域。

障害伝播の構造

graph TB
  subgraph "Naive な実装"
    U[Users] --> P1[Platform]
    P1 --> D1[Dependency<br/>これが死ぬと]
    D1 -. 全 user に伝播 .-> U
  end

  subgraph "Resilient な実装"
    U2[Users] --> P2[Platform<br/>+ Resilience patterns]
    P2 -.制御.- D2[Dependency<br/>これが死んでも]
    D2 -.遮断.-> P2
    P2 -. 縮退で対応 .-> U2
  end

  style D1 fill:#ffe1e1
  style D2 fill:#fff4e1
  style P2 fill:#e1ffe1

パターン 1:Timeout

最も基本で、最も忘れられがち

❌ Timeout なし

// 依存先が応答しない
const result = await dependentService.call();
// → 永遠に待つ、threads が枯渇、自分も死ぬ

依存先のフリーズが自分のフリーズに伝染する

✅ Timeout 必須

const result = await Promise.race([
  dependentService.call(),
  timeout(100),  // 100ms で諦める
]);

全ての外部呼び出しに timeout を付ける」が鉄則。HTTP / DB / Redis / Queue 全部。

Timeout の階層

Client timeout     : 5000ms  (利用者から)
  Service timeout  : 4500ms  (自分のサービス内)
    DB timeout     : 1000ms  (DB 呼び出し)
    Cache timeout  : 50ms    (Redis)
    External API timeout : 2000ms

外側 > 内側 という階層関係を保つ。内側が外側より長いと意味がない。

パターン 2:Retry

一時的失敗(ネットワーク揺らぎ、瞬間的過負荷)には retry が効く。

Naive な retry の罠

// ❌ 即座に retry
for (let i = 0; i < 3; i++) {
  try { return await call(); }
  catch (e) { /* retry */ }
}

問題:依存先が過負荷のとき、retry が殺到して状況悪化(retry storm)。

✅ Exponential Backoff + Jitter

// 1st: 100ms 後
// 2nd: 200ms + ランダム
// 3rd: 400ms + ランダム
async function withRetry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try { return await fn(); }
    catch (e) {
      if (i === attempts - 1) throw e;
      const delay = Math.pow(2, i) * 100 + Math.random() * 100;
      await sleep(delay);
    }
  }
}

Jitter(ランダム化)が重要。全 client が同じタイミングで retry すると、依存先がまた倒れる。

Idempotency が前提

retry は idempotent な操作にのみ有効。第 5 章の Idempotency-Key を組み合わせて、副作用ある操作も safely retry できるようにする。

パターン 3:Circuit Breaker

依存先が継続的に壊れている時、呼び出し自体を諦める

stateDiagram-v2
  [*] --> Closed
  Closed --> Open: 失敗が閾値超
  Open --> HalfOpen: 一定時間経過
  HalfOpen --> Closed: 成功
  HalfOpen --> Open: 失敗

3 状態:

状態挙動
Closed通常通り呼び出し
Open呼び出さずに即座に失敗を返す
Half-Open試験的に少数の呼び出しを許可

実装イメージ

class CircuitBreaker {
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  private failureCount = 0;
  private openedAt: number | null = null;

  async call(fn) {
    if (this.state === 'open') {
      if (Date.now() - this.openedAt > 30000) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit Open');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (e) {
      this.onFailure();
      throw e;
    }
  }

  private onFailure() {
    this.failureCount++;
    if (this.failureCount > 10) {
      this.state = 'open';
      this.openedAt = Date.now();
    }
  }

  private onSuccess() {
    this.failureCount = 0;
    this.state = 'closed';
  }
}

Netflix Hystrix → Resilience4J

Netflix Hystrix は 2018 年に maintenance mode に入り、後継として Resilience4J が広まった。

Hystrix の特徴:

  • thread-based isolation
  • complex configuration
  • Java のみ、JVM 重い

Resilience4J の特徴:

  • semaphore-based + thread-pool 選択可
  • lightweight, functional API
  • Modern Java、Kotlin 対応

各言語に 同等のライブラリ がある(go-resilience4j, polly for .NET, など)。

パターン 4:Bulkhead

リソースを区画化して、1 区画の沈没が船全体を沈めないようにする。船舶の Bulkhead(隔壁)の比喩。

graph TB
  subgraph "❌ 全部共通"
    P1[Pool: 100 threads]
    P1 --> A[Tenant A 70 req]
    P1 --> B[Tenant B 30 req]
    Note1[A が 100 全部使って B が止まる]
  end

  subgraph "✅ Bulkhead"
    Pa[Pool A: 50 threads]
    Pb[Pool B: 50 threads]
    Pa --> A2[Tenant A]
    Pb --> B2[Tenant B]
    Note2[A が暴走しても<br/>B には影響しない]
  end

  style Note1 fill:#ffe1e1
  style Note2 fill:#e1ffe1

適用例

リソース区画化単位
Connection pooltenant ごと、依存先ごと
Thread poolpriority ごと、依存先ごと
Memorycontainer / VM ごと
Disk I/Otenant ごと(cgroups)

Trade-off

全部共通: 利用率高 / 障害伝播あり
Bulkhead: 利用率下がる / 障害が局所化

→ Bulkhead は "保険" としてのコスト

利用率と隔離はトレードオフ。重要な依存先には Bulkhead、軽い依存には共通プール

パターン 5:Backpressure

第 7 章で扱った 早期に 503 で守る 設計。Bulkhead と Backpressure はしばしば組み合わせる:

// Bulkhead + Backpressure
class TenantBulkhead {
  private semaphore: Map<string, Semaphore> = new Map();

  async execute(tenantId: string, fn) {
    const sem = this.getOrCreate(tenantId);

    // 待たずに即座に判定
    if (!sem.tryAcquire()) {
      throw new ServiceUnavailableError('Tenant rate limit');
    }

    try { return await fn(); }
    finally { sem.release(); }
  }
}

待つのではなく、即座に拒否することで queue が無限に伸びるのを防ぐ。

5 パターンの組み合わせ

graph LR
  R[Request] --> T[Timeout]
  T --> CB[Circuit Breaker]
  CB --> BH[Bulkhead]
  BH --> BP[Backpressure]
  BP --> Retry[Retry]
  Retry --> D[Dependency]

  Note1[全部組み合わせて使う]

  style D fill:#fff4e1

順序の意味:

  1. Timeout: 待ち時間の上限
  2. Circuit Breaker: 壊れた依存先を遮断
  3. Bulkhead: tenant / 依存先ごとに分離
  4. Backpressure: 過負荷時に早期拒否
  5. Retry: 一時的失敗のリカバリ

Graceful Degradation(縮退運転)

依存先が完全に壊れても、一部機能を犠牲にして サービスを続ける。

例:認証基盤の縮退

async function validateToken(token) {
  try {
    return await tokenService.validate(token);  // 通常
  } catch (e) {
    // 通常 path 失敗 → 縮退
    if (cb.isOpen()) {
      // 古い cache でも通す
      const cached = await cache.get(`token:${token}`);
      if (cached && cached.expires > Date.now() - 3600000) {
        return { ...cached, degraded: true };  // 1h 前まで OK とする
      }
    }
    throw e;
  }
}

完全に止まるのではなく、少し甘く判定する ことで利用者の体験を守る。

Trade-off

縮退運転には判断が要る

  • セキュリティ的に許容できるか
  • どこまで甘くするか
  • 利用者に通知すべきか

すべての機能で縮退できるわけではない。Critical path(決済等) は縮退禁止。

レジリエンスのチェックリスト

graph TB
  Q1[全外部呼び出しに Timeout] --> Q2[Retry に Backoff + Jitter]
  Q2 --> Q3[Idempotency 確認]
  Q3 --> Q4[Circuit Breaker 配置]
  Q4 --> Q5[Bulkhead で分離]
  Q5 --> Q6[Backpressure で守る]
  Q6 --> Q7[Graceful Degradation]
  Q7 --> OK[完成]

  style OK fill:#e1ffe1

この章の要点

  • Timeout は鉄則、全外部呼び出しに付与、階層関係を保つ
  • Retry は exponential backoff + jitter、idempotent 前提
  • Circuit Breaker(Closed / Open / Half-Open)で壊れた依存を遮断
  • Hystrix(2018 maintenance mode)→ Resilience4J が後継
  • Bulkhead で tenant / 依存ごとにリソース区画化、利用率は下がるが隔離効果
  • Backpressure で過負荷時に早期 503
  • 5 パターンを組み合わせて使う
  • Graceful Degradation で完全停止を避ける、ただし critical path は禁止

次章への問いかけ

単一の障害は局所化できた。だが 特定の tenant が他を倒す 形の障害は、もう一段の対策が要る。

次章で マルチテナンシー ── Hot tenant、Quota、Bulkhead の組織化。