🕰️
概念 #データ設計 #分散システム #ベクタークロック #因果一貫性 #Lamportクロック #DDIA 📚 データ志向アプリケーション設計(DDIA)

ベクタークロックと因果順序

分散システムで「どちらが先か」を判断する方法。Lamportクロックからベクタークロックへの発展と、バージョンベクターによる競合検出を理解する

定義

ベクタークロック(Vector Clock):分散システムで複数のノードが生成するイベントの「因果的な順序関係」を追跡するアルゴリズム。物理的な時刻を使わず、論理的な「どちらが先か」を判断できる。

問題:物理クロックは使えない

[分散システムの問題](./concepts_ddia_distributed_problems.md)で述べた通り:

ノードA: 10:00:00.000 → イベント発生
ノードB: 10:00:00.003(Aより3ms進んでいる)→ イベント発生

タイムスタンプだけではAとBのどちらが「先」かを正確に判断できない
NTPの誤差が原因で順序が逆転することがある

Lamportクロック(論理クロック)

Leslie Lamport(1978)が提案した最もシンプルな論理クロック。

ルール

各ノードはカウンターtを持つ

1. イベント発生時: t = t + 1
2. メッセージ送信時: メッセージにtを付与
3. メッセージ受信時: t = max(自分のt, 受信したt) + 1

ノードA (t=0)    ノードB (t=0)    ノードC (t=0)
    │                │                │
t=1 ● a1            │                │
    │                │                │
    │────[t=1]──→   ● b1 (t=2)       │
    │                │                │
t=2 ● a2            │────[t=2]──→   ● c1 (t=3)
    │                │                │

限界

a1 → b1(a1が因果的にb1より前)の場合:
  a1のt=1 < b1のt=2 → ✅ 因果順序と一致

しかし逆は成立しない:
  a2のt=2 = b1のt=2 であっても、a2とb1に因果関係はない
  
「t(a) < t(b) ならa→b(aはbの前)」は正しい
「t(a) < t(b) ならa→b」の逆(t(a) >= t(b) ならb→a)は成立しない

Lamportクロックは「前後関係の確認」はできるが「同時性の検出」はできない。

ベクタークロック

各ノードが「全ノードのカウンター配列」を持つことで、Lamportクロックの限界を解決。

ルール

Nノードのシステムで、各ノードiはV[i]というベクターを持つ(初期値はすべて0)

1. ノードiでイベント発生: V[i] = V[i] + 1
2. メッセージ送信時: 現在のVをメッセージに付与、V[i] = V[i] + 1
3. メッセージ受信時: 
   各要素をmax取り: V[j] = max(V[j], 受信したV[j]) for all j
   V[i] = V[i] + 1

例(ノードA, B, C)

            ノードA        ノードB        ノードC
初期:       [0,0,0]        [0,0,0]        [0,0,0]

a1発生:     [1,0,0]
  A→Bへ送信:               [1,1,0](受信してV[B]++)

b1発生:                    [1,2,0]
  B→Cへ送信:                              [1,2,1]

a2発生:     [2,0,0]

イベント a2 と c1:
  a2: V=[2,0,0]
  c1: V=[1,2,1]
  
比較:
  a2[A]=2 > c1[A]=1  → a2の方が進んでいる要素あり
  a2[B]=0 < c1[B]=2  → c1の方が進んでいる要素あり
  
  → どちらの要素も一方が大きいとは言えない
  → a2とc1は「同時発生(concurrent)」と判断できる!

因果関係の比較規則

V(a) ≤ V(b) (aはbの前、または同時):
  V(a)[i] ≤ V(b)[i]  が全てのiで成立

a → b(aはbの直接的・間接的な原因):
  V(a) < V(b)(V(a) ≤ V(b) かつ V(a) ≠ V(b))

a ∥ b(同時発生、因果関係なし):
  V(a) < V(b) でも V(b) < V(a) でもない

バージョンベクター(Version Vector)

ベクタークロックをレプリカ間のデータ競合検出に応用したもの。Amazon Dynamoが採用。

ノードA, B, Cがkey="shopping_cart"を管理

初期: [A:0, B:0, C:0] = []

ユーザーがノードAに「りんご追加」:
  A書き込み: value=["りんご"], version=[A:1, B:0, C:0]

