LabHub

ブログ

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

한국어English日本語中文

はじめに — 分散システムが難しい本当の理由

Butler Lampsonの有名な定義:

「分散システムとは、存在すら知らなかったコンピュータの故障が、自分のコンピュータを使えなくするものである。」

分散システムが難しいのは、次の3つが同時に起こるからだ:

  1. 時計が違う (Clock Drift)
  2. 故障が部分的である (Partial Failure)
  3. ネットワークは信用してはいけない (Unreliable Network)

本稿はこの3つの問題に対する理論的な道具(時計・合意・一貫性)と実戦のパターン(Event Sourcing・Saga・CRDT)を一度に整理する。


第1部 — 8 Fallacies of Distributed Computing

Peter Deutschが1994年にまとめた分散システムの8つの誤った仮定:

  1. ネットワークは信頼できる
  2. 遅延はゼロである
  3. 帯域は無限である
  4. ネットワークは安全である
  5. トポロジは変わらない
  6. 管理者は一人である
  7. 転送コストはゼロである
  8. ネットワークは均質である

この8つに反するすべての仮定は、いつか本番で爆発する。


第2部 — 時間の3つのモデル

2.1 Physical Clock (物理時計)

time.Now() — OSの時計。NTPで同期する。

問題:

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]

2.4 HLC (Hybrid Logical Clock, 2014)

物理時間 + 論理カウンタ の組み合わせ:

HLC = (physical_time, logical_counter)

2.5 TrueTime (Google Spanner)

GPS・原子時計で ±7ms を保証 → 「この時点より後のイベントは確実に後ろ」が可能になる。


第3部 — 一貫性モデルのスペクトラム

3.1 主要な一貫性モデル

強い → 弱い順:

  1. Linearizable: すべての操作が実時間の順序どおりに見える
  2. Sequential: すべてのプロセスが同じ順序で見るが、実時間ではない
  3. Causal: 因果関係のあるものだけ順序が保証される
  4. Eventual: 結局は同じになる

3.2 CAPの再訪

ネットワーク分断は避けられない → CPかAPかを選ぶ。

3.3 PACELC (より現実的)

ほとんどのシステムはPA/EL (分断時は可用性 + 正常時は低遅延)。

3.4 2025のDB一貫性の選択

