EXERCISE 60分

Mission 1: レイテンシ分布の分析

以下のレイテンシデータを分析し、パフォーマンス状況を評価せよ。

const latencies = [
  12, 15, 18, 22, 25, 14, 16, 19, 23, 27,
  13, 17, 20, 24, 28, 11, 14, 18, 21, 26,
  120, 135, 150, 180, 210,  // 時折遅い
  850, 1200, 2500,          // テールレイテンシ
  15, 16, 18, 20, 22, 14, 17, 19, 23, 25,
];

要件:

  1. 平均値、p50、p90、p95、p99 を計算
  2. Apdex スコアを T=100ms で計算
  3. SLO「p99 < 500ms を 99.9% の時間で達成」に対するエラーバジェット消費を評価
  4. ボトルネックの原因仮説を3つ挙げ、調査方法を提案
解答例
function analyzeLatencies(latencies: number[]): void {
  const sorted = [...latencies].sort((a, b) => a - b);
  const n = sorted.length;

  // 1. 基本統計
  const avg = latencies.reduce((s, v) => s + v, 0) / n;
  const p50 = sorted[Math.floor(n * 0.50)];
  const p90 = sorted[Math.floor(n * 0.90)];
  const p95 = sorted[Math.floor(n * 0.95)];
  const p99 = sorted[Math.floor(n * 0.99)];

  console.log(`Average: ${avg.toFixed(1)}ms`);  // ~85ms
  console.log(`p50: ${p50}ms`);                  // ~20ms
  console.log(`p90: ${p90}ms`);                  // ~150ms
  console.log(`p95: ${p95}ms`);                  // ~850ms
  console.log(`p99: ${p99}ms`);                  // ~2500ms

  // 2. Apdex (T=100ms)
  const T = 100;
  let satisfied = 0, tolerating = 0, frustrated = 0;
  for (const l of latencies) {
    if (l <= T) satisfied++;
    else if (l <= T * 4) tolerating++;
    else frustrated++;
  }
  const apdex = (satisfied + tolerating * 0.5) / n;
  console.log(`Apdex: ${apdex.toFixed(3)}`);  // ~0.80 (Fair)

  // 3. SLO 評価
  const violationCount = latencies.filter(l => l >= 500).length;
  const violationRate = violationCount / n;
  console.log(`SLO violations: ${violationCount}/${n} (${(violationRate * 100).toFixed(2)}%)`);
  // p99 < 500ms SLO → 3/38 = 7.9% が違反 → SLO 未達

  // 4. ボトルネック仮説
  // 仮説A: 外部API呼び出しのタイムアウト → 分散トレーシングで外部呼び出しを確認
  // 仮説B: GC の停止時間 → GC ログを確認、ヒープサイズを調査
  // 仮説C: DBコネクションプール枯渇 → コネクション待ち時間を計測
}

Mission 2: アムダールの法則による容量計画

以下のシステムのスケーリング計画を策定せよ。

現在の構成:
- Web サーバー: 4台
- 現在のスループット: 2,000 req/s
- 目標スループット: 10,000 req/s
- 平均レイテンシ: 80ms
- シリアル処理(DB書き込み、外部API): 推定20%

要件:

  1. アムダールの法則で、目標達成に必要なノード数を計算
  2. USL を考慮した場合の現実的なノード数を推定(σ=0.05, κ=0.002)
  3. 目標達成が困難な場合、シリアル処理を削減するための具体的施策を3つ提案
解答例
// 1. アムダールの法則
const parallelFraction = 0.80;
const targetSpeedupRatio = 10000 / 2000; // 5倍

// S(N) = 1 / ((1-P) + P/N)
// 5 = 1 / (0.2 + 0.8/N)
// 0.2 + 0.8/N = 0.2
// 0.8/N = 0  → N = ∞
// 最大高速化 = 1/0.2 = 5倍 → ぎりぎり達成可能

// 実際には N → ∞ では4倍の比率でしか到達しないため
// 必要ノード数の逆算
function requiredNodes(targetSpeedup: number, p: number): number {
  // S = 1 / ((1-p) + p/N)
  // (1-p) + p/N = 1/S
  // p/N = 1/S - (1-p)
  // N = p / (1/S - (1-p))
  const denominator = (1 / targetSpeedup) - (1 - p);
  if (denominator <= 0) return Infinity;
  return p / denominator;
}