ユーザーがノードBに「バナナ追加」(Aの更新が届く前に):
  B書き込み: value=["バナナ"], version=[A:0, B:1, C:0]

2つのバージョン:
  [A:1, B:0] = ["りんご"]
  [A:0, B:1] = ["バナナ"]

バージョンベクターの比較:
  [A:1, B:0] と [A:0, B:1] → どちらも相手より大きい要素と小さい要素がある
  → 競合(Concurrent writes)を検出!

競合の解決

競合を検出した後の解決策:

1. Last Write Wins(Dynamo のデフォルト)
   → タイムスタンプで最後を選ぶ(精度問題あり、データロス)

2. アプリケーションで解決
   → 「りんご」と「バナナ」の両方をクライアントに返す
   → ユーザーかアプリが正しいマージを決定

3. CRDT(自動マージ)
   → セット型CRDTなら「りんご + バナナ」を自動的に正解とする
   → [CRDT](./concepts_ddia_crdt.md)を参照

ドットバージョンベクター

実際のDynamoDB系では、「誰が書いたか」の情報をより精密に管理するドットバージョンベクターを使うことが多い。詳細は省略するが、基本思想は同じ。

ベクタークロックの限界

ノード数Nに比例してベクターサイズが増える
→ N=1000のクラスターでは1000要素のベクター

対策:
  - ノード数が増えたら古いエントリを刈り込む
  - CassandraはHybrid Logical Clock(HLC)でサイズ問題を軽減
  - Riak, AmazonDynamoはバージョンベクターを採用

まとめ

方式判定できること限界
物理クロック大まかな前後NTP誤差で逆転しうる
Lamportクロック因果的な前後同時発生を検出できない
ベクタークロック因果関係 + 同時発生の検出ノード数でサイズ増大

関連概念

出典・参考文献

  • Leslie Lamport, “Time, Clocks, and the Ordering of Events in a Distributed System” (1978) — Communications of the ACM
  • Colin Fidge, “Timestamps in Message-Passing Systems” (1988)
  • Amazon Dynamo Paper — DeCandia et al. (2007)
  • Martin Kleppmann, Designing Data-Intensive Applications (2017) Chapter 8-9
  1. 1. 🗄️データ志向アプリケーション設計:概要
  2. 2. 🧩データモデルとクエリ言語
  3. 3. 💾ストレージエンジンとインデックス
  4. 4. 🔁レプリケーション
  5. 5. 🍕パーティショニング(シャーディング)
  6. 6. 🔒トランザクションとACID
  7. 7. 分散システムの本質的な問題
  8. 8. 🤝一貫性と分散合意
  9. 9. 📦バッチ処理
  10. 10. 🌊ストリーム処理
  11. 11. 📋エンコーディングとスキーマ進化
  12. 12. 🔗Sagaパターンと分散トランザクション
  13. 13. 🏗️データシステムの統合設計
  14. 14. 📸MVCC(多版型同時実行制御)
  15. 15. 📊列指向ストレージとOLAP設計
  16. 16. 🕰️ベクタークロックと因果順序
  17. 17. 🔀CRDT(競合なし複製データ型)
  18. 18. 🔍クエリオプティマイザーと実行計画
  19. 19. キャッシュ戦略とRedis設計
  20. 20. 🔎全文検索と転置インデックス
  21. 21. 🌐NewSQL(分散ACIDデータベース)
  22. 22. 📝WALと論理レプリケーション
  23. 23. 🔌コネクションプーリング
  24. 24. 🚧ゼロダウンタイムマイグレーション
  25. 25. 🆔分散ID生成
  26. 26. 🔄N+1問題とDataLoaderパターン
  27. 27. 📈タイムシリーズDB
  28. 28. 🛡️Row Level Security(行レベルセキュリティ)
  29. 29. 📤Outboxパターン(トランザクショナルアウトボックス)
  30. 30. 💾DBバックアップとPITR
  31. 31. ⚠️データベース設計アンチパターン
  32. 32. 🕸️グラフDB深掘り
  33. 33. 🔋バックプレッシャーとサーキットブレーカー

出典: Martin Kleppmann, 'Designing Data-Intensive Applications' (2017) Chapter 8-9