DBモデル
PostgreSQL単一ノードのSerializable
MySQLRead Committed (デフォルト)
SpannerLinearizable + External Consistency
CockroachDBSerializable + Causal
CassandraTunable (Quorum・Eventual)
DynamoDBEventual (オプションで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が作った元祖。正確だが理解も実装も難しいことで悪名高い。

役割:

問題: 実戦では使いにくく、Multi-Paxos、Fast Paxos、EPaxosなどの変種が生まれた。

4.4 Raft (2014)

Stanfordで「理解可能なConsensus」を目標に設計された。現在は事実上の標準。

3つの核心:

  1. Leader Election: リーダーを一人選ぶ
  2. Log Replication: リーダーがFollowerにログを複製する
  3. Safety: Committedなログは覆らない

状態:

使用先: 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つ

  1. Single-Leader: 書き込みはリーダー、読み取りはどこでも。単純。ほとんどのRDBMS。
  2. Multi-Leader: 複数のリーダーが書き込みを受ける。衝突解決が必要。まれにしか使わない。
  3. Leaderless: すべてのノードが対等。Dynamo-style。Cassandra, Riak, DynamoDB。
  4. Quorum-based: R + W > N なら強い一貫性。

5.2 Replication Lag

Leader → Followerの伝播遅延が次の問題を生む:

解決: Leaderから読む、Sticky Session、Timestampベースの一貫性。

5.3 Conflict Resolution (Multi-Leader/Leaderless)


第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 実戦での使用先

6.5 限界


第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

メリット:

デメリット:

7.2 CQRS (Command Query Responsibility Segregation)

書き込みモデル(Command) ≠ 読み取りモデル(Query) の分離。

Write: events → EventStore
Projection
Read: Materialized View (最適化)

メリット: 読み取りと書き込みを独立に最適化・スケールできる。 デメリット: 一貫性の遅れ (Eventual)。

7.3 Event Sourcing + CQRSのスタック

7.4 いつ使い、いつ避けるか

良いとき:

避けるとき:


第8部 — Sagaパターン

8.1 分散トランザクションの問題

2PC (Two-Phase Commit): 完璧だがblockし、Coordinatorの失敗に弱い。現代ではほとんど使われない。

代替: Saga — ローカルトランザクションの連続 + 失敗時は補償トランザクション。

8.2 Choreography (振り付け)

各サービスがイベントを発行 + 購読する:

OrderCreatedInventoryReservedPaymentChargedOrderConfirmed
                  ↓ 失敗
              InventoryReleasedOrderCancelled (補償)

メリット: サービス間の結合度が低い。 デメリット: 流れの追跡が難しい。

8.3 Orchestration (指揮)

中央のOrchestratorが明示的に流れを管理する:

Orchestrator:
  1. ReserveInventory
  2. ChargePayment
  3. ShipOrder
  失敗時 ← CompensateAll

メリット: 流れが明示的で、デバッグしやすい。 デメリット: Orchestratorが単一障害点になりうる。

8.4 実装ツール

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 障害の伝播パターン

  1. Cascading Failure: あるサービスが遅くなる → 呼び出し側のタイムアウトが積み上がる → 伝播する
  2. Thundering Herd: キャッシュ失効時にすべてのリクエストがOriginへ殺到する
  3. Retry Storm: 失敗したリクエストがリトライで増幅する
  4. Metastable Failure: 一時的な問題が正常復帰の後も持続する
  5. Split-Brain: ネットワーク分断で二人のリーダーができる
  6. Clock Skew Bug: 時計のずれでタイムスタンプが逆転する
  7. Gray Failure: 死んではいないが機能が劣化している
  8. Poison Pill: 特定のメッセージがすべてのConsumerを落とす
  9. Queue Backlog: Consumerが遅く、無限に積み上がる
  10. Hot Partition: 特定のシャードにトラフィックが集中する
  11. Fan-out Overload: 1件のリクエストが100件の内部リクエストになる
  12. Noisy Neighbor: 共有資源で1テナントが全体に影響する

9.2 防御パターン

障害防御
CascadingCircuit Breaker, Bulkhead, Timeout
Thundering HerdRequest Coalescing, Jittered refresh
Retry StormExponential Backoff + Jitter
MetastableLoad Shedding, Queue Depthの制限
Split-BrainFencing Token, Quorum
Clock SkewHLC, NTPの監視
Gray FailureHealth Check + 積極的な切り離し
Poison PillDead Letter Queue + Alert
Queue BacklogBackpressure, Auto-scale
Hot PartitionConsistent Hashing + replication
Fan-outTimeoutの深さを考慮、Async
Noisy NeighborResource Quota, QoS

9.3 Chaos Engineering

意図的に障害を注入する → システムを検証する。

原則:


第10部 — 分散システムのパターン10

  1. Idempotency: 同じリクエストをN回 = 1回の効果
  2. Exactly-once is a lie: At-least-once + Idempotent Consumer
  3. Outbox Pattern: DBトランザクションとイベント発行の原子性
  4. Inbox Pattern: 冪等なConsumerの実装方法
  5. Circuit Breaker: 失敗の検知 + 回避
  6. Bulkhead: スレッドプールの隔離で連鎖失敗を防ぐ
  7. Leader Election: Leaseベース
  8. Sharding: データの分割
  9. Sidecar: 共通機能 (ロギング、セキュリティ) を別コンテナに
  10. Service Mesh: Sidecarをネットワークレイヤへ

第11部 — 分散システムのロードマップ6か月

Month 1: 理論の基礎

Month 2: Consensusの実習

Month 3: Replication + Consistency

Month 4: Event Sourcing + Saga

Month 5: CRDT + リアルタイム

Month 6: 障害 + 運用


第12部 — 分散システムチェックリスト12

  1. 8 Fallacies を言える
  2. Lamport vs Vector vs HLC の違いを知っている
  3. CAP vs PACELC の違いを知っている
  4. Raftの3つの核心 を説明できる
  5. FLP Impossibility の意味を知っている
  6. Single-Leader vs Leaderless のトレードオフを知っている
  7. CRDTが解決する問題 を知っている
  8. Event Sourcing + CQRS の長所と短所を知っている
  9. Saga Choreography vs Orchestration の違いを知っている
  10. Cascading Failureの防御3つ を言える
  11. Outbox・Inboxパターン の役割を知っている
  12. Chaos Engineeringの原則 を知っている

第13部 — 分散システムのアンチパターン10

  1. 「ネットワークは信頼できる」: 8 Fallaciesの1番。Timeout・リトライは必須
  2. 2PCをマイクロサービスに: Blockの危険。Sagaで
  3. イベント順序をKafkaのパーティション間で仮定する: パーティション単位でしか保証されない
  4. Exactly-onceをDBトランザクションなしで: 不可能。Outboxまたは冪等性で
  5. Clockを信頼する: time.Now() で並べる → バグ。論理時計が必須
  6. 無限リトライ: Retry Stormを誘発する。Backoff + Circuit Breaker
  7. 単一ノードDB + 複製だけで安全だと仮定: Split-Brainの考慮が必須
  8. Consumer失敗時にNACKせずブロック: DLQの設計が必須
  9. グローバルトランザクション: 性能・可用性 ↓。境界を再設計する
  10. 「Eventual Consistencyは結局同じになる」: ユーザー視点では問題。UXで解決する

おわりに — 分散システムは「心のモデル」だ

分散システムは直観が最も頻繁に裏切る領域だ。「直観が強力な」エンジニアが最も危ない。

必要なもの:

2025年のシニアエンジニアの分散の力量は年収を2倍に広げる軸だ。この領域はそれだけ難しく、だからこそそれだけ価値がある。


次回予告 — 「DB完全ガイド: 内部構造・インデックス・クエリプランナ・パーティショニング・Vector DB」

Season 2 Ep 13はデータの心臓、DB深掘り。次回は:

DBがどうやってクエリを処理するのかを、次回に。

コメント

まだコメントはありません。

ログインするとコメントできます