「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分