バルクヘッドパターン入門 - 障害の隔離によるシステム耐障害性

2026.04.10

公式ドキュメント

この記事の要点

バルクヘッドパターンはリソースを隔離して障害の波及を防ぐ設計パターン
カスケード障害の防止が最大の目的で、サーキットブレーカーとの併用が推奨
• セマフォ/スレッドプール/接続プール/セル分離など強度に応じた実装方式を選択する

バルクヘッドパターン(Bulkhead Pattern)は、船舶の船体が隔壁(bulkhead)で仕切られ、1区画が浸水しても船全体が沈没しないよう設計されていることに由来するソフトウェア設計パターンです。アプリケーションのリソースを隔離することで、1つの障害がシステム全体に波及することを防ぎます。本記事では、バルクヘッドパターンの原則から実装、ベストプラクティスまでを体系的に解説します。

概要

バルクヘッドパターンとは

バルクヘッドパターンは、障害分離(Fault Isolation)を実現するアーキテクチャパターンです。リソース(スレッド、コネクション、メモリなど)を論理的/物理的に分割し、ある区画で問題が発生しても他の区画に影響を与えないようにします。

flowchart TB
    subgraph NoBulkhead["バルクヘッドなし"]
        Client1["クライアント"]
        SharedPool["共有スレッドプール<br/>(100 threads)"]
        SvcA1["Service A<br/>(遅い)"]
        SvcB1["Service B"]
        SvcC1["Service C"]

        Client1 --> SharedPool
        SharedPool --> SvcA1
        SharedPool --> SvcB1
        SharedPool --> SvcC1

        SvcA1 -. 全スレッド占有 .-> SharedPool
    end

    subgraph WithBulkhead["バルクヘッドあり"]
        Client2["クライアント"]
        PoolA["Pool A<br/>(30 threads)"]
        PoolB["Pool B<br/>(40 threads)"]
        PoolC["Pool C<br/>(30 threads)"]
        SvcA2["Service A"]
        SvcB2["Service B"]
        SvcC2["Service C"]

        Client2 --> PoolA --> SvcA2
        Client2 --> PoolB --> SvcB2
        Client2 --> PoolC --> SvcC2
    end

    style NoBulkhead fill:#fee
    style WithBulkhead fill:#efe

なぜ必要か

分散システムでは「カスケード障害」が大きなリスクです。あるサービスの遅延や障害が、呼び出し元のリソースを枯渇させ、結果としてシステム全体がダウンする現象です。バルクヘッドパターンは、この連鎖的障害を防ぐ最も基本的な手段です。

原則・定義

基本原則

  1. リソース分離(Resource Isolation): コンシューマー別/機能別にリソースを分ける
  2. 失敗の局所化(Fail Locally): 障害を発生箇所に閉じ込める
  3. 独立スケール(Independent Scaling): 区画ごとに容量を調整可能にする
  4. 優雅な劣化(Graceful Degradation): 一部機能停止でもコアは動作する
  5. 明示的な上限(Explicit Limits): 各区画に明確な制限を設ける

バルクヘッドの種類

種類説明適用レイヤ
スレッドプール分離呼び出し先ごとにスレッドを分離アプリケーション
セマフォ分離同時実行数をセマフォで制限アプリケーション
プロセス分離別プロセス/コンテナに分離インフラ
ノード分離物理的に別ノードへ配置インフラ
セルベース分離顧客/テナント単位のフルスタック分離アーキテクチャ

構成要素

全体像

flowchart LR
    Client["Client Request"]
    Router["Request Router"]

    subgraph Bulkhead1["Bulkhead: Critical"]
        Q1["Queue (limit: 50)"]
        T1["Threads (20)"]
        DB1["DB Pool (10)"]
    end

    subgraph Bulkhead2["Bulkhead: Standard"]
        Q2["Queue (limit: 100)"]
        T2["Threads (30)"]
        DB2["DB Pool (15)"]
    end

    subgraph Bulkhead3["Bulkhead: Batch"]
        Q3["Queue (limit: 200)"]
        T3["Threads (10)"]
        DB3["DB Pool (5)"]
    end

    Client --> Router
    Router --> Bulkhead1
    Router --> Bulkhead2
    Router --> Bulkhead3

構成要素の詳細

  • Request Classifier: リクエストを優先度/種類で分類する
  • Queue: 区画ごとのキュー。オーバーフロー時は拒否
  • Worker Pool: 区画専用のワーカースレッド/ファイバー
  • Resource Pool: DB接続、HTTPクライアントなど区画専用リソース
  • Health Monitor: 区画ごとの健全性を監視

実装例

1. セマフォベースの実装(TypeScript)

class Semaphore {
  private permits: number;
  private queue: Array<() => void> = [];

  constructor(permits: number) {
    this.permits = permits;
  }

  async acquire(): Promise<void> {
    if (this.permits > 0) {
      this.permits--;
      return;
    }
    return new Promise((resolve) => this.queue.push(resolve));
  }

