Skip to content

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

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

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

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

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

// ✅ 良い例: キャッシュによるフォールバック
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function getProduct(productId: number): Promise<Product> {
// 1. キャッシュから取得を試みる
const cached = await redis.get(`product:${productId}`);
if (cached) {
return JSON.parse(cached);
}
// 2. データベースから取得を試みる
try {
const product = await prisma.product.findUnique({
where: { id: productId },
});
if (!product) {
throw new Error('Product not found');
}
// キャッシュに保存(TTL: 1時間)
await redis.setex(`product:${productId}`, 3600, JSON.stringify(product));
return product;
} catch (error) {
// 3. データベースが落ちている場合、外部APIから取得を試みる
try {
const response = await fetch(`https://external-api.example.com/products/${productId}`);
const product = await response.json();
// キャッシュに保存(次回はキャッシュから取得可能)
await redis.setex(`product:${productId}`, 3600, JSON.stringify(product));
return product;
} catch (apiError) {
// 4. すべて失敗した場合、スタブデータを返す
logger.warn('All data sources failed, returning stub data', {
productId,
dbError: error.message,
apiError: apiError.message,
});
return createStubProduct(productId);
}
}
}
function createStubProduct(productId: number): Product {
// スタブデータ(最低限の情報のみ)
return {
id: productId,
name: 'Product information temporarily unavailable',
price: 0,
};
}

なぜ重要か:

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

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

// ✅ 良い例: 再起動時にOutboxを再処理
async function recoverPendingEvents() {
// アプリケーション起動時に、PENDING状態のイベントを再処理
const pendingEvents = await prisma.outbox.findMany({
where: { status: 'PENDING' },
});
logger.info(`Recovering ${pendingEvents.length} pending outbox events`);
for (const event of pendingEvents) {
// リトライ回数が上限を超えていない場合のみ再処理
if (event.retryCount < 3) {
await processOutboxEvent(event);
} else {
logger.warn('Outbox event exceeded retry limit', {
eventId: event.id,
retryCount: event.retryCount,
});
// 手動対応が必要な状態としてマーク
await prisma.outbox.update({
where: { id: event.id },
data: { status: 'MANUAL_REVIEW_REQUIRED' },
});
}
}
}
// アプリケーション起動時に実行
app.on('ready', async () => {
await recoverPendingEvents();
});

なぜ重要か:

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

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

// ✅ 良い例: Exponential Backoff + Jitter
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) {
throw error;
}
// Exponential Backoff + Jitter
const baseDelay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
const jitter = Math.random() * 1000; // 0-1s
const delay = baseDelay + jitter;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}
async function chargePayment(orderId: number, amount: number): Promise<PaymentResult> {
return await retryWithBackoff(async () => {
const response = await fetch('https://payment-api.example.com/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount }),
});
if (!response.ok) {
throw new Error('Payment failed');
}
return await response.json();
});
}

バックオフの計算:

リトライ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でサーバーへの負荷を段階的に軽減

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

// ✅ 良い例: 機能フラグによる制御
interface FeatureFlag {
key: string;
enabled: boolean;
description: string;
}
const featureFlags = new Map<string, FeatureFlag>();
async function isFeatureEnabled(key: string): Promise<boolean> {
const flag = featureFlags.get(key);
return flag?.enabled ?? false;
}
app.post('/orders', async (req, res) => {
const order = await createOrder(req.body);
// 機能フラグで外部API呼び出しを制御
if (await isFeatureEnabled('payment-api.enabled')) {
try {
await paymentService.chargePayment(order.id, order.amount);
} catch (error) {
// 外部APIが無効化されている場合、エラーをログに記録するが処理は続行
logger.warn('Payment API disabled, skipping payment', {
orderId: order.id,
});
}
} else {
logger.info('Payment API disabled by feature flag');
}
res.json(order);
});
// 管理画面で機能フラグを切り替え可能
app.post('/admin/feature-flags/:key/enable', async (req, res) => {
const flag = featureFlags.get(req.params.key);
if (flag) {
flag.enabled = true;
res.json({ message: 'Feature flag enabled' });
} else {
res.status(404).json({ error: 'Feature flag not found' });
}
});
app.post('/admin/feature-flags/:key/disable', async (req, res) => {
const flag = featureFlags.get(req.params.key);
if (flag) {
flag.enabled = false;
res.json({ message: 'Feature flag disabled' });
} else {
res.status(404).json({ error: 'Feature flag not found' });
}
});

なぜ重要か:

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

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

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

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