Skip to content

復旧設計とフォールバック戦略

復旧設計とフォールバック戦略

Section titled “復旧設計とフォールバック戦略”

障害から自動/手動で安全に戻れる設計を詳しく解説します。

外部依存が落ちたらキャッシュ・スタブで縮退運転する。

// ✅ 良い例: キャッシュによるフォールバック
function getProduct($productId) {
$redis = new Redis();
$redis->connect('localhost', 6379);
// 1. キャッシュから取得を試みる
$cached = $redis->get("product:{$productId}");
if ($cached) {
return json_decode($cached, true);
}
// 2. データベースから取得を試みる
try {
$pdo = getConnection();
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$productId]);
$product = $stmt->fetch();
if (!$product) {
throw new ProductNotFoundException($productId);
}
// キャッシュに保存(TTL: 1時間)
$redis->setex("product:{$productId}", 3600, json_encode($product));
return $product;
} catch (\Exception $e) {
// 3. データベースが落ちている場合、外部APIから取得を試みる
try {
$ch = curl_init("https://external-api.example.com/products/{$productId}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$product = json_decode($response, true);
// キャッシュに保存(次回はキャッシュから取得可能)
$redis->setex("product:{$productId}", 3600, json_encode($product));
return $product;
}
} catch (\Exception $apiError) {
// 4. すべて失敗した場合、スタブデータを返す
error_log("All data sources failed, returning stub data: product_id={$productId}, db_error={$e->getMessage()}, api_error={$apiError->getMessage()}");
return createStubProduct($productId);
}
}
}
function createStubProduct($productId) {
// スタブデータ(最低限の情報のみ)
return [
'id' => $productId,
'name' => 'Product information temporarily unavailable',
'price' => 0,
];
}

なぜ重要か:

  • 可用性の向上: 外部依存が落ちても、最低限のサービスを提供可能
  • ユーザー体験: 完全なエラーではなく、スタブデータを返すことでユーザー体験を維持

再起動時に中途状態をリカバリ可能にする(例:処理キューの再読込)。

// ✅ 良い例: 再起動時にOutboxを再処理
function recoverPendingEvents() {
// アプリケーション起動時に、PENDING状態のイベントを再処理
$pdo = getConnection();
$stmt = $pdo->prepare("SELECT * FROM outbox_events WHERE status = 'PENDING'");
$stmt->execute();
$pendingEvents = $stmt->fetchAll();
error_log("Recovering " . count($pendingEvents) . " pending outbox events");
foreach ($pendingEvents as $event) {
// リトライ回数が上限を超えていない場合のみ再処理
if ($event['retry_count'] < 3) {
processOutboxEvent($event);
} else {
error_log("Outbox event exceeded retry limit: event_id={$event['id']}, retry_count={$event['retry_count']}");
// 手動対応が必要な状態としてマーク
$stmt = $pdo->prepare("UPDATE outbox_events SET status = 'MANUAL_REVIEW_REQUIRED' WHERE id = ?");
$stmt->execute([$event['id']]);
}
}
}
// アプリケーション起動時に実行
// index.php
recoverPendingEvents();

なぜ重要か:

  • データの整合性: 再起動時に中途状態のデータをリカバリ可能
  • 自動復旧: 手動介入なしで自動的に復旧可能

Exponential Backoff とJitterでセルフDDoSを防止する。

// ✅ 良い例: Exponential Backoff + Jitter
function retryWithBackoff($callback, $maxRetries = 3) {
$retries = 0;
while ($retries < $maxRetries) {
try {
return $callback();
} catch (\Exception $e) {
$retries++;
if ($retries >= $maxRetries) {
throw $e;
}
// Exponential Backoff + Jitter
$baseDelay = pow(2, $retries) * 1000; // 1s, 2s, 4s
$jitter = rand(0, 1000); // 0-1s
$delay = ($baseDelay + $jitter) / 1000;
usleep($delay * 1000000);
}
}
}
function chargePayment($orderId, $amount) {
return retryWithBackoff(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);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new PaymentException("Payment failed: HTTP {$httpCode}");
}
return json_decode($response, true);
});
}

バックオフの計算:

リトライ1: 1000ms + random(0-1000ms) = 1000-2000ms
リトライ2: 2000ms + random(0-2000ms) = 2000-4000ms
リトライ3: 4000ms + random(0-4000ms) = 4000-8000ms

なぜ重要か:

  • セルフDDoS防止: すべてのクライアントが同時にリトライしないよう、Jitterでランダム化
  • サーバー負荷の軽減: Exponential Backoffでサーバーへの負荷を段階的に軽減

フラグ切替・一時停止がコード修正なしで可能な設計。

// ✅ 良い例: 機能フラグによる制御
function isFeatureEnabled($key) {
$pdo = getConnection();
$stmt = $pdo->prepare("SELECT enabled FROM feature_flags WHERE key = ?");
$stmt->execute([$key]);
$flag = $stmt->fetch();
return $flag ? (bool)$flag['enabled'] : false;
}
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();
// 機能フラグで外部API呼び出しを制御
if (isFeatureEnabled('payment-api.enabled')) {
try {
chargePayment($orderId, $orderData['amount']);
} catch (\Exception $e) {
// 外部APIが無効化されている場合、エラーをログに記録するが処理は続行
error_log("Payment API disabled, skipping payment: order_id={$orderId}, error={$e->getMessage()}");
}
} else {
error_log("Payment API disabled by feature flag");
}
return $orderId;
}
// 管理画面で機能フラグを切り替え可能
function enableFeatureFlag($key) {
$pdo = getConnection();
$stmt = $pdo->prepare("INSERT INTO feature_flags (key, enabled) VALUES (?, 1) ON DUPLICATE KEY UPDATE enabled = 1");
$stmt->execute([$key]);
}
function disableFeatureFlag($key) {
$pdo = getConnection();
$stmt = $pdo->prepare("UPDATE feature_flags SET enabled = 0 WHERE key = ?");
$stmt->execute([$key]);
}

なぜ重要か:

  • 迅速な対応: コード修正なしで、機能を一時的に無効化可能
  • リスクの軽減: 問題が発生した場合、すぐに機能を無効化して影響を最小化

復旧設計とフォールバック戦略のポイント:

  • Graceful Degradation: 外部依存が落ちたらキャッシュ・スタブで縮退運転
  • 再起動安全性: 再起動時に中途状態をリカバリ可能にする
  • バックオフ戦略: Exponential Backoff + JitterでセルフDDoSを防止
  • 手動オペ対応: フラグ切替・一時停止がコード修正なしで可能な設計

これらの設計により、障害から自動/手動で安全に戻れます。