論理時計:Lamportタイムスタンプとベクタークロック
分散システムで「どっちが先か」を物理時刻に頼らず正しく判定。因果順序を捉えるLamport時計とベクタークロックの数理を、happened-before関係から原理で理解できます。
- 1.物理時計はノード間でずれ、巻き戻りも起きるため、分散環境のイベント順序付けには使えない。代わりに論理時計で因果順序を捉える。
- 2.Lamport時計はカウンタ1つで因果を半順序として保存するが、a<bでも因果関係があるとは限らず、並行(concurrent)を判定できない。
- 3.ベクタークロックはノード数分の配列を持ち、VC比較で「因果あり」と「並行」を厳密に区別できる。コストは配列サイズに比例する。
なぜ物理時計では順序を決められないのか
分散システム(/devops/microservices/)で「イベントAとBはどちらが先に起きたか」を判定したくなる場面は多い。たとえば同じデータへの2つの更新が競合したとき、どちらを最終値にするかを決めるには順序が要る。素朴には各ノードの壁時計(wall clock)のタイムスタンプを比較すればよさそうに見えるが、これは破綻する。
理由は2つ。第一に、ノードごとの物理時計は 必ずずれる(clock skew)。NTP で同期しても数ミリ秒〜数十ミリ秒の誤差は残り、その間に起きた2イベントの大小はあてにならない。第二に、物理時計は 巻き戻る。NTP の補正やうるう秒で時刻が後退すると、「後のイベントの方が小さいタイムスタンプ」という逆転が起きる。
2つのノードのタイムスタンプを比べて t_A < t_B だったとしても、それは「A が因果的に B より前」を意味しない。誤差より短い間隔のイベントでは順序が反転しうる。物理時刻に依存した順序判定は、運が良ければ当たる程度の信頼性しかない。
happened-before 関係:因果の定義
そこで Lamport(1978)は、物理時刻を捨て、イベント間の因果関係そのもの を定義した。これが happened-before 関係(記号 →)である。次の3条件で定める。
- 同一ノード内:同じプロセス内でイベント a が b より前に実行されたなら
a → b。 - メッセージ:a が「送信」、b がその「受信」なら
a → b(送る前に受け取ることはない)。 - 推移律:
a → bかつb → cならa → c。
この → が「a は b に影響を与ええた」という因果の可能性を表す。重要なのは、a → b でも b → a でもないペアが存在することだ。これを 並行(concurrent、記号 a ∥ b) と呼ぶ。並行とは「互いに情報が伝わっていない=どちらが先と言う意味がない」状態であり、矛盾ではなく 分散系の本質 である。→ は全順序ではなく 半順序(partial order) になる。
Lamport タイムスタンプ:半順序を数値に埋め込む
Lamport 時計は、各ノードが整数カウンタ C を1つ持ち、次の規則で更新する。
# 各ノードはローカルカウンタ C を持つ(初期値 0)
# 1) ローカルイベントが起きるたび:
C = C + 1
# 2) メッセージ送信時: まず C をインクリメントし、その値を添付して送る
C = C + 1
send(msg, timestamp = C)
# 3) メッセージ受信時: 自分の C と受信値の大きい方を取り、さらに +1
C = max(C, msg.timestamp) + 1
この規則が保証するのは 時計条件(clock condition):
a → b ならば C(a) < C(b)
つまり因果があれば必ずタイムスタンプは増える。受信時に max を取るのが肝で、これにより送信イベントの値が受信側へ「伝播」し、因果の鎖に沿って単調増加する。
C(a) < C(b) だからといって a → b とは限らない。並行な2イベントにも Lamport 時計は別々の値を割り当てるため、無関係なのに大小がついてしまう。Lamport 時計だけでは「因果あり」と「並行」を区別できない。
実用では、(C, ノードID) の辞書順で全順序(total order)を作れる。同じ C 値のときノードIDで決着をつける手法で、これを Lamport 全順序 と呼ぶ。分散ロックやトランザクションの順序付けに使えるが、あくまで「一貫した任意の順序」であって因果を完全に反映するわけではない点に注意する。
ベクタークロック:並行性を検出する
「因果あり」と「並行」を厳密に判定したいなら ベクタークロック(vector clock) を使う。N ノードの系で、各ノードは長さ N の整数配列 V(各要素が各ノードでの観測カウント)を持つ。
# ノード i の配列を V_i とする(初期は全要素 0)
# 1) ローカルイベント: 自分の要素だけ +1
V_i[i] = V_i[i] + 1
# 2) 送信時: 上記で +1 した後、配列 V_i 全体を添付して送る
# 3) 受信時: 要素ごとに max を取り、最後に自分の要素を +1
for k in 0..N-1:
V_i[k] = max(V_i[k], msg.V[k])
V_i[i] = V_i[i] + 1
比較規則は次の通り。2つのベクタ VA と VB について、
VA ≤ VB ⇔ すべての k で VA[k] ≤ VB[k]
VA < VB ⇔ VA ≤ VB かつ ある k で VA[k] < VB[k]
これを使うと因果関係が 過不足なく 判定できる。
| 判定 | 条件 | 意味 |
|---|---|---|
| a → b(因果あり) | VA < VB | a は b に影響を与ええた |
| b → a(因果あり・逆) | VB < VA | b が a より前 |
| a ∥ b(並行) | VA ≮ VB かつ VB ≮ VA | 互いに未到達。順序なし |
| a と b が同一 | VA = VB | 同じイベント |
Lamport 時計との決定的な違いは、並行を「どちらにも ≤ が成り立たない」状態として明示的に検出できる ことだ。たとえば VA = (2,1,0)、VB = (1,2,0) は、1要素目は A が大きく2要素目は B が大きいので、どちらの方向にも < が成り立たない。これが並行の数学的なシグネチャである。
どちらを使うか:コストと用途
ベクタークロックは万能に見えるが、配列がノード数 N に比例して大きくなる。ノードが動的に増減する系(メッセージ1本ごとに N 要素を運ぶコスト)では重い。一方 Lamport 時計は整数1つで済むが並行を見分けられない。
| 観点 | Lamport時計 | ベクタークロック |
|---|---|---|
| 保持データ | 整数1つ | 長さNの整数配列 |
| 保証 | a→b ⇒ C(a)<C(b)(一方向) | a→b ⇔ VA<VB(双方向) |
| 並行の検出 | 不可 | 可能 |
| 全順序付け | (C, ノードID)で可能 | そのままでは半順序 |
| コスト | O(1) | O(N) |
実務での使い分けは明確だ。全順序のチケット番号 が欲しい(分散ロック、ログのシーケンス付け)だけなら Lamport で十分。競合を検出して両方残したい(Dynamo 系 KVS の兄弟バージョン、CRDT、Git のマージ判定)ならベクタークロックが要る。
分散トレーシング(/devops/distributed-tracing/)の親子スパンも因果を表すが、あれは「1リクエスト内の呼び出し木」を記録するもの。論理時計は、独立に動く複数イベントの大域的な因果半順序 を扱う点が異なる。両者は補完的だ。
まとめ
- 物理時計はずれと巻き戻りのため、分散環境のイベント順序付けには使えない。代わりに 因果(happened-before, →) を直接定義し、論理時計で表す。
- Lamport 時計 は整数1つで
a → b ⇒ C(a) < C(b)を保証するが逆は成り立たず、並行を区別できない。(C, ノードID)で全順序は作れる。 - ベクタークロック は長さ N の配列で
a → b ⇔ VA < VBを双方向に保証し、並行(どちらにも ≤ が立たない)を厳密に検出 できる。コストは O(N)。 - 全順序のシーケンスが欲しいなら Lamport、競合検出が要るならベクタークロック。観測(/devops/observability/)やログ突き合わせ(/devops/log-aggregation/)でも、物理時刻の限界を意識することが正確な因果分析の前提になる。
DevOps/インフラ Article
論理時計:Lamportタイムスタンプとベクタークロックを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
論理時計
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 5
導入後に効く点
Lamport時計はカウンタ1つで因果を半順序として保存するが、a<bでも因果関係があるとは限らず、並行(concurrent)を判定できない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「論理時計 / 分散システム」に近いか確認する。
- 強みである「物理時計はノード間でずれ、巻き戻りも起きるため、分散環境のイベント順序付けには使えない。代わりに論理時計で因果順序を捉える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。