LESSON 30分

「10台サーバーを並べたら10倍速くなる、なんて思ってないよね?」佐藤CTOが苦笑した。「並列化できない部分が1%でもあれば、どれだけサーバーを足しても100倍が限界だ。これがアムダールの法則だ。」

1. アムダールの法則(Amdahl’s Law)

基本公式

S(N) = 1 / ((1 - P) + P/N)

S(N): N プロセッサでの高速化率
P:    並列化可能な割合 (0〜1)
N:    プロセッサ数(並列度)
// アムダールの法則のシミュレーション
function amdahlSpeedup(parallelFraction: number, processors: number): number {
  const serialFraction = 1 - parallelFraction;
  return 1 / (serialFraction + parallelFraction / processors);
}

// 並列化率ごとの最大高速化
function printAmdahlTable(): void {
  const parallelFractions = [0.5, 0.75, 0.9, 0.95, 0.99];
  const processorCounts = [2, 4, 8, 16, 64, 256, Infinity];

  console.log('P\\N\t' + processorCounts.map(n => n === Infinity ? '∞' : n).join('\t'));

  for (const p of parallelFractions) {
    const speedups = processorCounts.map(n =>
      amdahlSpeedup(p, n).toFixed(2)
    );
    console.log(`${(p * 100)}%\t${speedups.join('\t')}`);
  }
}

// 出力:
// P\N     2     4     8     16    64    256   ∞
// 50%    1.33  1.60  1.78  1.88  1.97  1.99  2.00
// 75%    1.60  2.29  2.91  3.37  3.82  3.95  4.00
// 90%    1.82  3.08  4.71  6.40  8.77  9.58  10.00
// 95%    1.90  3.48  5.93  9.14  15.42 18.28 20.00
// 99%    1.98  3.88  7.48  13.91 39.26 72.11 100.00

実践的な含意

並列化率N=∞ での限界教訓
50%2倍シリアル処理が半分あればスケールしない
90%10倍10%のシリアル部分が壁になる
95%20倍大規模分散でも20倍が限界
99%100倍シリアル部分1%でも100倍止まり

2. USL(Universal Scalability Law)

アムダールの法則を拡張し、コヒーレンシ(一貫性)コストも考慮するモデル。

C(N) = N / (1 + σ(N-1) + κN(N-1))

C(N): N プロセッサでの相対容量
σ:    競合(contention)パラメータ
κ:    コヒーレンシ(coherency)パラメータ
N:    ノード数
// USL モデルのシミュレーション
interface UslParams {
  sigma: number;  // 競合パラメータ (0〜1)
  kappa: number;  // コヒーレンシパラメータ (0〜1)
}

function uslCapacity(params: UslParams, n: number): number {
  const { sigma, kappa } = params;
  return n / (1 + sigma * (n - 1) + kappa * n * (n - 1));
}

function findOptimalNodes(params: UslParams): { nodes: number; capacity: number } {
  let maxCapacity = 0;
  let optimalNodes = 1;

  for (let n = 1; n <= 1000; n++) {
    const capacity = uslCapacity(params, n);
    if (capacity > maxCapacity) {
      maxCapacity = capacity;
      optimalNodes = n;
    } else if (capacity < maxCapacity * 0.99) {
      // 容量が減少し始めたら終了
      break;
    }
  }

  return { nodes: optimalNodes, capacity: maxCapacity };
}

// 例1: 低競合・低コヒーレンシ(理想的)
const ideal = findOptimalNodes({ sigma: 0.02, kappa: 0.0001 });
// → nodes: ~70, capacity: ~35x

// 例2: 高競合(ロック競合が多い)
const highContention = findOptimalNodes({ sigma: 0.1, kappa: 0.001 });
// → nodes: ~30, capacity: ~7x

// 例3: 高コヒーレンシ(分散キャッシュの同期コスト)
const highCoherency = findOptimalNodes({ sigma: 0.02, kappa: 0.005 });
// → nodes: ~10, capacity: ~6x

