CAP定理を完全に理解する - 分散システムの基礎
はじめに
この記事では、分散データベースシステムの設計において避けて通れない「CAP定理」について、具体例を交えながら徹底的に解説します。
CAP定理とは
分散データベースシステムでは、以下の3つの特性を同時に満たすことは不可能という定理です。
C: Consistency (一貫性)
A: Availability (可用性)
P: Partition tolerance (分断耐性)
重要: どれか2つしか選べません!
前提知識:分散データベースシステムとは
通常のデータベース(単一サーバー)
[1台のサーバー]
┌─────────────┐
│ データベース │
│ 全データ │
└─────────────┘
↑
ユーザー
- すべてのデータが1台のマシンに保存
- シンプルだが、故障したら全て停止
- 処理能力に限界がある
分散データベース(複数サーバー)
[東京] [大阪] [福岡]
┌───────┐ ┌───────┐ ┌───────┐
│データA│◄─────►│データA│◄─────►│データA│
│データB│ 同期 │データB│ 同期 │データB│
└───────┘ └───────┘ └───────┘
↑ ↑ ↑
ユーザー ユーザー ユーザー
- データを複数のサーバーにコピー(レプリケーション)
- 1台壊れても他が動く
- 大量のアクセスを処理できる
CAPの3つの特性を詳しく解説
C: Consistency(一貫性)
定義: 全てのノードで、同じタイミングで同じデータが見える
例: 銀行口座から1,000円引き出したら、どのATMで確認しても即座に残高が減っている
// 一貫性がある場合
await db.update({ balance: 9000 }); // サーバー1で更新
// 直後に別のサーバーで読み取り
const balance = await db.read(); // → 9000円(最新データ)
A: Availability(可用性)
定義: システムの一部が故障しても、必ず応答が返ってくる
例: データセンターの1つがダウンしても、ユーザーは必ず何らかの応答を受け取れる
// 可用性が高い場合
try {
const result = await db.query();
// サーバーの一部が死んでいても、
// 生きているサーバーから応答が返る
} catch (e) {
// エラーにならない(タイムアウトしない)
}
P: Partition tolerance(分断耐性)
定義: ネットワーク障害でサーバー間の通信が切れても、システムが動き続ける
例: 東京と大阪のデータセンター間の回線が切れても、両方のセンターが独立して動作し続ける
[正常時]
東京 ◄─────► 大阪
↓ ↓
ユーザー ユーザー
[ネットワーク分断時]
東京 X 大阪 (通信不可)
↓ ↓
ユーザー ユーザー
(両方とも動作し続ける)
具体例で理解する:チャットアプリのケース
シナリオ設定
世界規模のチャットアプリで、サーバーを2箇所に配置している状況を考えます。
[アメリカ] [日本]
サーバーA ◄─────► サーバーB
↑ ↑
アリス 太郎
正常時(分断なし)
// アリスがメッセージ送信(アメリカサーバー)
alice.send("Hello!");
// ↓ 即座に同期 ↓
// 太郎が受信(日本サーバー)
taro.receive("Hello!"); // ✓ 届く
ネットワーク分断が発生!
海底ケーブルが切断されました。
[アメリカ] [日本]
サーバーA X X サーバーB
↑ 通信不可 ↑
アリス 太郎
ここで**Partition(分断)**が発生しました。
Pがある場合(分断耐性あり)
// アメリカサーバーAの処理
alice.send("Hello!");
// → 日本サーバーと同期できないけど、ローカルに保存
// → "送信成功" と応答(システムは動き続ける)
// 日本サーバーBの処理
taro.send("こんにちは!");
// → アメリカサーバーと同期できないけど、ローカルに保存
// → "送信成功" と応答(システムは動き続ける)
// 結果:
// アメリカサーバー: ["Hello!"]
// 日本サーバー: ["こんにちは!"]
// ↑ 両方とも動作している(P がある)
// ↑ でもデータが違う...(一貫性は失われている)
結果:
- ✓ アリスはアメリカの友達とチャット可能
- ✓ 太郎は日本の友達とチャット可能
- ✗ お互いのメッセージは見えない(後で同期される)
Pがない場合(分断耐性なし)
// アメリカサーバーAの処理
alice.send("Hello!");
// → 日本サーバーと同期できない...
// → エラー!システム停止!
throw new Error("Cannot sync with Japan server");
// 日本サーバーBの処理
taro.send("こんにちは!");
// → アメリカサーバーと同期できない...
// → エラー!システム停止!
throw new Error("Cannot sync with USA server");
// 結果:全世界でサービスが使えなくなる
なぜ3つ同時は不可能なのか?
シナリオ:ネットワーク分断が発生した場合
[状況]
東京サーバー: 残高 10,000円
大阪サーバー: 残高 10,000円
↓
ネットワーク切断!
↓
東京で 5,000円 引き出し
ここで3つの選択肢があります。
選択肢1: CP(一貫性 + 分断耐性)を選ぶ → Aを諦める
// 東京で引き出し操作
await tokyoDB.withdraw(5000);
// → 大阪と同期できない!
// → エラーを返す(可用性を犠牲)
// ユーザー体験:
"申し訳ございません。現在サービスを利用できません"
例: 銀行システム、証券取引システム(PostgreSQL, MySQL)
選択肢2: AP(可用性 + 分断耐性)を選ぶ → Cを諦める
// 東京で引き出し
await tokyoDB.withdraw(5000);
// → OK! 残高 5,000円(一貫性を犠牲)
// 同時に大阪でも引き出し
await osakaDB.withdraw(3000);
// → OK! 残高 7,000円(一貫性を犠牲)
// ネットワーク復旧後...
// 東京: 5,000円、大阪: 7,000円
// → 矛盾!(後で調整が必要)
例: SNS、ショッピングカート(DynamoDB, Cassandra, MongoDB)
選択肢3: CA(一貫性 + 可用性) → Pを諦める
分散システムでは実質的に不可能
理由:ネットワーク障害は必ず起きるため、
Pは「選択肢」ではなく「前提条件」
単一サーバーならCA可能だが、
それは「分散」システムではない
実際のデータベースでの適用例
CP系データベース
代表例: PostgreSQL, MySQL, MongoDB(設定次第)
適用シーン:
- 銀行の残高管理
- 在庫管理システム
- 予約システム
特徴:
- ✓ データ整合性が完璧
- ✗ 障害時にサービス停止する可能性
実装例:
// トランザクションで厳密に管理
await db.transaction(async (tx) => {
const account = await tx.account.findUnique({
where: { id: 1 }
});
if (account.balance < 5000) {
throw new Error('残高不足');
}
await tx.account.update({
where: { id: 1 },
data: { balance: account.balance - 5000 }
});
// ここで他のサーバーと同期できなければ
// 全体がロールバック(エラーになる)
});
AP系データベース
代表例: DynamoDB, Cassandra, Couchbase
適用シーン:
- SNSのタイムライン
- ログ収集
- ショッピングカート
特徴:
- ✓ 常にサービスが使える
- ✗ 一時的にデータが不整合(最終的整合性)
実装例:
// 最終的整合性(Eventually Consistent)
await dynamoDB.updateItem({
TableName: 'accounts',
Key: { id: 1 },
UpdateExpression: 'SET balance = balance - :amount',
ExpressionAttributeValues: { ':amount': 5000 }
});
// すぐに読み取ると古いデータの可能性
const account = await dynamoDB.getItem({
TableName: 'accounts',
Key: { id: 1 }
// ConsistentRead: false (デフォルト)
});
// → balance: 10,000円 (古い値の可能性)
// 数秒後には整合性が取れる
setTimeout(async () => {
const account = await dynamoDB.getItem({
TableName: 'accounts',
Key: { id: 1 }
});
// → balance: 5,000円 (正しい値)
}, 3000);
なぜPは避けられないのか
ネットワーク障害の原因(現実に起きる)
-
物理的な切断
- 海底ケーブルが船の錨で切れる(年間数百件)
- 工事でケーブルを掘り返す
- 地震、台風、火災
-
ソフトウェア障害
- ルーターのバグ
- ファイアウォールの誤設定
- DDoS攻撃
-
遅延も「分断」と同じ
// タイムアウト設定: 3秒
await syncWithServer({ timeout: 3000 });
// → 5秒かかる場合
// → タイムアウトエラー = 実質「分断」
コードで見る分断耐性の実装
Pなし(素朴な実装)
async function saveData(data) {
// 全サーバーに同期するまで待つ
await server1.save(data);
await server2.save(data);
await server3.save(data);
// ↑ 1つでも失敗したら全体が失敗
return "成功";
}
// 問題:server2が通信できないと、全体が止まる
Pあり(分断に耐える実装)
async function saveData(data) {
const results = await Promise.allSettled([
server1.save(data),
server2.save(data),
server3.save(data)
]);
// 過半数が成功すればOK(Quorum方式)
const successCount = results.filter(
r => r.status === 'fulfilled'
).length;
if (successCount >= 2) {
// 2台以上成功 → 処理続行(P がある)
syncLater(failedServers); // 後で同期
return "成功";
} else {
return "失敗";
}
}
// 利点:1台が通信不可でも、システムは動く
実際の企業の例
Netflix(AP系)
// あなたが「お気に入り」に追加
await netflix.addFavorite(movieId);
// → すぐに成功(A: 可用性)
// でも他のデバイスでは...
const favorites = await netflix.getFavorites();
// → まだ反映されてない(C を諦めた)
// 数秒後に同期される
銀行システム(CP系)
// 引き出し操作
await bank.withdraw(5000);
// → 全サーバーと同期確認(C: 一貫性)
// もしネットワーク障害なら...
// → エラーになる(A を諦めた)
"システムメンテナンス中です"
AWS(Pあり)
AWSはAvailability Zone(AZ)の概念で分断耐性を実現しています。
[東京リージョン]
AZ-A: データセンター1
AZ-B: データセンター2
AZ-C: データセンター3
1つのAZがダウンしても、
他のAZでサービス継続
→ これがPartition tolerance
まとめ
CAP定理の本質
- C(一貫性): 全サーバーで同じデータが見える
- A(可用性): 必ず応答が返ってくる
- P(分断耐性): 通信障害でも動き続ける