LESSON 25分

ストーリー

あなた
APIのレスポンスが遅いです。コードを最適化しましょうか?

高橋アーキテクトは首を振った。

高橋アーキテクト
待て。まず原因を特定しろ。もしボトルネックがデータベースにあるなら、アプリケーションコードをいくらチューニングしても無駄だ。制約理論を知っているか?
あなた
制約理論…ですか?
高橋アーキテクト
チェーンの強さは最も弱いリンクで決まる。システムも同じだ。最も遅い箇所を見つけない限り、全体は速くならない

4つのボトルネック

1. CPU バウンド

CPUの計算能力が制約となるケース。暗号化処理、画像変換、複雑なビジネスロジックなどが該当します。

// CPU バウンドの例:大量データの集計処理
function aggregateReport(records: Record[]): Report {
  // 数百万レコードのフィルタリング・集計
  return records
    .filter(r => r.isActive)
    .map(r => transformRecord(r))     // 重い変換処理
    .reduce((acc, r) => merge(acc, r), initialReport);
}

// 対策例:Worker Thread で並列化
import { Worker } from 'worker_threads';

async function parallelAggregate(records: Record[]): Promise<Report> {
  const chunkSize = Math.ceil(records.length / 4); // 4スレッドに分割
  const chunks = splitIntoChunks(records, chunkSize);

  const results = await Promise.all(
    chunks.map(chunk => runInWorker(chunk))
  );

  return mergeReports(results);
}

兆候: CPU使用率が常に90%以上、処理待ちキューの増大

2. I/O バウンド

ディスクI/Oやネットワーク通信の待ち時間が制約となるケース。

// I/O バウンドの例:逐次的なAPI呼び出し
async function fetchAllData(): Promise<Data[]> {
  // 悪い例:1つずつ順番に待つ
  const users = await fetchUsers();           // 200ms
  const orders = await fetchOrders();         // 300ms
  const products = await fetchProducts();     // 150ms
  // 合計: 650ms

  return combineData(users, orders, products);
}

// 改善:並列実行
async function fetchAllDataOptimized(): Promise<Data[]> {
  const [users, orders, products] = await Promise.all([
    fetchUsers(),     // 200ms
    fetchOrders(),    // 300ms
    fetchProducts(),  // 150ms
  ]);
  // 合計: 300ms(最も遅い処理の時間)

  return combineData(users, orders, products);
}

兆候: CPU使用率は低いのに処理が遅い、I/O wait が高い

3. メモリバウンド

メモリ不足やGC(ガベージコレクション)が制約となるケース。

// メモリバウンドの例:大量データの一括読み込み
async function processLargeFile(path: string): Promise<void> {
  // 悪い例:全データをメモリに読み込む
  const allData = await fs.readFile(path, 'utf-8'); // 数GBのファイル
  const lines = allData.split('\n');
  lines.forEach(line => process(line));

  // 改善:ストリーム処理
}

// 改善:ストリームで少しずつ処理
async function processLargeFileStream(path: string): Promise<void> {
  const stream = fs.createReadStream(path, { encoding: 'utf-8' });
  const rl = readline.createInterface({ input: stream });

  for await (const line of rl) {
    await process(line); // 1行ずつ処理
  }
}

兆候: GC停止が頻発、スワップ発生、OOM(Out of Memory)エラー

4. ネットワークバウンド

ネットワーク帯域や接続数が制約となるケース。

// ネットワークバウンドの対策例
class NetworkOptimizer {
  // 1. レスポンスの圧縮
  enableCompression(): void {
    // gzip/brotli 圧縮で転送量を50-80%削減
  }

  // 2. バッチリクエスト
  async batchFetch(ids: string[]): Promise<Item[]> {
    // N回のAPI呼び出しを1回にまとめる
    return fetch('/api/items/batch', {
      method: 'POST',
      body: JSON.stringify({ ids }),
    }).then(r => r.json());
  }

  // 3. コネクションプーリング
  createPool(): Pool {
    return new Pool({
      max: 20,        // 最大接続数
      idleTimeout: 30000,
    });
  }
}

兆候: 帯域使用率が上限に近い、接続タイムアウトの頻発


ボトルネック特定のフローチャート

レスポンスが遅い

  ├─ CPU使用率は高い?
  │   ├─ Yes → CPU バウンド
  │   │        → プロファイリングでホットスポット特定
  │   └─ No ─┐
  │          │
  ├─ I/O wait は高い?
  │   ├─ Yes → I/O バウンド
  │   │        → 非同期化、キャッシュ導入を検討
  │   └─ No ─┐
  │          │
  ├─ メモリ使用量は高い?
  │   ├─ Yes → メモリバウンド
  │   │        → メモリリーク調査、ストリーム化
  │   └─ No ─┐
  │          │
  └─ ネットワーク遅延は大きい?
      ├─ Yes → ネットワークバウンド
      │        → 圧縮、バッチ化、CDN導入
      └─ No → アプリケーションロジック見直し

計測ツール

カテゴリツール用途
CPUtop, htop, perfCPU使用率、プロセス別負荷
メモリfree, vmstatメモリ使用量、スワップ状況
I/Oiostat, iotopディスクI/O統計
ネットワークnetstat, ss, tcpdump接続状況、トラフィック分析
APMDatadog, New Relicアプリケーション全体の可視化

まとめ

ポイント内容
CPUバウンド計算処理が制約 → 並列化、アルゴリズム改善
I/Oバウンドディスク/ネットワーク待ちが制約 → 非同期、キャッシュ
メモリバウンドメモリ不足/GCが制約 → ストリーム処理、メモリリーク修正
ネットワークバウンド帯域/接続数が制約 → 圧縮、バッチ、CDN
原則まず計測してボトルネックを特定してから最適化する

チェックリスト

  • 4つのボトルネックの種類と兆候を理解した
  • ボトルネック特定の手順を説明できる
  • 各ボトルネックへの対策方針を把握した
  • 主要な計測ツールを知った

次のステップへ

次は「パフォーマンスバジェット」を学びます。どの程度の速度が必要かを定量的に定義する方法です。


推定読了時間: 25分