ハイブリッド論理クロック(HLC)とTrueTime
分散DBで「あの更新の後にこの読み取り」を正しく並べたいとき、物理時計は時計ずれで嘘をつきます。HLC と Spanner の TrueTime が、時刻の不確実性をどう飼いならして外部一貫性を保証するのかをつかめます。
- 1.HLC は物理時刻の上限に論理カウンタを足した64ビット時刻で、近似した実時刻を保ちつつ因果順序(happens-before)を必ず壊さず単調増加させる。NTP がずれても順序が逆転しない。
- 2.TrueTime は時刻を1点ではなく不確実区間 [earliest, latest] として返すAPI。GPS・原子時計で誤差εを数ミリ秒に抑え、now() の真の時刻が必ずこの区間内にあると保証する。
- 3.Spanner は commit 時に区間の上限が過ぎるまで待つ commit-wait で、トランザクションの時刻区間が重ならないようにし、外部一貫性(=厳密直列化/strict serializability)を達成する。
なぜ分散DBで「時刻」が難しいのか
単一ノードなら、トランザクションに連番タイムスタンプを振れば全順序が決まります。問題は複数ノードに分かれた瞬間です。各ノードの物理時計(ウォールクロック)は NTP で同期しても数十〜数百ミリ秒ずれ、しかも閏秒補正で逆行することすらあります。あるノードで「後」に起きたイベントが、別ノードの時計では「前」に見える――この逆転が、一貫性モデルの強弱階層で最上位に置かれる 外部一貫性(external consistency) を壊します。外部一貫性とは「トランザクション T1 のコミットが T2 の開始より実時間で前なら、システムが付ける順序でも必ず T1 が前」という、人間の直感どおりの保証です。
対極にあるのが Lamport の 論理クロック です。これはイベントごとに増えるカウンタで、happens-before(因果先行)の関係を必ず保ちます。ただし物理時刻とは無関係なので、「3秒前のスナップショット」のような実時間クエリには使えません。HLC と TrueTime は、この**物理(実時間に近い)と論理(因果順序を壊さない)**の両立をそれぞれ別アプローチで解く技術です。
HLC:物理時刻の上に論理カウンタを重ねる
ハイブリッド論理クロック(Hybrid Logical Clock)は、各時刻を (l, c) の組で表します。l は観測した物理時刻の上限(ミリ秒など)、c はその l の中での連番カウンタです。実装上は l の上位ビットと c の下位ビットを連結し、単一の64ビット整数として比較できます。更新規則は次の通りです。
ローカルイベント / 送信時:
l_new = max(l_old, pt) # pt はローカル物理時計の現在値
if l_new == l_old: c = c + 1 # 物理時刻が進んでいなければ論理側を進める
else: c = 0
return (l_new, c)
メッセージ受信時(受信メッセージの時刻 (l_m, c_m)):
l_new = max(l_old, l_m, pt)
if l_new == l_old == l_m: c = max(c_old, c_m) + 1
elif l_new == l_old: c = c_old + 1
elif l_new == l_m: c = c_m + 1
else: c = 0
return (l_new, c)
肝は受信時に max(l_old, l_m, pt) を取る点です。メッセージ送信側が進んでいれば、受信側はその l_m を取り込んで自分の時刻を引き上げます。これにより、因果関係 a → b があれば必ず HLC(a) < HLC(b)(辞書式順序)が成り立ち、論理クロックの因果保存を引き継ぎます。一方 l は常に物理時刻を上限として追従するため、l と実時刻の乖離は時計ずれの範囲内に有界――つまり物理時刻の近似でもあり続けます。
論理カウンタ c が増えるのは「物理時刻が進んでいないのに新イベントが起きた」場合だけです。物理時計が前進すれば l が更新され c は 0 に戻ります。したがって c の上限は「1つの物理時刻刻みの中で発生するイベント数+取り込んだ最大時計ずれ」程度に抑えられ、現実には16ビットもあれば溢れません。物理時刻という錨があるおかげで、純粋な論理クロックのような無制限の増加を避けられます。
HLC の決定的な利点は 追加ハードウェアが不要なことです。CockroachDB や YugabyteDB はこの HLC を採用し、ノード間で交換するメッセージに HLC を載せて単調増加を維持します。ただし HLC は「真の実時刻との誤差」を保証はしない――NTP の同期品質に依存する点が、次の TrueTime との決定的な違いです。
TrueTime:時刻を「点」でなく「区間」で返す
Google Spanner の TrueTime は発想を逆転させます。「正確な時刻は原理的に知り得ない」と認め、TT.now() が単一の時刻ではなく不確実区間 [earliest, latest] を返します。真の絶対時刻 t_abs は必ずこの区間内にあることが保証され、区間幅 2ε(ε は片側誤差)は GPS 受信機と原子時計を各データセンタに置くことで、平均で数ミリ秒程度に抑えられます。
| API | 返り値 | 意味 |
|---|---|---|
| TT.now() | [earliest, latest] | 真の時刻が必ずこの区間内にある |
| TT.after(t) | 真偽値 | t は確実に過去か(earliest > t なら真) |
| TT.before(t) | 真偽値 | t は確実に未来か(latest < t なら真) |
GPS と原子時計を異なる故障モードを持つ独立系として併用するのが要点です。GPS はアンテナ障害やスプーフィングで狂い得ますが、その場合も原子時計が局所的に時刻を保ち、両者の乖離を監視して ε を動的に拡大させます。ε は時刻同期直後に最小で、次の同期までドリフトとともに増え、また縮む鋸歯状に変動します。重要なのは ε が未知の量ではなく、APIが明示的に返す既知の上界である点で、これがアルゴリズムで時刻ずれを正面から扱える根拠になります。
commit-wait:不確実性を「待ち」で吸収する
TrueTime があっても、区間が重なれば2つのトランザクションの前後は確定しません。Spanner はこれを commit-wait という巧妙な待機で解決します。読み書きトランザクションのコミット手順はこうです。
1. コミットタイムスタンプ s を選ぶ: s = TT.now().latest
2. ロックを保持したまま、TT.after(s) が真になるまで待つ
(= 現在の earliest が s を追い越す=実時刻が確実に s を過ぎるまで)
3. 待機後にコミットを確定し、ロックを解放する
ステップ2の待機時間はおよそ 2ε です。この待ちにより、コミット確定の瞬間には実時刻が必ず s を過ぎていることが保証されます。すると、T1 のコミット後に開始した T2 は、自分のタイムスタンプを TT.now().latest で取るので必ず s より大きい値になります。結果として「実時間で後のトランザクションは必ず大きいタイムスタンプを持つ」――外部一貫性が成立します。ε を小さく保つことが、そのまま commit-wait の待ち時間短縮、すなわちスループット向上に直結するため、Spanner は ε の最小化に投資しているわけです。
Spanner のトランザクションは MVCC でバージョンにタイムスタンプを刻み、複数シャードにまたがる場合は 2相コミット(2PC)で原子性を取ります。commit-wait はこの 2PC の**最終フェーズ(コミット確定の直前)**に挿入されます。読み取り専用トランザクションは逆に commit-wait が不要で、TT.now().latest 時点のスナップショットを取れば、それより前にコミット済みの全データを矛盾なく読めます(ロック不要のラグなし読み取り)。
HLC と TrueTime の対比
両者は「物理と論理の融合」というゴールは同じでも、実時刻の誤差を保証するか否かで別物です。
| 観点 | HLC | TrueTime |
|---|---|---|
| 時刻の表現 | (物理上限 l, 論理 c) の組 | 区間 [earliest, latest] |
| 誤差の扱い | 保証なし(NTP 品質に依存) | ε として明示的に上界保証 |
| 特別なハード | 不要 | GPS+原子時計が必要 |
| 因果順序 | メッセージ交換で必ず保存 | 区間と commit-wait で保証 |
| 外部一貫性 | 原理上は保証しない | commit-wait で保証する |
| 主な採用 | CockroachDB, YugabyteDB | Google Spanner |
CockroachDB は TrueTime 用ハードを前提にできないため、HLC に最大時計ずれの設定値(既定 500ms 相当)を組み合わせ、読み取りで時刻が曖昧な範囲に当たったら uncertainty restart(タイムスタンプを進めて再試行)で安全側に倒します。これは commit-wait のように待つのではなく「疑わしきは読み直す」戦略で、専用ハード不要の代償としてレイテンシ変動を受け入れる設計です。どちらが優れているかではなく、ε を保証できる環境を作れるかという前提の違いが設計を分けます。
TrueTime も HLC も、根底のクロック誤差が想定上界 ε(または設定した最大時計ずれ)を超えれば保証が崩れます。GPS スプーフィングや原子時計の同時故障、NTP の大幅ずれが ε を超過すれば、外部一貫性は静かに破れます。だからこそ Spanner は ε を監視し、信頼できない場合はノードを停止させてまで保証を守ります。時刻同期に依存しない整合性が欲しいなら、CRDT のように因果情報だけで収束するデータ構造を選ぶ、という別系統の解もあります。
まとめ
分散DBで実時間順序を正しく扱う難しさは、物理時計の時計ずれと逆行に起因します。HLC は物理時刻の上限 l に論理カウンタ c を重ね、メッセージ交換で max を取ることで因果順序(happens-before)を必ず保ちつつ実時刻を近似し、専用ハードなしで単調増加する時刻を実現します。TrueTime は時刻を不確実区間 [earliest, latest] で返し、GPS と原子時計で誤差 ε を明示的に上界保証する点が決定的に違います。Spanner はコミットタイムスタンプを区間の上限に取り、TT.after(s) が真になるまで待つ commit-wait で、トランザクションの順序を実時間と一致させ外部一貫性(厳密直列化)を達成します。ε を保証できる環境なら TrueTime+commit-wait、できないなら HLC+uncertainty restart、という前提依存の選択になります。関連して 2相コミット・MVCC・一貫性モデル階層と併せて読むと、分散トランザクションの全体像がつながります。
データベース Article
ハイブリッド論理クロック(HLC)とTrueTimeを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
分散システム
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
TrueTime は時刻を1点ではなく不確実区間 [earliest, latest] として返すAPI。GPS・原子時計で誤差εを数ミリ秒に抑え、now() の真の時刻が必ずこの区間内にあると保証する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「分散システム / 時刻同期」に近いか確認する。
- 強みである「HLC は物理時刻の上限に論理カウンタを足した64ビット時刻で、近似した実時刻を保ちつつ因果順序(happens-before)を必ず壊さず単調増加させる。NTP がずれても順序が逆転しない。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。