復旧設計とフォールバック戦略
復旧設計とフォールバック戦略
Section titled “復旧設計とフォールバック戦略”障害から自動/手動で安全に戻れる設計を詳しく解説します。
Graceful Degradation
Section titled “Graceful Degradation”外部依存が落ちたらキャッシュ・スタブで縮退運転する。
// ✅ 良い例: キャッシュによるフォールバック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, };}なぜ重要か:
- 可用性の向上: 外部依存が落ちても、最低限のサービスを提供可能
- ユーザー体験: 完全なエラーではなく、スタブデータを返すことでユーザー体験を維持
再起動安全性
Section titled “再起動安全性”再起動時に中途状態をリカバリ可能にする(例:処理キューの再読込)。
// ✅ 良い例: 再起動時に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();});なぜ重要か:
- データの整合性: 再起動時に中途状態のデータをリカバリ可能
- 自動復旧: 手動介入なしで自動的に復旧可能
バックオフ戦略
Section titled “バックオフ戦略”Exponential Backoff とJitterでセルフDDoSを防止する。
// ✅ 良い例: Exponential Backoff + Jitterasync 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でサーバーへの負荷を段階的に軽減
手動オペ対応
Section titled “手動オペ対応”フラグ切替・一時停止がコード修正なしで可能な設計。
// ✅ 良い例: 機能フラグによる制御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を防止
- 手動オペ対応: フラグ切替・一時停止がコード修正なしで可能な設計
これらの設計により、障害から自動/手動で安全に戻れます。