// 5倍には理論上 ∞ ノード必要(限界値が5.0)
// 4.5倍なら: N = 0.8 / (1/4.5 - 0.2) = 0.8 / 0.022 = 36ノード
// 4倍なら: N = 0.8 / (1/4 - 0.2) = 0.8 / 0.05 = 16ノード

// 2. USL考慮
function uslCapacity(sigma: number, kappa: number, n: number): number {
  return n / (1 + sigma * (n - 1) + kappa * n * (n - 1));
}

// σ=0.05, κ=0.002 の場合
// 最適ノード数を探索
let maxCap = 0;
let optN = 1;
for (let n = 1; n <= 100; n++) {
  const cap = uslCapacity(0.05, 0.002, n);
  if (cap > maxCap) { maxCap = cap; optN = n; }
}
// optN ≈ 15, maxCap ≈ 5.8倍
// 15ノードで約 5.8倍 → 2000 * 5.8 = 11,600 req/s → 目標達成可能

// 3. シリアル処理削減施策
// 施策A: DB書き込みを非同期化(キューイング)→ シリアル部分を20%→5%に
// 施策B: 外部API呼び出しを並列化(Promise.all)→ 待ち時間を削減
// 施策C: リードレプリカ導入 + CQRS → 読み取りの並列度向上

Mission 3: パフォーマンスバジェットの設計

以下の EC サイトに対してパフォーマンスバジェットを設計せよ。

サービス概要:
- 月間 PV: 500万
- 平均注文数: 1万件/日
- ピーク時間: 12:00-13:00, 20:00-22:00
- モバイルユーザー比率: 70%
- 直近の障害: 商品検索の p99 が 3秒を超え、コンバージョン率が 15% 低下

要件:

  1. フロントエンド・バックエンド合わせて10個以上のバジェットを定義
  2. 各バジェットの根拠を説明
  3. リグレッション検出の仕組みを設計
解答例
const ecSiteBudgets: PerformanceBudget[] = [
  // フロントエンド (モバイル重視)
  { name: 'LCP', metric: 'web-vitals.lcp', threshold: 2500, unit: 'ms',
    severity: 'error', comparison: 'lt' },
  // 根拠: Google の Good 基準 2.5s、モバイル70%ならこの基準は必須

  { name: 'INP', metric: 'web-vitals.inp', threshold: 200, unit: 'ms',
    severity: 'error', comparison: 'lt' },
  // 根拠: インタラクション応答性、カート操作の体感に直結

  { name: 'CLS', metric: 'web-vitals.cls', threshold: 0.1, unit: 'score',
    severity: 'error', comparison: 'lt' },
  // 根拠: 画像遅延読み込みによるレイアウトシフト防止

  { name: 'JS Bundle (main)', metric: 'bundle.main.gzip', threshold: 200 * 1024, unit: 'bytes',
    severity: 'error', comparison: 'lt' },
  // 根拠: 3Gモバイルで2秒以内にパース完了

  { name: 'Total Page Weight', metric: 'page.total.weight', threshold: 1500 * 1024, unit: 'bytes',
    severity: 'warning', comparison: 'lt' },
  // 根拠: モバイルデータ通信量を考慮

  { name: 'HTTP Requests', metric: 'page.requests.count', threshold: 40, unit: 'count',
    severity: 'warning', comparison: 'lt' },

  // バックエンド
  { name: 'Search API p99', metric: 'api.search.p99', threshold: 500, unit: 'ms',
    severity: 'error', comparison: 'lt' },
  // 根拠: 直近障害の再発防止、3秒→500msに大幅改善

  { name: 'Cart API p99', metric: 'api.cart.p99', threshold: 200, unit: 'ms',
    severity: 'error', comparison: 'lt' },
  // 根拠: カート操作はコンバージョンに直結

  { name: 'Checkout p99', metric: 'api.checkout.p99', threshold: 1000, unit: 'ms',
    severity: 'error', comparison: 'lt' },
  // 根拠: 決済処理を含むため緩め

  { name: 'API Error Rate', metric: 'api.error_rate', threshold: 0.005, unit: 'ratio',
    severity: 'error', comparison: 'lt' },
  // 根拠: 0.5%以下、月間500万PVで25,000エラーが上限

  { name: 'DB Query p95', metric: 'db.query.p95', threshold: 30, unit: 'ms',
    severity: 'warning', comparison: 'lt' },
];