  release(): void {
    this.permits++;
    const next = this.queue.shift();
    if (next) {
      this.permits--;
      next();
    }
  }
}

class BulkheadExecutor {
  constructor(
    private readonly name: string,
    private readonly semaphore: Semaphore,
    private readonly maxQueueSize: number,
  ) {}

  private pending = 0;

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.pending >= this.maxQueueSize) {
      throw new BulkheadFullError(
        `Bulkhead [${this.name}] is full: ${this.pending}/${this.maxQueueSize}`,
      );
    }

    this.pending++;
    try {
      await this.semaphore.acquire();
      try {
        return await fn();
      } finally {
        this.semaphore.release();
      }
    } finally {
      this.pending--;
    }
  }
}

class BulkheadFullError extends Error {}

// 使用例
const criticalBulkhead = new BulkheadExecutor("payment", new Semaphore(20), 50);
const standardBulkhead = new BulkheadExecutor("catalog", new Semaphore(50), 200);

async function processPayment(orderId: string) {
  return criticalBulkhead.execute(() => paymentGateway.charge(orderId));
}

async function fetchCatalog() {
  return standardBulkhead.execute(() => catalogService.list());
}

2. スレッドプール分離(Node.js Worker Threads)

import { Worker } from "node:worker_threads";

class WorkerPool {
  private readonly workers: Worker[] = [];
  private readonly queue: Array<{
    task: unknown;
    resolve: (v: unknown) => void;
    reject: (e: Error) => void;
  }> = [];
  private readonly idleWorkers: Worker[] = [];

  constructor(
    private readonly scriptPath: string,
    private readonly size: number,
  ) {
    for (let i = 0; i < size; i++) {
      const w = new Worker(scriptPath);
      this.workers.push(w);
      this.idleWorkers.push(w);
    }
  }

  async run<T>(task: unknown): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      this.queue.push({ task, resolve: resolve as (v: unknown) => void, reject });
      this.dispatch();
    });
  }

  private dispatch() {
    while (this.queue.length && this.idleWorkers.length) {
      const worker = this.idleWorkers.pop()!;
      const job = this.queue.shift()!;

      const onMessage = (msg: unknown) => {
        cleanup();
        this.idleWorkers.push(worker);
        job.resolve(msg);
        this.dispatch();
      };
      const onError = (err: Error) => {
        cleanup();
        this.idleWorkers.push(worker);
        job.reject(err);
        this.dispatch();
      };
      const cleanup = () => {
        worker.off("message", onMessage);
        worker.off("error", onError);
      };

      worker.on("message", onMessage);
      worker.on("error", onError);
      worker.postMessage(job.task);
    }
  }
}

// 区画ごとにプールを分離
const imagePool = new WorkerPool("./image-worker.js", 4);
const pdfPool = new WorkerPool("./pdf-worker.js", 2);

3. HTTP クライアントの接続プール分離

import { Agent } from "node:https";

// サービスごとに Agent を分ける = 接続プールが独立
const userServiceAgent = new Agent({
  maxSockets: 20,
  keepAlive: true,
  timeout: 5000,
});

const paymentServiceAgent = new Agent({
  maxSockets: 10, // 決済は厳しく制限
  keepAlive: true,
  timeout: 10000,
});

async function callUserService(path: string) {
  const res = await fetch(`https://user-service${path}`, {
    // @ts-expect-error Node-specific option
    agent: userServiceAgent,
  });
  return res.json();
}

async function callPaymentService(path: string, body: unknown) {
  const res = await fetch(`https://payment-service${path}`, {
    method: "POST",
    // @ts-expect-error Node-specific option
    agent: paymentServiceAgent,
    body: JSON.stringify(body),
  });
  return res.json();
}

4. Kubernetes でのバルクヘッド(ResourceQuota + 名前空間)

apiVersion: v1
kind: Namespace
metadata:
  name: critical-services
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: critical-quota
  namespace: critical-services
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "50"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: critical-limits
  namespace: critical-services
spec:
  limits:
    - default:
        cpu: 500m
        memory: 512Mi
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      type: Container

5. セルベースアーキテクチャ(AWS風)

flowchart TB
    DNS["Route 53 / Shuffle Sharding"]
    DNS --> Cell1
    DNS --> Cell2
    DNS --> Cell3

    subgraph Cell1["Cell 1 (Tenants A, B, C)"]
        LB1["ALB"]
        App1["App Tier"]
        DB1["RDS"]
        LB1 --> App1 --> DB1
    end
    subgraph Cell2["Cell 2 (Tenants D, E, F)"]
        LB2["ALB"]
        App2["App Tier"]
        DB2["RDS"]
        LB2 --> App2 --> DB2
    end
    subgraph Cell3["Cell 3 (Tenants G, H, I)"]
        LB3["ALB"]
        App3["App Tier"]
        DB3["RDS"]
        LB3 --> App3 --> DB3
    end