// USL パラメータの回帰分析
// 実測データ (nodes, throughput) から σ と κ を推定
function fitUslParams(
  data: Array<{ nodes: number; throughput: number }>
): UslParams {
  // 最小二乗法で σ と κ を推定
  // C(N)/N = 1/(1 + σ(N-1) + κN(N-1))
  // N/C(N) = 1 + σ(N-1) + κN(N-1)
  // y = 1 + σx1 + κx2 where x1 = N-1, x2 = N(N-1), y = N/C(N)

  const baseline = data[0].throughput; // N=1 のスループット
  const points = data.map(d => ({
    x1: d.nodes - 1,
    x2: d.nodes * (d.nodes - 1),
    y: d.nodes / (d.throughput / baseline),
  }));

  // 線形回帰 y = 1 + σ*x1 + κ*x2
  let sumX1Y = 0, sumX2Y = 0, sumX1X1 = 0, sumX2X2 = 0, sumX1X2 = 0;
  for (const p of points) {
    const yAdj = p.y - 1;
    sumX1Y += p.x1 * yAdj;
    sumX2Y += p.x2 * yAdj;
    sumX1X1 += p.x1 * p.x1;
    sumX2X2 += p.x2 * p.x2;
    sumX1X2 += p.x1 * p.x2;
  }

  const det = sumX1X1 * sumX2X2 - sumX1X2 * sumX1X2;
  const sigma = (sumX2X2 * sumX1Y - sumX1X2 * sumX2Y) / det;
  const kappa = (sumX1X1 * sumX2Y - sumX1X2 * sumX1Y) / det;

  return { sigma: Math.max(0, sigma), kappa: Math.max(0, kappa) };
}

3. 垂直スケーリング vs 水平スケーリング

項目垂直(スケールアップ)水平(スケールアウト)
方法CPU/メモリ増強ノード数追加
限界ハードウェア上限アムダール/USLの限界
コスト指数的に増加線形に増加(理想)
複雑さ低い高い(分散システム)
ダウンタイム必要な場合ありローリング可能
適用場面DB、シングルスレッド処理Webサーバー、ステートレス処理
// スケーリング戦略の判断フレームワーク
interface ScalingDecision {
  strategy: 'vertical' | 'horizontal' | 'both';
  rationale: string;
  estimatedCost: number;
  estimatedCapacityGain: number;
}

function decideScalingStrategy(
  currentMetrics: {
    cpuUtilization: number;
    memoryUtilization: number;
    connectionPoolSaturation: number;
    parallelizableFraction: number;
    currentNodes: number;
  }
): ScalingDecision {
  const { cpuUtilization, memoryUtilization, connectionPoolSaturation,
          parallelizableFraction, currentNodes } = currentMetrics;

  // CPU/メモリが高く、並列化率が低い → 垂直スケーリング
  if (parallelizableFraction < 0.7 && (cpuUtilization > 80 || memoryUtilization > 80)) {
    return {
      strategy: 'vertical',
      rationale: '並列化率が低いため、水平スケールの効果が限定的',
      estimatedCost: 1.5, // 相対コスト
      estimatedCapacityGain: 1.8,
    };
  }

  // 並列化率が高く、まだノード追加の余地あり → 水平スケーリング
  const maxEffectiveNodes = Math.floor(1 / (1 - parallelizableFraction));
  if (currentNodes < maxEffectiveNodes * 0.5) {
    return {
      strategy: 'horizontal',
      rationale: `並列化率${(parallelizableFraction*100).toFixed(0)}%、最大効果${maxEffectiveNodes}ノードまで余地あり`,
      estimatedCost: 1.0 * (currentNodes + 2) / currentNodes,
      estimatedCapacityGain: amdahlSpeedup(parallelizableFraction, currentNodes + 2)
        / amdahlSpeedup(parallelizableFraction, currentNodes),
    };
  }

  // コネクションプール飽和 → まず垂直、次に水平
  if (connectionPoolSaturation > 80) {
    return {
      strategy: 'both',
      rationale: 'コネクションプール飽和 — DBの垂直スケール + アプリの水平スケール',
      estimatedCost: 2.5,
      estimatedCapacityGain: 2.0,
    };
  }

  return {
    strategy: 'horizontal',
    rationale: 'デフォルト: 水平スケーリング',
    estimatedCost: 1.2,
    estimatedCapacityGain: 1.5,
  };
}

