Skip to content

カスケード障害のメカニズム

1つの遅延が全体を死滅させるメカニズムと、その対策を詳しく解説します。

1. 外部APIが遅延(通常100ms → 5s)
2. 接続待ちプロセスが解放されずプールが飽和
3. 全エンドポイントが応答不能に陥る
4. 不完全なDBトランザクションを残して全プロセスダウン
→ 1つの遅延が全体を死滅させる
時刻: 2024-01-01 10:00:00
状況: 外部決済APIが遅延
10:00:00.000 - 外部決済APIが通常の100msから5秒に遅延開始
10:00:00.100 - リクエスト1受信(プロセス1を取得、外部API呼び出し開始)
10:00:00.200 - リクエスト2受信(プロセス2を取得、外部API呼び出し開始)
10:00:00.300 - リクエスト3受信(プロセス3を取得、外部API呼び出し開始)
...
10:00:01.000 - プロセスプールが飽和(5/5プロセスが外部API待ち)
10:00:01.100 - リクエスト6受信(プロセスプールが満杯、待機)
10:00:01.200 - リクエスト7受信(プロセスプールが満杯、待機)
...
10:00:05.000 - 外部APIが応答(5秒経過)
10:00:05.100 - プロセス1が解放されるが、すぐに次のリクエストで使用される
10:00:05.200 - プロセス2が解放されるが、すぐに次のリクエストで使用される
10:00:05.300 - 外部APIが再び遅延(負荷が高い)
10:00:10.000 - すべてのプロセスが再び外部API待ちで飽和
10:00:10.100 - 新しいリクエストが処理できず、タイムアウトエラーが発生
10:00:10.200 - エラーログが大量に出力され、ログシステムも負荷が高い
10:00:15.000 - システム全体が応答不能に陥る
// ❌ 問題のあるコード: タイムアウトなし、サーキットブレーカーなし
function chargePayment($orderId, $amount) {
// 問題: タイムアウトが設定されていない
// 問題: サーキットブレーカーがない
$ch = curl_init('https://payment-api.example.com/charge');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'order_id' => $orderId,
'amount' => $amount,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}

なぜ事故るか:

  1. タイムアウトなし: 外部APIが遅延すると、プロセスが長時間占有される
  2. サーキットブレーカーなし: 外部APIが障害状態でも呼び出し続ける
  3. プロセスプールの飽和: すべてのプロセスが外部API待ちで、他のリクエストが処理できない
// ✅ 良い例: タイムアウトを設定
function chargePayment($orderId, $amount) {
$ch = curl_init('https://payment-api.example.com/charge');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'order_id' => $orderId,
'amount' => $amount,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3); // 3秒でタイムアウト
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new PaymentTimeoutException("Payment API timeout: " . $error);
}
return json_decode($response, true);
}

2. サーキットブレーカーの実装

Section titled “2. サーキットブレーカーの実装”
// ✅ 良い例: サーキットブレーカーの実装
class CircuitBreaker {
private $key;
private $failureThreshold;
private $timeout;
private $cache;
public function __construct($key, $options = []) {
$this->key = $key;
$this->failureThreshold = $options['failure_threshold'] ?? 5;
$this->timeout = $options['timeout'] ?? 30;
$this->cache = new Redis();
}
public function call($callback) {
$state = $this->getState();
if ($state === 'OPEN') {
if (time() - $this->getLastFailureTime() > $this->timeout) {
$this->setState('HALF_OPEN');
} else {
throw new CircuitBreakerOpenException('Circuit breaker is OPEN');
}
}
try {
$result = $callback();
$this->onSuccess();
return $result;
} catch (\Exception $e) {
$this->onFailure();
throw $e;
}
}
private function getState() {
return $this->cache->get("circuit:{$this->key}:state") ?: 'CLOSED';
}
private function setState($state) {
$this->cache->set("circuit:{$this->key}:state", $state);
}
private function onSuccess() {
$this->cache->set("circuit:{$this->key}:failures", 0);
$this->setState('CLOSED');
}
private function onFailure() {
$failures = (int)$this->cache->get("circuit:{$this->key}:failures") + 1;
$this->cache->set("circuit:{$this->key}:failures", $failures);
$this->cache->set("circuit:{$this->key}:last_failure", time());
if ($failures >= $this->failureThreshold) {
$this->setState('OPEN');
}
}
private function getLastFailureTime() {
return (int)$this->cache->get("circuit:{$this->key}:last_failure") ?: 0;
}
}
$circuitBreaker = new CircuitBreaker('payment-api');
function chargePayment($orderId, $amount) {
return $circuitBreaker->call(function () use ($orderId, $amount) {
$ch = curl_init('https://payment-api.example.com/charge');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'order_id' => $orderId,
'amount' => $amount,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
});
}
// ✅ 良い例: キューで非同期処理
function createOrder($orderData) {
$pdo = getConnection();
$stmt = $pdo->prepare("INSERT INTO orders (user_id, amount) VALUES (?, ?)");
$stmt->execute([$orderData['user_id'], $orderData['amount']]);
$orderId = $pdo->lastInsertId();
// 非同期処理をキューに投入(プロセスプールを占有しない)
enqueueJob('process_payment', ['order_id' => $orderId, 'amount' => $orderData['amount']]);
return $orderId;
}
// ワーカープロセスでキューを処理
function processPaymentJob($orderId, $amount) {
chargePayment($orderId, $amount);
}

カスケード障害のメカニズムのポイント:

  • 発生フロー: 外部APIの遅延 → プロセスプールの飽和 → 全エンドポイントの応答不能 → システム全体のダウン
  • 対策: タイムアウト設定、サーキットブレーカー、キューによる非同期処理
  • 原則: すべての設計はこのフローを想定して、「時間とリソース」で守る

適切な対策により、1つの遅延が全体を死滅させることを防げます。