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,
];
要件:
- 平均値、p50、p90、p95、p99 を計算
- Apdex スコアを T=100ms で計算
- SLO「p99 < 500ms を 99.9% の時間で達成」に対するエラーバジェット消費を評価
- ボトルネックの原因仮説を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%
要件:
- アムダールの法則で、目標達成に必要なノード数を計算
- USL を考慮した場合の現実的なノード数を推定(σ=0.05, κ=0.002)
- 目標達成が困難な場合、シリアル処理を削減するための具体的施策を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% 低下
要件:
- フロントエンド・バックエンド合わせて10個以上のバジェットを定義
- 各バジェットの根拠を説明
- リグレッション検出の仕組みを設計
解答例
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
要件:
- 各処理のCPU時間の割合を計算
- 上位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%の削減)