4. 並列化のボトルネックパターン

// よくあるシリアルボトルネックとその解消法

// パターン1: 逐次的なAPI呼び出し(非並列化)
// Before: シリアル実行
async function fetchUserDataSerial(userId: string) {
  const profile = await fetchProfile(userId);     // 100ms
  const orders = await fetchOrders(userId);       // 150ms
  const recommendations = await fetchRecommendations(userId); // 200ms
  return { profile, orders, recommendations };     // Total: 450ms
}

// After: 並列実行
async function fetchUserDataParallel(userId: string) {
  const [profile, orders, recommendations] = await Promise.all([
    fetchProfile(userId),       // 100ms
    fetchOrders(userId),        // 150ms
    fetchRecommendations(userId), // 200ms
  ]);
  return { profile, orders, recommendations }; // Total: 200ms (max)
}

// パターン2: グローバルロックの排除
// Before: 単一のロックで直列化
class GlobalLockCache {
  private lock = new Mutex();
  private cache = new Map<string, unknown>();

  async get(key: string): Promise<unknown> {
    const release = await this.lock.acquire();
    try {
      return this.cache.get(key);
    } finally {
      release();
    }
  }
}

// After: キーごとのロック(ストライプロック)
class StripedLockCache {
  private readonly stripes: Mutex[];
  private cache = new Map<string, unknown>();

  constructor(stripeCount = 16) {
    this.stripes = Array.from({ length: stripeCount }, () => new Mutex());
  }

  private getStripe(key: string): Mutex {
    const hash = this.hashCode(key);
    return this.stripes[hash % this.stripes.length];
  }

  async get(key: string): Promise<unknown> {
    const release = await this.getStripe(key).acquire();
    try {
      return this.cache.get(key);
    } finally {
      release();
    }
  }

  private hashCode(str: string): number {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
    }
    return Math.abs(hash);
  }
}

// 型定義
declare class Mutex {
  acquire(): Promise<() => void>;
}
declare function fetchProfile(userId: string): Promise<any>;
declare function fetchOrders(userId: string): Promise<any>;
declare function fetchRecommendations(userId: string): Promise<any>;
コラム: グスタフソンの法則

アムダールの法則は「問題サイズ固定」の前提。グスタフソンの法則は「実行時間固定」で考える。

S(N) = N - σ(N - 1)

σ: シリアル処理の割合

「プロセッサを増やしたら、同じ時間でより大きな問題を解ける」という考え方。

ビッグデータ処理(MapReduce等)ではグスタフソンの方が現実的。データ量がノード数に応じて増えるため。

まとめ

トピック要点
アムダールの法則並列化できない部分が全体の限界を決める
USL競合 + コヒーレンシのコストでスケーラビリティが低下
垂直 vs 水平並列化率・コスト・複雑さで判断
ボトルネックパターン逐次API呼び出し、グローバルロックなど

チェックリスト

  • アムダールの法則の公式と含意を説明できる
  • USL の σ と κ の意味を理解した
  • 垂直・水平スケーリングの判断基準を説明できる
  • シリアルボトルネックの特定と解消パターンを知っている

次のステップへ

スケーラビリティの理論を理解した。次は パフォーマンスバジェット を学び、性能目標の設定と維持の方法を身につけよう。

推定読了時間: 30分