はじめに
「CAP定理?3つから2つ選ぶやつでしょ」
これは半分正解で、半分間違いだ。
CAP定理は単なる「2択」ではない。 分散システムを設計するときの本質的なトレードオフを示している。
- なぜ3つを同時に満たせないのか
- 「ネットワーク分断」は避けられるのか
- 実際のデータベースはどう選択しているのか
この記事では、CAP定理の本質から、実務での設計判断までを解説する。
CAP定理とは
3つの特性
| 特性 | 英語 | 意味 |
|---|---|---|
| C | Consistency | 一貫性:全ノードが同じデータを返す |
| A | Availability | 可用性:リクエストに常に応答を返す |
| P | Partition Tolerance | 分断耐性:ネットワーク分断でも動作する |
CAP定理の主張
分散システムにおいて、C・A・Pの3つを同時に満たすことはできない。 最大で2つまでしか保証できない。
graph TD
CAP["CAP定理の3要素"]
C["Consistency<br/>一貫性"]
A["Availability<br/>可用性"]
P["Partition Tolerance<br/>分断耐性"]
CAP --> C
CAP --> A
CAP --> P
style CAP fill:#f0f0f0,stroke:#333,stroke-width:2px
style C fill:#e3f2fd
style A fill:#e8f5e9
style P fill:#fff3e0
よくある誤解
誤解: 「3つから2つを選ぶ」
現実: ネットワーク分断(P)は避けられない。 だから実質的には「分断が起きたとき、CとAのどちらを優先するか」という選択。
なぜ3つを同時に満たせないのか
シナリオ: ネットワーク分断が起きた
graph TB
subgraph Partition["ネットワーク分断"]
NodeA["Node A<br/>Data: 1"]
NodeB["Node B<br/>Data: 1"]
Client1["Client 1"]
Client2["Client 2"]
Client1 -.->|接続| NodeA
Client2 -.->|接続| NodeB
NodeA -.->|✗ 分断| NodeB
end
style Partition fill:#ffebee,stroke:#d32f2f
style NodeA fill:#e3f2fd
style NodeB fill:#e3f2fd
style Client1 fill:#fff3e0
style Client2 fill:#fff3e0
Client 1 が Node A に書き込み
sequenceDiagram
participant C1 as Client 1
participant NA as Node A
participant NB as Node B
C1->>NA: WRITE data = 2
NA->>NA: Data = 2
NA--xNB: ✗ 分断により同期できない
Note over NB: Data = 1 (古いまま)
Client 2 が Node B から読み取りしようとする
ここで選択が必要:
選択1: 一貫性(C)を優先
sequenceDiagram
participant C2 as Client 2
participant NB as Node B
participant NA as Node A
C2->>NB: READ data
NB--xNA: ✗ 同期できない
NB-->>C2: ERROR (利用不可)
Note over C2,NB: 一貫性は保たれる<br/>不正確なデータは返さない<br/>可用性を失う
結果: 一貫性は保たれる(不正確なデータを返さない)が、可用性を失う
選択2: 可用性(A)を優先
sequenceDiagram
participant C2 as Client 2
participant NB as Node B
participant NA as Node A
C2->>NB: READ data
NB--xNA: ✗ 同期できない
NB-->>C2: data = 1 (古いデータ)
Note over C2,NB: 可用性は保たれる<br/>応答は返す<br/>一貫性を失う
結果: 可用性は保たれる(応答は返す)が、一貫性を失う
これがCAP定理の本質
graph TB
partition["ネットワーク分断(P)が起きたとき"]
choice{{"選択が必要"}}
subgraph cp["C を優先"]
c_take["✓ 一貫性を保証"]
a_drop["✗ 可用性を捨てる<br/>(応答しない)"]
end
subgraph ap["A を優先"]
a_take["✓ 可用性を保証"]
c_drop["✗ 一貫性を捨てる<br/>(古いデータを返す)"]
end
partition --> choice
choice -->|"CP"| cp
choice -->|"AP"| ap
note["⚠️ 両方同時には無理"]
style partition fill:#ffebee,stroke:#d32f2f,stroke-width:3px
style choice fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style cp fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style ap fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style c_take fill:#c8e6c9
style a_drop fill:#ffcdd2
style a_take fill:#c8e6c9
style c_drop fill:#ffcdd2
style note fill:#fff9c4,stroke:#f57f17,stroke-width:2px
CP、AP、CAの分類
CP(一貫性 + 分断耐性)
graph LR
subgraph CP["CP: 一貫性 + 分断耐性"]
direction TB
action["分断発生時の動作"]
c["✓ 一貫性を保証"]
a["✗ 可用性を犠牲<br/>(応答しない)"]
action --> c
action --> a
end
style CP fill:#e3f2fd,stroke:#1976d2,stroke-width:3px
style action fill:#fff3e0,stroke:#f57c00
style c fill:#c8e6c9,stroke:#4caf50,stroke-width:2px
style a fill:#ffcdd2,stroke:#f44336,stroke-width:2px
代表的なシステム:
- HBase: 強い一貫性、分断時は一部が利用不可
- MongoDB: デフォルトはCP(writeConcern: majority)
- Redis Cluster: 分断時、マイノリティ側は書き込み不可
- Zookeeper: 一貫性重視、過半数のノードが必要
ユースケース:
- 金融取引
- 在庫管理
- 予約システム
- 一貫性が絶対に必要なシステム
AP(可用性 + 分断耐性)
graph LR
subgraph AP["AP: 可用性 + 分断耐性"]
direction TB
action["分断発生時の動作"]
a["✓ 可用性を保証<br/>(常に応答)"]
c["✗ 一貫性を犠牲<br/>(古いデータの可能性)"]
action --> a
action --> c
end
style AP fill:#e8f5e9,stroke:#4caf50,stroke-width:3px
style action fill:#fff3e0,stroke:#f57c00
style a fill:#c8e6c9,stroke:#4caf50,stroke-width:2px
style c fill:#ffcdd2,stroke:#f44336,stroke-width:2px
代表的なシステム:
- Cassandra: 結果整合性、常に書き込み可能
- DynamoDB: 結果整合性モード
- CouchDB: 結果整合性、コンフリクト解決
- Riak: 結果整合性
ユースケース:
- SNSのタイムライン
- ショッピングカート
- センサーデータ収集
- 可用性が最優先のシステム
CA(一貫性 + 可用性)
graph LR
subgraph CA["CA: 一貫性 + 可用性"]
direction TB
premise["前提条件"]
p["✗ 分断耐性なし<br/>(分断が起きない前提)"]
reality["実質的に"]
single["単一ノード or<br/>同一ネットワーク内"]
premise --> p
p --> reality
reality --> single
end
style CA fill:#fff3e0,stroke:#f57c00,stroke-width:3px
style premise fill:#e3f2fd,stroke:#1976d2
style p fill:#ffcdd2,stroke:#f44336,stroke-width:2px
style reality fill:#f5f5f5,stroke:#666
style single fill:#fff9c4,stroke:#f57f17
代表的なシステム:
- 単一ノードのRDBMS: PostgreSQL、MySQL(レプリケーションなし)
- 同一DC内のクラスタ: 分断が起きにくい環境
現実: 分散システムでは分断は避けられないため、純粋なCAは存在しない
まとめ
| 分類 | 分断時の動作 | 代表例 |
|---|---|---|
| CP | 可用性を犠牲 | HBase, MongoDB, Zookeeper |
| AP | 一貫性を犠牲 | Cassandra, DynamoDB, CouchDB |
| CA | 分断は起きない前提 | 単一ノードRDBMS |
一貫性のレベル
CAP定理の「C」は強い一貫性を指すが、実際にはいくつかのレベルがある。
強い一貫性(Strong Consistency)
sequenceDiagram
participant C as Client
participant N1 as Node 1
participant N2 as Node 2
C->>N1: Write: data = 2
N1->>N2: 同期レプリケーション
N2-->>N1: ACK
N1-->>C: 完了
C->>N2: Read
N2-->>C: data = 2 ✓
Note over C,N2: 全ての読み取りが<br/>最新の書き込みを返す
実現方法: 同期レプリケーション、2フェーズコミット
結果整合性(Eventual Consistency)
sequenceDiagram
participant C as Client
participant N1 as Node 1
participant N2 as Node 2
C->>N1: Write: data = 2
N1-->>C: 即座に完了
Note over N1,N2: 非同期でレプリケーション
N1-->>N2: バックグラウンド同期
C->>N2: Read (すぐ)
N2-->>C: data = 1 (古いかも)
Note over N2: 少し待つ...
N2->>N2: 同期完了
C->>N2: Read (後で)
N2-->>C: data = 2 ✓
Note over C,N2: 時間が経てば<br/>全ノードが同じデータになる
実現方法: 非同期レプリケーション、バックグラウンド同期
その他のレベル
| レベル | 説明 |
|---|---|
| Causal Consistency | 因果関係のある操作の順序を保証 |
| Read Your Writes | 自分の書き込みは必ず読める |
| Monotonic Reads | 一度読んだ値より古い値は読まない |
| Session Consistency | セッション内での一貫性を保証 |
実務での選択
flowchart TD
Q1["絶対に最新データが必要?"]
Q2["どのくらいの遅延は許容?"]
A1["強い一貫性<br/>性能トレードオフ"]
A2["結果整合性"]
A3["Read Your Writes"]
Q1 -->|"Yes"| A1
Q1 -->|"No"| Q2
Q2 -->|"数秒OK"| A2
Q2 -->|"自分の書き込みは見たい"| A3
style Q1 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style Q2 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style A1 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style A2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style A3 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
実際のデータベースの選択
MongoDB
// CP: 強い一貫性(デフォルト)
db.collection.insertOne(
{ name: "test" },
{ writeConcern: { w: "majority" } }
);
// AP寄り: 結果整合性(ローカル書き込み)
db.collection.insertOne(
{ name: "test" },
{ writeConcern: { w: 1 } }
);
// 読み取りも設定可能
db.collection.find().readConcern("majority"); // 強い一貫性
db.collection.find().readConcern("local"); // 結果整合性
Cassandra
-- AP: 結果整合性(デフォルト)
CREATE KEYSPACE myapp WITH replication = {
'class': 'SimpleStrategy',
'replication_factor': 3
};
-- 一貫性レベルの指定
CONSISTENCY QUORUM; -- 過半数が応答すればOK(CP寄り)
CONSISTENCY ONE; -- 1ノードでOK(AP寄り)
CONSISTENCY ALL; -- 全ノード(強い一貫性、可用性低下)
Cassandraの一貫性レベル:
| レベル | 説明 | 特性 |
|---|---|---|
| ONE | 1ノードでOK | 最も高い可用性、低い一貫性 |
| QUORUM | 過半数 | バランス型 |
| ALL | 全ノード | 最も高い一貫性、低い可用性 |
| LOCAL_QUORUM | DC内の過半数 | DC障害に強い |
DynamoDB
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('MyTable')
# 結果整合性(デフォルト、安い)
response = table.get_item(Key={'id': '123'})
# 強い一貫性(高い、レイテンシ増加)
response = table.get_item(
Key={'id': '123'},
ConsistentRead=True
)
PostgreSQL(同期レプリケーション)
-- postgresql.conf
synchronous_commit = on
synchronous_standby_names = 'replica1'
-- CP: 同期レプリケーションで強い一貫性
-- Replica に反映されるまで COMMIT が完了しない
PACELC定理
CAP定理の拡張。より現実的なトレードオフを示す。
PACELC の意味
P: Partition(分断時)
A: Availability
C: Consistency
E: Else(通常時)
L: Latency
C: Consistency
読み方:
- 分断時は A か C を選ぶ(CAP定理)
- 通常時も L(レイテンシ)か C を選ぶ必要がある
なぜ通常時もトレードオフがあるのか
graph TB
subgraph strong["強い一貫性"]
w1["Write"]
sync1["全ノードに同期"]
slow["遅い<br/>(レイテンシ増加)"]
w1 --> sync1
sync1 --> slow
end
subgraph eventual["結果整合性"]
w2["Write"]
local["ローカルに書いて<br/>即応答"]
fast["速い<br/>(低レイテンシ)"]
bg["バックグラウンドで同期"]
w2 --> local
local --> fast
local -.-> bg
end
style strong fill:#e3f2fd,stroke:#1976d2,stroke-width:3px
style eventual fill:#e8f5e9,stroke:#4caf50,stroke-width:3px
style slow fill:#ffcdd2,stroke:#f44336
style fast fill:#c8e6c9,stroke:#2e7d32
データベースの分類(PACELC)
| システム | 分断時 (PAC) | 通常時 (ELC) |
|---|---|---|
| Cassandra | AP | EL(低レイテンシ) |
| DynamoDB | AP | EL |
| MongoDB | CP | EC(一貫性) |
| PostgreSQL | CP | EC |
| PNUTS (Yahoo) | AP | EC |
実務での意味
flowchart TD
Q1["常に高速レスポンスが必要?"]
Q2["一貫性が重要?"]
A1["EL<br/>結果整合性、低レイテンシ<br/>Cassandra, DynamoDB"]
A2["EC<br/>強い一貫性<br/>MongoDB, PostgreSQL"]
A3["要件に応じて選択"]
Q1 -->|"Yes"| A1
Q1 -->|"No"| Q2
Q2 -->|"Yes"| A2
Q2 -->|"No"| A3
style Q1 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style Q2 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
style A1 fill:#e1f5ff,stroke:#2196f3,stroke-width:2px
style A2 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style A3 fill:#fff9c4,stroke:#f57f17,stroke-width:2px
実務での設計判断
ユースケース別の選択
1. 金融取引システム
要件:
- 残高の一貫性が絶対
- 多少の遅延は許容
- 分断時はエラーでも構わない
選択: CP + EC
例: PostgreSQL(同期レプリケーション)
2. SNSのタイムライン
要件:
- 常に表示できることが重要
- 最新じゃなくてもOK(数秒遅れは許容)
- 高スループット
選択: AP + EL
例: Cassandra
3. ECサイトの商品カタログ
要件:
- 常に表示できること
- 価格は最新じゃないと困る(決済時に再確認)
- 在庫は結果整合性でも許容(カートに入れた後で確認)
選択: AP + EL(閲覧)、CP(決済)
例: DynamoDB(閲覧)+ RDS(決済)
4. IoTセンサーデータ
要件:
- 大量のデータを書き込み
- 多少の欠損は許容
- リアルタイム分析
選択: AP + EL
例: Cassandra, InfluxDB
ハイブリッドアプローチ
graph TB
subgraph App["Application"]
ReadPath["読み取りパス<br/>(高速)"]
WritePath["書き込みパス<br/>(一貫性)"]
Redis["Redis<br/>(キャッシュ)<br/>AP"]
PostgreSQL["PostgreSQL<br/>(マスター)<br/>CP"]
ReadPath --> Redis
WritePath --> PostgreSQL
PostgreSQL -.->|同期| Redis
end
style App fill:#f5f5f5,stroke:#333
style ReadPath fill:#e8f5e9
style WritePath fill:#e3f2fd
style Redis fill:#fff3e0
style PostgreSQL fill:#e1f5ff
class HybridDataStore:
def __init__(self):
self.cache = Redis() # AP: 高速な読み取り
self.master = PostgreSQL() # CP: 一貫性のある書き込み
def read(self, key):
# まずキャッシュから
value = self.cache.get(key)
if value:
return value
# なければマスターから
value = self.master.query(key)
self.cache.set(key, value, ttl=60)
return value
def write(self, key, value):
# マスターに書き込み(一貫性保証)
self.master.write(key, value)
# キャッシュを更新
self.cache.set(key, value)
分断への対処パターン
1. フェイルオーバー(CP)
graph TB
subgraph Before["分断発生"]
Primary1["Primary<br/>(Active)"]
Replica1["Replica<br/>(Standby)"]
Primary1 -.-> Replica1
end
subgraph After["分断検知後"]
Primary2["Primary<br/>(停止)"]
Replica2["Replica<br/>→ Primary"]
Client["クライアントは<br/>新Primaryに接続"]
Client --> Replica2
end
Before --> After
style Before fill:#ffebee
style After fill:#e8f5e9
style Primary1 fill:#e3f2fd
style Replica1 fill:#fff3e0
style Primary2 fill:#ccc
style Replica2 fill:#4caf50,color:#fff
2. 読み取り専用モード
graph TB
subgraph Before["分断発生時"]
NodeA1["Node A<br/>(R/W)"]
NodeB1["Node B<br/>(R/W)"]
NodeA1 -.-> NodeB1
end
subgraph After["対応"]
NodeA2["Node A<br/>(R only)"]
NodeB2["Node B<br/>(R only)"]
Note["書き込みは拒否<br/>読み取りは許可<br/>(古いデータかも)"]
Note -.-> NodeA2
Note -.-> NodeB2
end
Before --> After
style Before fill:#ffebee
style After fill:#fff3e0
style NodeA1 fill:#e3f2fd
style NodeB1 fill:#e3f2fd
style NodeA2 fill:#ffcc80
style NodeB2 fill:#ffcc80
style Note fill:#f5f5f5
3. Quorum(過半数)
graph TB
subgraph Cluster["5ノードクラスタ"]
subgraph Majority["3ノード(過半数)<br/>→ 処理を継続"]
N1["N1"]
N2["N2"]
N3["N3"]
end
subgraph Minority["2ノード(少数派)<br/>→ 停止"]
N4["N4"]
N5["N5"]
end
Majority -.-> Minority
end
style Cluster fill:#f5f5f5
style Majority fill:#e8f5e9,stroke:#4caf50
style Minority fill:#ffebee,stroke:#f44336
style N1 fill:#4caf50,color:#fff
style N2 fill:#4caf50,color:#fff
style N3 fill:#4caf50,color:#fff
style N4 fill:#f44336,color:#fff
style N5 fill:#f44336,color:#fff
# Quorum の計算
def can_process(available_nodes, total_nodes):
quorum = (total_nodes // 2) + 1
return available_nodes >= quorum
# 例: 5ノード中3ノードが利用可能
can_process(3, 5) # True: 過半数があるので処理継続
can_process(2, 5) # False: 過半数がないので停止
4. 結果整合性 + コンフリクト解決
graph TB
subgraph During["分断中の書き込み"]
NodeA["Node A<br/>Write: x = 1"]
NodeB["Node B<br/>Write: x = 2"]
NodeA -.-> NodeB
end
subgraph After["分断解消後"]
Conflict["⚠️ コンフリクト検出"]
LWW["Last Write Wins<br/>タイムスタンプが新しい方"]
VC["Vector Clocks<br/>因果関係で判断"]
App["Application-level<br/>アプリケーションが決定"]
Conflict --> LWW
Conflict --> VC
Conflict --> App
end
During --> After
style During fill:#ffebee
style After fill:#fff3e0
style NodeA fill:#e3f2fd
style NodeB fill:#e3f2fd
style Conflict fill:#ff9800,color:#fff
style LWW fill:#e8f5e9
style VC fill:#e1f5ff
style App fill:#f3e5f5
監視と運用
監視すべきメトリクス
| メトリクス | 説明 | 意味 |
|---|---|---|
| Replication Lag | レプリカの遅延 | 一貫性の度合い |
| Network Latency | ノード間の遅延 | 分断の予兆 |
| Node Health | ノードの状態 | 可用性 |
| Quorum Status | 過半数の状態 | 処理可能かどうか |
アラート例
groups:
- name: cap_related
rules:
- alert: HighReplicationLag
expr: replication_lag_seconds > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Replication lag is high - eventual consistency window is large"
- alert: NetworkPartitionDetected
expr: node_connectivity < 0.5
for: 1m
labels:
severity: critical
annotations:
summary: "Network partition detected - CAP tradeoff in effect"
- alert: QuorumLost
expr: available_nodes < (total_nodes / 2) + 1
for: 30s
labels:
severity: critical
annotations:
summary: "Quorum lost - system may become unavailable"
実務チェックリスト
設計時
- データの一貫性要件は明確か
- 可用性の要件は明確か
- 分断時の動作は決まっているか
- 結果整合性は許容できるか
実装時
- 一貫性レベルは適切に設定されているか
- フェイルオーバーの仕組みはあるか
- コンフリクト解決の戦略はあるか
運用時
- レプリケーション遅延は監視されているか
- ネットワーク分断は検知できるか
- 分断時の手順は文書化されているか
まとめ
CAP定理の本質は、分散システムにおける避けられないトレードオフだ。
重要なポイント
-
分断(P)は避けられない
- 実質的には「分断時に C か A を選ぶ」
- 「3つから2つ選ぶ」は誤解を招く表現
-
一貫性にはレベルがある
- 強い一貫性と結果整合性の間にも選択肢がある
- 要件に応じて適切なレベルを選ぶ
-
PACELC も考慮
- 通常時もレイテンシと一貫性のトレードオフがある
- 「常に高速」か「常に一貫」か
設計の指針
| 要件 | 選択 | 例 |
|---|---|---|
| 一貫性が絶対 | CP + EC | 金融、予約 |
| 可用性が絶対 | AP + EL | SNS、IoT |
| バランス | 用途に応じて使い分け | ECサイト |
CAP定理を理解することは、分散システムを設計する上での基礎教養だ。
「何を捨てるか」を意識的に選択できるようになれば、より良いシステム設計ができる。
関連記事
- データベースレプリケーション完全ガイド — CAP定理が適用されるレプリケーション戦略の詳細
- データベースシャーディング・パーティショニングガイド — 分散データベース設計のもう一つの軸
- メッセージキュー実践ガイド — 結果整合性を活用した非同期処理パターン