// リグレッション検出の仕組み
// 1. PR ごとに k6 負荷テストを実行(ベースラインブランチと比較)
// 2. マン・ホイットニー U 検定で有意差を判定
// 3. 有意に悪化した場合、PR にコメントで警告
// 4. severity: error のバジェット違反は CI を失敗させる
// 5. 週次でトレンドレポートを Slack に送信

Mission 4: フレームグラフの分析

以下の folded stack 形式のプロファイルデータから、ボトルネックを特定し改善策を提案せよ。

main;handleRequest;authenticate;verifyJWT 150
main;handleRequest;authenticate;fetchUserFromDB 820
main;handleRequest;authorize;checkPermissions 45
main;handleRequest;processOrder;validateInput 30
main;handleRequest;processOrder;calculatePrice;fetchProductPrices 340
main;handleRequest;processOrder;calculatePrice;applyDiscounts 55
main;handleRequest;processOrder;calculatePrice;calculateTax 20
main;handleRequest;processOrder;saveOrder;beginTransaction 10
main;handleRequest;processOrder;saveOrder;insertOrder 180
main;handleRequest;processOrder;saveOrder;insertOrderItems 450
main;handleRequest;processOrder;saveOrder;commitTransaction 15
main;handleRequest;processOrder;sendNotification;formatEmail 25
main;handleRequest;processOrder;sendNotification;sendSMTP 680
main;handleRequest;serializeResponse;toJSON 40
main;handleRequest;serializeResponse;compress 85

要件:

  1. 各処理のCPU時間の割合を計算
  2. 上位3つのボトルネックを特定
  3. それぞれの改善策を具体的に提案(コード例付き)
解答例
// 1. CPU時間の分析
const totalSamples = 2945;
const breakdown = [
  { func: 'fetchUserFromDB', samples: 820, pct: 27.8 },      // 1位
  { func: 'sendSMTP', samples: 680, pct: 23.1 },             // 2位
  { func: 'insertOrderItems', samples: 450, pct: 15.3 },     // 3位
  { func: 'fetchProductPrices', samples: 340, pct: 11.5 },
  { func: 'insertOrder', samples: 180, pct: 6.1 },
  { func: 'verifyJWT', samples: 150, pct: 5.1 },
  // ... 残りは軽微
];

// 2. 上位3ボトルネック
// #1: fetchUserFromDB (27.8%) - 認証のたびにDBアクセス
// #2: sendSMTP (23.1%) - 同期的なメール送信
// #3: insertOrderItems (15.3%) - 注文明細の逐次INSERT

// 3. 改善策

// #1: fetchUserFromDB → キャッシュ導入
async function authenticateOptimized(token: string): Promise<User> {
  const payload = verifyJWT(token);
  const cacheKey = `user:${payload.userId}`;

  // Redis キャッシュを確認
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(payload.userId);
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 300); // 5分TTL
  return user;
}

// #2: sendSMTP → 非同期キューに変更
async function sendNotificationAsync(order: Order): Promise<void> {
  // 同期的にSMTP送信する代わりにキューに投入
  await messageQueue.publish('notifications', {
    type: 'order_confirmation',
    orderId: order.id,
    email: order.customerEmail,
    data: formatEmailData(order),
  });
  // メール送信はバックグラウンドワーカーが処理
}

// #3: insertOrderItems → バルクINSERT
async function saveOrderOptimized(order: Order): Promise<void> {
  await db.transaction(async (trx) => {
    await trx.orders.insert(order);

    // 逐次INSERTではなくバルクINSERT
    await trx.orderItems.insertMany(
      order.items.map(item => ({
        orderId: order.id,
        productId: item.productId,
        quantity: item.quantity,
        price: item.price,
      }))
    );
  });
}

// 改善効果の推定:
// fetchUserFromDB: 820 → ~50 (キャッシュヒット率94%想定)
// sendSMTP: 680 → ~5 (キューへのpublishのみ)
// insertOrderItems: 450 → ~60 (バルクINSERT)
// 合計: 2945 → ~1050 (約64%の削減)