LESSON 40分

ストーリー

あなた
“遅い”って報告が来たんですが、どこが遅いのかわかりません…

高橋アーキテクトがJaegerでトレースを開いた。

高橋アーキテクト
トレースのウォーターフォールを見れば、どの処理に時間がかかっているか一目瞭然だ。ボトルネックの特定は、3つのパターンに分類できる。それぞれの対処法を学ぼう

ボトルネックの3つのパターン

パターン1: 単一の遅いSpan

[API Gateway]     ━━━━━━━━━━━━━━━━━━━━  2500ms
  ├─[Auth]        ━━  20ms
  ├─[Order]       ━━━━━━━━━━━━━━━━━━  2300ms
  │   ├─[DB]      ━━  10ms
  │   └─[Payment] ━━━━━━━━━━━━━━━━  2200ms  ← ボトルネック!
  │       └─[Stripe API] ━━━━━━━━━━━━━━  2150ms
  └─[Notify]      ━━  15ms

原因: 外部APIの遅延、重いDBクエリ、計算量の多い処理 対策: タイムアウト設定、キャッシュ、非同期処理への変更

パターン2: 多数のシーケンシャル呼び出し

[Order Service]  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  1800ms
  ├─[DB: get user]        ━━━  50ms
  ├─[DB: get order]       ━━━  50ms    順番に実行
  ├─[DB: get items]       ━━━  50ms    されている!
  ├─[DB: get inventory]   ━━━  50ms
  ├─[DB: get pricing]     ━━━  50ms
  ├─[DB: get shipping]    ━━━  50ms
  ├─[DB: get tax]         ━━━  50ms
  ├─[Validate]            ━━━  50ms
  ├─[Calculate total]     ━━━  50ms
  └─[DB: insert order]    ━━━  50ms
     合計: 50ms x 10 = 500ms(直列実行)

原因: 並列実行可能な処理を直列に実行している 対策: Promise.allで並列実行

// Before: 直列実行(遅い)
const user = await getUser(userId);
const order = await getOrder(orderId);
const items = await getItems(orderId);
const inventory = await getInventory(items);

// After: 並列実行(速い)
const [user, order, items] = await Promise.all([
  getUser(userId),
  getOrder(orderId),
  getItems(orderId),
]);
const inventory = await getInventory(items); // itemsに依存するので後

パターン3: N+1問題

[Order Service]  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━  1200ms
  ├─[DB: SELECT orders]          ━━  20ms     1回のクエリ
  ├─[DB: SELECT items WHERE order_id=1]  ━━  15ms  ┐
  ├─[DB: SELECT items WHERE order_id=2]  ━━  15ms  │
  ├─[DB: SELECT items WHERE order_id=3]  ━━  15ms  │ N回のクエリ
  ├─[DB: SELECT items WHERE order_id=4]  ━━  15ms  │
  │  ... (50回繰り返し)                              │
  └─[DB: SELECT items WHERE order_id=50] ━━  15ms  ┘
     合計: 20 + 15 x 50 = 770ms

原因: ループ内で個別にDBクエリを発行 対策: JOINまたはIN句でバッチ取得

// Before: N+1問題
const orders = await db.query('SELECT * FROM orders LIMIT 50');
for (const order of orders) {
  order.items = await db.query('SELECT * FROM items WHERE order_id = $1', [order.id]);
}

// After: バッチ取得
const orders = await db.query('SELECT * FROM orders LIMIT 50');
const orderIds = orders.map(o => o.id);
const items = await db.query('SELECT * FROM items WHERE order_id = ANY($1)', [orderIds]);
// メモリ上でマッピング

トレースによるボトルネック分析フロー

// 体系的な分析フロー
interface BottleneckAnalysis {
  step1: {
    action: '遅いトレースをJaegerで検索';
    query: 'Duration > 2s, Service = order-service';
  };
  step2: {
    action: 'ウォーターフォールで最も長いSpanを特定';
    check: 'Total Durationの50%以上を占めるSpanはどれか';
  };
  step3: {
    action: 'ボトルネックのパターンを判定';
    patterns: [
      '単一の遅いSpan → 外部API/重いクエリ',
      '多数の直列Span → 並列化可能',
      '同種の繰り返しSpan → N+1問題',
    ];
  };
  step4: {
    action: 'Span属性で詳細を確認';
    attributes: ['db.statement', 'http.url', 'http.status_code'];
  };
  step5: {
    action: '対策を実施し、トレースで効果を検証';
  };
}

対策パターンの一覧

ボトルネック対策効果
外部APIの遅延タイムアウト + サーキットブレーカー障害時の影響を限定
重いDBクエリインデックス追加、クエリ最適化クエリ時間短縮
直列処理Promise.allで並列化待ち時間の削減
N+1問題バッチ取得(JOIN/IN句)クエリ数の削減
キャッシュミスRedis等のキャッシュ導入DB負荷の軽減
不要な処理非同期化(キューイング)レスポンスタイム短縮
// 対策例: 非同期化
// Before: 同期的に通知を送信(レスポンスに含まれる)
async function createOrder(data: OrderInput) {
  const order = await saveOrder(data);
  await chargePayment(order);
  await sendNotification(order);  // 通知は非同期でOK
  return order;
}

// After: 通知をキューに投入(レスポンスから切り離し)
async function createOrder(data: OrderInput) {
  const order = await saveOrder(data);
  await chargePayment(order);
  await messageQueue.publish('order.created', { orderId: order.id });
  return order;  // 通知を待たずにレスポンス
}

まとめ

ポイント内容
3つのパターン単一遅延、直列実行、N+1問題
分析フロー遅いトレース検索 → 長いSpan特定 → パターン判定 → 対策
対策並列化、バッチ取得、キャッシュ、非同期化
検証対策後にトレースで効果を測定

チェックリスト

  • 3つのボトルネックパターンを説明できる
  • トレースのウォーターフォールからボトルネックを特定できる
  • N+1問題をトレースで検出し、対策を提案できる
  • 直列処理の並列化を実装できる

次のステップへ

次は演習「トレーシングを導入しよう」です。これまで学んだ知識を使って、実際にトレーシングを導入してみましょう。


推定読了時間: 40分