各セルは完全に独立したフルスタックで、1セルの障害が他セルに波及しません。シャッフルシャーディング(Shuffle Sharding)により、特定テナントの問題が特定セルに固定されることを防ぎます。

メリット・デメリット

メリット

  1. カスケード障害の防止: 1サービス障害が全体停止を引き起こさない
  2. 可観測性の向上: 区画ごとのメトリクスで問題を特定しやすい
  3. 優先度制御: 重要処理に専用リソースを確保できる
  4. 独立スケーリング: 区画単位でキャパシティを調整できる
  5. セキュリティ境界: テナント間のリソース干渉を防ぐ

デメリット

  1. リソース使用率の低下: 各プールに余剰を持たせるため全体効率が下がる
  2. 複雑性の増加: 設定・チューニングすべきパラメータが増える
  3. 容量計画の難易度: 区画ごとに適切なサイズ見積もりが必要
  4. デッドロックの懸念: 区画間をまたぐ処理設計に注意が必要
  5. 過剰設計のリスク: 単純なシステムには不要

ユースケース

適した場面

  • 決済処理: 他機能の障害に影響されず必ず実行させたい
  • マルチテナントSaaS: テナント間の「ノイジーネイバー」防止
  • BFF(Backend for Frontend): 下流サービスごとの分離
  • バッチとオンラインの共存: バッチがオンラインAPIを圧迫しない
  • 外部API統合: サードパーティAPIの遅延から自サービスを保護

適さない場面

  • リクエスト数が非常に少ない小規模システム
  • 同期処理が完全に1経路しか存在しないアプリ
  • リソース制約が非常に厳しい組込み環境

注意: プールサイズの設定は勘ではなくリトルの法則(同時実行数 = スループット x レイテンシ)に基づいて計算し、負荷試験で検証してください。

落とし穴

1. プールサイズの誤設定

プールが小さすぎると健全なリクエストまで拒否され、大きすぎるとバルクヘッドの意味がなくなります。リトルの法則(Little’s Law)で 同時実行数 = スループット × レイテンシ を計算し、観測に基づいて調整しましょう。

2. キュー無限化

キューを無制限にすると、遅延が積み重なり結局OOMになります。必ず有限のキュー上限を設定してください。

3. 区画をまたぐトランザクション

1リクエストの中で複数の区画にまたがる処理は、デッドロックや相互枯渇の原因です。区画境界は処理フローの自然な切れ目に合わせましょう。

4. モニタリング不足

区画ごとの「拒否率」「キュー深さ」「平均待ち時間」を観測しないと、過剰/過少を判断できません。Prometheus等で区画別メトリクスを公開します。

5. シャッフルシャーディングなしのセル分割

単純な区画分割だけでは、1テナントの障害がそのテナントの属するセル内の全顧客に波及します。シャッフルシャーディングで分散させることで「影響範囲」をさらに狭められます。

比較表

他の耐障害性パターンとの比較

パターン目的仕組みバルクヘッドとの関係
バルクヘッド障害の隔離リソース分離本記事のテーマ
サーキットブレーカー障害の拡散防止呼び出し遮断併用推奨
リトライ一時障害の回復再試行バルクヘッド内で実施
タイムアウト待機時間の制限時間上限前提条件
レートリミット過負荷防止入口制限上位層で連携
フォールバック代替応答縮退動作バルクヘッド枯渇時に発動

分離方式の比較

方式強度コスト実装難度
セマフォ
スレッドプール
プロセス分離中高中高
コンテナ分離
セル分離最高最高

実践メモ: バルクヘッドは単独ではなく、サーキットブレーカー・タイムアウト・フォールバックと組み合わせて使うことで最大の効果を発揮します。

ポイント: 完璧な分離より、重要なクリティカルパスを守る実用的な分離を目指しましょう。

ベストプラクティス

  1. サーキットブレーカーと併用: 拒否ではなくフェイルファストへ
  2. メトリクスを常に公開: 区画別の飽和度を Grafana で可視化
  3. 負荷試験で上限決定: 勘ではなく実測値でサイジング
  4. フォールバック応答を用意: 拒否時にも UX を損なわない応答
  5. タイムアウトを必ず設定: 区画外へ出る呼び出しには必須
  6. キューは必ず有限: バックプレッシャーを上流へ伝播させる
  7. セル境界=テナント境界: マルチテナントではセルで隔離
  8. カオステストで検証: 実際に障害注入し、分離が効くか確認

まとめ

バルクヘッドパターンは、分散システムで「一部の障害で全体が倒れない」ことを保証する基本パターンです。

  • 本質: リソースを分離し、障害を局所化する
  • 実装: セマフォ/スレッドプール/接続プール/セル分離
  • 併用: サーキットブレーカー、タイムアウト、リトライ
  • 計測: 区画ごとのメトリクスで継続的に調整
  • 設計: 重要度と処理特性でバルクヘッドを切る

完璧な分離より、重要なクリティカルパスを守る実用的な分離を目指しましょう。

参考リソース

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る