- Published on
分散システム完全ガイド — 時計・Consensus・Event Sourcing・Saga・CRDT・障害パターン (Season 2 Ep 12, 2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
はじめに — 分散システムが難しい本当の理由
Butler Lampsonの有名な定義:
「分散システムとは、存在すら知らなかったコンピュータの故障が、自分のコンピュータを使えなくするものである。」
分散システムが難しいのは、次の3つが同時に起こるからだ:
- 時計が違う (Clock Drift)
- 故障が部分的である (Partial Failure)
- ネットワークは信用してはいけない (Unreliable Network)
本稿はこの3つの問題に対する理論的な道具(時計・合意・一貫性)と実戦のパターン(Event Sourcing・Saga・CRDT)を一度に整理する。
第1部 — 8 Fallacies of Distributed Computing
Peter Deutschが1994年にまとめた分散システムの8つの誤った仮定:
- ネットワークは信頼できる ❌
- 遅延はゼロである ❌
- 帯域は無限である ❌
- ネットワークは安全である ❌
- トポロジは変わらない ❌
- 管理者は一人である ❌
- 転送コストはゼロである ❌
- ネットワークは均質である ❌
この8つに反するすべての仮定は、いつか本番で爆発する。
第2部 — 時間の3つのモデル
2.1 Physical Clock (物理時計)
time.Now() — OSの時計。NTPで同期する。
問題:
- Clock Drift (ppm単位の誤差)
- NTPのジャンプ (時刻調整で飛び越える)
- Leap Second
- データセンタ間でms〜数十msの誤差
2.2 Logical Clock (論理時計)
Lamport Timestamp (1978):
イベント発生時に counter++
メッセージ送信: (event, counter) を送る
メッセージ受信: counter = max(local, received) + 1
メリット: 因果関係の保存 (happens-before) デメリット: 因果関係を持っていたのかどうかを区別できない (concurrent vs causal)
2.3 Vector Clock
ノード数Nと同じ長さのベクトル:
A: [2, 1, 0]
B: [1, 3, 0]
- AとBがconcurrentなのか、どちらが先なのかを判別できる
- サイズの問題: ノード数が多いとメタデータが大きくなる
2.4 HLC (Hybrid Logical Clock, 2014)
物理時間 + 論理カウンタ の組み合わせ:
HLC = (physical_time, logical_counter)
- 物理時計を基準にした整列 + 論理時計による因果の保存
- CockroachDB、YugabyteDBなど現代の分散DBの標準
2.5 TrueTime (Google Spanner)
GPS・原子時計で ±7ms を保証 → 「この時点より後のイベントは確実に後ろ」が可能になる。
- Commit Wait: 不確実性の区間(ms)だけ待ってから完了する
- 強い一貫性 + 性能 (Linearizable)
- 一般企業には実現不可能 (ハードウェア投資)
第3部 — 一貫性モデルのスペクトラム
3.1 主要な一貫性モデル
強い → 弱い順:
- Linearizable: すべての操作が実時間の順序どおりに見える
- Sequential: すべてのプロセスが同じ順序で見るが、実時間ではない
- Causal: 因果関係のあるものだけ順序が保証される
- Eventual: 結局は同じになる
3.2 CAPの再訪
- Consistency: Linearizability
- Availability: すべてのリクエストが答えを受け取る
- Partition Tolerance: ネットワーク分断時にも動作する
ネットワーク分断は避けられない → CPかAPかを選ぶ。
3.3 PACELC (より現実的)
- Partition: C or A
- Else (正常運用時): L(atency) or C(onsistency)
ほとんどのシステムはPA/EL (分断時は可用性 + 正常時は低遅延)。
3.4 2025のDB一貫性の選択
| DB | モデル |
|---|---|
| PostgreSQL | 単一ノードのSerializable |
| MySQL | Read Committed (デフォルト) |
| Spanner | Linearizable + External Consistency |
| CockroachDB | Serializable + Causal |
| Cassandra | Tunable (Quorum・Eventual) |
| DynamoDB | Eventual (オプションでStrong) |
| Redis Cluster | 単一キーはLinearizable、複数キーは ❌ |
第4部 — Consensus: 合意アルゴリズム
4.1 なぜConsensusなのか
分散した複数のノードが同じ値に同意する — リーダー選出、状態複製、トランザクションのコミットなど。
4.2 FLP Impossibility (1985)
非同期システムにおいて、1ノードの故障すら許容するdeterministic consensusは不可能。
実際のシステムはタイミングの仮定 (eventually synchronous) または確率的な方法を使う。
4.3 Paxos (1989)
Leslie Lamportが作った元祖。正確だが理解も実装も難しいことで悪名高い。
役割:
- Proposer (値を提案)
- Acceptor (投票)
- Learner (決定を学習)
問題: 実戦では使いにくく、Multi-Paxos、Fast Paxos、EPaxosなどの変種が生まれた。
4.4 Raft (2014)
Stanfordで「理解可能なConsensus」を目標に設計された。現在は事実上の標準。
3つの核心:
- Leader Election: リーダーを一人選ぶ
- Log Replication: リーダーがFollowerにログを複製する
- Safety: Committedなログは覆らない
状態:
- Follower → Candidate (Election Timeout) → Leader (過半数の投票)
- Leaderがheartbeatに失敗したら → 新しい選出
使用先: etcd, Consul, TiKV, CockroachDB, Kafka KRaft (2024〜)。
4.5 PBFT (Practical Byzantine Fault Tolerance, 1999)
悪意あるノードが存在する状況で3f+1ノードのうちf個の故障を許容する。ブロックチェーンで使われる。
段階: Pre-prepare → Prepare → Commit。メッセージ複雑度 O(N²)。
4.6 Tendermint・HotStuff (2018〜)
BFTをブロックチェーン向けに現代化したもの。Libra/DiemのHotStuff、CosmosのTendermint。
4.7 Nakamoto Consensus
BitcoinのProof-of-Work。確率的な合意 — 十分に多くのブロックが後ろに積まれればconfirmed。
欠点: エネルギー、遅さ。だからEthereumはPoSへ移行した。
第5部 — Replication
5.1 複製パターン4つ
- Single-Leader: 書き込みはリーダー、読み取りはどこでも。単純。ほとんどのRDBMS。
- Multi-Leader: 複数のリーダーが書き込みを受ける。衝突解決が必要。まれにしか使わない。
- Leaderless: すべてのノードが対等。Dynamo-style。Cassandra, Riak, DynamoDB。
- Quorum-based: R + W > N なら強い一貫性。
5.2 Replication Lag
Leader → Followerの伝播遅延が次の問題を生む:
- Read-your-writes: たった今書いたものが読めない
- Monotonic reads: 以前に見えていたものが消える
- Consistent Prefix: 原因のない結果を見る
解決: Leaderから読む、Sticky Session、Timestampベースの一貫性。
5.3 Conflict Resolution (Multi-Leader/Leaderless)
- LWW (Last Writer Wins): 単純、データ損失の危険
- Version Vectors: 正確、複雑
- Application-level Merge: ユーザー定義 (CRDT)
第6部 — CRDT (Conflict-free Replicated Data Type)
6.1 CRDTとは
数学的に衝突が不可能なデータ構造。どの順序で合わせても同じ結果になる。
6.2 2つの型
State-based (CvRDT): 状態そのものを転送 + merge Operation-based (CmRDT): 演算を転送 + 再適用
6.3 代表的なCRDT
| CRDT | 用途 |
|---|---|
| G-Counter | 増加のみ (カウンタ) |
| PN-Counter | 増減 |
| G-Set | 追加のみ |
| OR-Set | 追加・削除 |
| LWW-Register | 最後の書き込みが勝つ |
| RGA | 順序のあるリスト (テキスト編集) |
| JSON CRDT (Automerge) | 入れ子構造 |
6.4 実戦での使用先
- Redis CRDT (Enterprise): Active-Active Geo
- Riak: Dynamo + CRDT
- Automerge (Ink & Switch): 協働エディタ
- Yjs: Real-time collaboration (Notion, Linearの着想元)
- Figma: デザイン協働のための独自CRDT
6.5 限界
- 任意の不変条件を表現しにくい (例: 一意性)
- メタデータのサイズ (Tombstone)
- 操作順序の意味を失う (これが利点でもあり欠点でもある)
第7部 — Event Sourcing + CQRS
7.1 Event Sourcing
状態の代わりにイベントを保存する。状態はイベントの再生から導出する。
events = [
AccountCreated(id=1, balance=0),
MoneyDeposited(account=1, amount=100),
MoneyWithdrawn(account=1, amount=30),
]
balance = fold(events, 0, apply) // 70
メリット:
- 完全な履歴 (監査・デバッグ)
- タイムトラベル
- イベントの再生で新しいViewを作れる
- 分散に向く (イベントはappend-only)
デメリット:
- 複雑度 ↑
- スキーマ進化の問題 (古いイベントの解釈)
- Snapshotがないと再生が遅い
7.2 CQRS (Command Query Responsibility Segregation)
書き込みモデル(Command) ≠ 読み取りモデル(Query) の分離。
Write: events → EventStore
↓
Projection
↓
Read: Materialized View (最適化)
メリット: 読み取りと書き込みを独立に最適化・スケールできる。 デメリット: 一貫性の遅れ (Eventual)。
7.3 Event Sourcing + CQRSのスタック
- EventStoreDB: 専用DB
- Kafka + Kafka Streams: イベントログ + Projection
- Axon Framework (Java): 統合型
- MartenDB (.NET): Postgresの上に実装
7.4 いつ使い、いつ避けるか
良いとき:
- 金融・取引のように監査可能性が重要なとき
- 複雑なビジネスロジック + 過去の再構成
- イベント中心のアーキテクチャ
避けるとき:
- 単純なCRUD
- チームの多くが初心者
- 一貫性の要求が単純
第8部 — Sagaパターン
8.1 分散トランザクションの問題
2PC (Two-Phase Commit): 完璧だがblockし、Coordinatorの失敗に弱い。現代ではほとんど使われない。
代替: Saga — ローカルトランザクションの連続 + 失敗時は補償トランザクション。
8.2 Choreography (振り付け)
各サービスがイベントを発行 + 購読する:
OrderCreated → InventoryReserved → PaymentCharged → OrderConfirmed
↓ 失敗
InventoryReleased → OrderCancelled (補償)
メリット: サービス間の結合度が低い。 デメリット: 流れの追跡が難しい。
8.3 Orchestration (指揮)
中央のOrchestratorが明示的に流れを管理する:
Orchestrator:
1. ReserveInventory
2. ChargePayment
3. ShipOrder
失敗時 ← CompensateAll
メリット: 流れが明示的で、デバッグしやすい。 デメリット: Orchestratorが単一障害点になりうる。
8.4 実装ツール
- Temporal: ワークフローエンジン。Sagaの事実上の標準。
- AWS Step Functions: マネージド
- Camunda: BPMNベース
- Cadence (Uber、Temporalの前身): オープンソース
8.5 Temporalの例
func ProcessOrder(ctx workflow.Context, order Order) error {
err := workflow.ExecuteActivity(ctx, ReserveInventory, order).Get(ctx, nil)
if err != nil { return err }
err = workflow.ExecuteActivity(ctx, ChargePayment, order).Get(ctx, nil)
if err != nil {
workflow.ExecuteActivity(ctx, ReleaseInventory, order)
return err
}
// ...
}
Temporalが再起動安全性・タイマー・可視化を無料で提供してくれる。
第9部 — 障害パターン12
9.1 障害の伝播パターン
- Cascading Failure: あるサービスが遅くなる → 呼び出し側のタイムアウトが積み上がる → 伝播する
- Thundering Herd: キャッシュ失効時にすべてのリクエストがOriginへ殺到する
- Retry Storm: 失敗したリクエストがリトライで増幅する
- Metastable Failure: 一時的な問題が正常復帰の後も持続する
- Split-Brain: ネットワーク分断で二人のリーダーができる
- Clock Skew Bug: 時計のずれでタイムスタンプが逆転する
- Gray Failure: 死んではいないが機能が劣化している
- Poison Pill: 特定のメッセージがすべてのConsumerを落とす
- Queue Backlog: Consumerが遅く、無限に積み上がる
- Hot Partition: 特定のシャードにトラフィックが集中する
- Fan-out Overload: 1件のリクエストが100件の内部リクエストになる
- Noisy Neighbor: 共有資源で1テナントが全体に影響する
9.2 防御パターン
| 障害 | 防御 |
|---|---|
| Cascading | Circuit Breaker, Bulkhead, Timeout |
| Thundering Herd | Request Coalescing, Jittered refresh |
| Retry Storm | Exponential Backoff + Jitter |
| Metastable | Load Shedding, Queue Depthの制限 |
| Split-Brain | Fencing Token, Quorum |
| Clock Skew | HLC, NTPの監視 |
| Gray Failure | Health Check + 積極的な切り離し |
| Poison Pill | Dead Letter Queue + Alert |
| Queue Backlog | Backpressure, Auto-scale |
| Hot Partition | Consistent Hashing + replication |
| Fan-out | Timeoutの深さを考慮、Async |
| Noisy Neighbor | Resource Quota, QoS |
9.3 Chaos Engineering
意図的に障害を注入する → システムを検証する。
- Chaos Monkey (Netflix 2011): インスタンスをランダムにkill
- Chaos Mesh: K8sネイティブ
- LitmusChaos: オープンソース
- Gremlin: 商用
原則:
- 本番で (最初はステージングで)
- Blast Radiusを制限する
- 自動ロールバック
- 定期的に実行する (Game Day)
第10部 — 分散システムのパターン10
- Idempotency: 同じリクエストをN回 = 1回の効果
- Exactly-once is a lie: At-least-once + Idempotent Consumer
- Outbox Pattern: DBトランザクションとイベント発行の原子性
- Inbox Pattern: 冪等なConsumerの実装方法
- Circuit Breaker: 失敗の検知 + 回避
- Bulkhead: スレッドプールの隔離で連鎖失敗を防ぐ
- Leader Election: Leaseベース
- Sharding: データの分割
- Sidecar: 共通機能 (ロギング、セキュリティ) を別コンテナに
- Service Mesh: Sidecarをネットワークレイヤへ
第11部 — 分散システムのロードマップ6か月
Month 1: 理論の基礎
- DDIA (Designing Data-Intensive Applications) を精読
- Lamport Paper 2編
- 8 Fallaciesの思考実験
Month 2: Consensusの実習
- Raftを自分で実装 (MIT 6.824)
- etcd・Consulの内部を読む
- Kafka KRaftの構造を理解する
Month 3: Replication + Consistency
- 一貫性モデルを区別する練習
- CockroachDB・Spannerの白書
- TrueTime・HLCの実装
Month 4: Event Sourcing + Saga
- ミニ銀行システムをEvent Sourcingで
- TemporalでSagaを実装
- Outboxパターンを実際に適用
Month 5: CRDT + リアルタイム
- Automerge・Yjsの学習
- 協働エディタのプロトタイプ
- Figmaの技術ブログを精読
Month 6: 障害 + 運用
- Chaos Monkeyの導入
- 主要な障害パターンの防御を実装
- Incident Postmortemの分析10件
第12部 — 分散システムチェックリスト12
- 8 Fallacies を言える
- Lamport vs Vector vs HLC の違いを知っている
- CAP vs PACELC の違いを知っている
- Raftの3つの核心 を説明できる
- FLP Impossibility の意味を知っている
- Single-Leader vs Leaderless のトレードオフを知っている
- CRDTが解決する問題 を知っている
- Event Sourcing + CQRS の長所と短所を知っている
- Saga Choreography vs Orchestration の違いを知っている
- Cascading Failureの防御3つ を言える
- Outbox・Inboxパターン の役割を知っている
- Chaos Engineeringの原則 を知っている
第13部 — 分散システムのアンチパターン10
- 「ネットワークは信頼できる」: 8 Fallaciesの1番。Timeout・リトライは必須
- 2PCをマイクロサービスに: Blockの危険。Sagaで
- イベント順序をKafkaのパーティション間で仮定する: パーティション単位でしか保証されない
- Exactly-onceをDBトランザクションなしで: 不可能。Outboxまたは冪等性で
- Clockを信頼する:
time.Now()で並べる → バグ。論理時計が必須 - 無限リトライ: Retry Stormを誘発する。Backoff + Circuit Breaker
- 単一ノードDB + 複製だけで安全だと仮定: Split-Brainの考慮が必須
- Consumer失敗時にNACKせずブロック: DLQの設計が必須
- グローバルトランザクション: 性能・可用性 ↓。境界を再設計する
- 「Eventual Consistencyは結局同じになる」: ユーザー視点では問題。UXで解決する
おわりに — 分散システムは「心のモデル」だ
分散システムは直観が最も頻繁に裏切る領域だ。「直観が強力な」エンジニアが最も危ない。
必要なもの:
- 疑う心: ネットワーク・時計・ノードのすべてを疑う
- 補償する心: 原子性が無理なら補償を設計する
- 再現する心: イベントとして記録する → いつでも再現できる
2025年のシニアエンジニアの分散の力量は年収を2倍に広げる軸だ。この領域はそれだけ難しく、だからこそそれだけ価値がある。
次回予告 — 「DB完全ガイド: 内部構造・インデックス・クエリプランナ・パーティショニング・Vector DB」
Season 2 Ep 13はデータの心臓、DB深掘り。次回は:
- B-Tree・LSM-Tree・Hash Indexの内部構造
- クエリプランナとEXPLAINの深い読み方
- トランザクション分離レベル (Read Uncommitted 〜 Serializable)
- シャーディング・パーティショニング戦略
- PostgreSQLの独走 (2025)
- Vector DBとpgvector・Qdrant・Weaviate
DBがどうやってクエリを処理するのかを、次回に。