タイムスタンプオラクル(TSO)とTrueTimeの深層:Cloud Spannerは、いかにして「グローバルな現在時刻」の幻影を実装したか
こんにちは。チーフアーキテクトの私だ。
今日のコードレビュー、あるいは昨日の分散アーキテクチャの設計ミーティングで、こんな幻想を抱いた者はいないか?
「全世界に分散したリージョン間で、トランザクションの前後関係を完全なタイムスタンプ順に保証したい。アプリケーション側で時刻をとって、それをキーにすればいいのではないか?」
もし君のチームにこの設計を持ち込む者がいたら、即座にそのプルリクエストをブロックし、熱いコーヒーでも奢りながらこの記事を読ませてほしい。分散システムにおいて「共通の現在時刻」などというものは存在しない。物理的な光速の限界と、各サーバーが持つハードウェアクロックの「ドリフト(ずれ)」が存在する限り、単一のマスター時計に頼ったシステムはいずれスケーラビリティの壁にぶつかるか、データ破損の悪夢を見る。
だが、Cloud Spannerはこの物理的制約をTrueTimeとタイムスタンプオラクル(TSO)のコンビネーションによって見事にハックし、グローバル規模での外部一貫性(External Consistency / Serializable)を実現している。
今回は、Spannerの心臓部であるTSOのメカニズムを丸裸にし、実務の現場で我々エンジニアがどのような設計上の配慮をすべきかをロジカルに伝授しよう。
—
1. なぜ「普通の時計」では分散トランザクションを裁けないのか?
分散データベースの最大の敵は「因果律の逆転」だ。
東京のユーザーAがデータを更新した「直後」に、ニューヨークのユーザーBがそのデータを読み取るとしよう。この時、データベースが分散された複数のノードで動いている場合、各ノードのCPU内蔵クロック(NTPなどで同期されていても)には必ず数ミリ秒の誤差(ジッタ・ドリフト)がある。
もし東京の時計が5ミリ秒進んでいたらどうなるか?
ニューヨークから見た物理的時間軸では「後」のイベントが、タイムスタンプ上では「前」になってしまい、トランザクションの直列化可能性(Serializable)が盛大に崩壊する。ダーティリードや幻影読取のオンパレードだ。
これを力技で解決しようとすると、全ノードが単一のロックサーバーや単一のタイムスタンプ生成マスターに往復通信(RPC)を行う必要がある。結果として、スケーラビリティは数台のサーバーで頭打ちになり、グローバルデータベースとしての存在価値は消え失せる。
Spannerは、この課題に対して「不確実性(Uncertainty)をファーストクラスの概念として受け入れる」という極めて数学的かつエレガントなアプローチを取った。
—
2. TrueTimeとTSOのコアメカニズム:不確実性との共存
Spannerのタイムスタンプ割当の中核には、TrueTimeと呼ばれるAPIが存在する。これは単なる時刻取得APIではない。時刻を「絶対的な一点」ではなく、誤差の幅(不確実性)を持った「時間区間(Time Interval)」として返す。
$$\text{TrueTime.now}() \rightarrow [t_{\text{earliest}}, t_{\text{latest}}]$$
ここで保証されるのは、真の絶対時刻 $t_{\text{absolute}}$ が常にこの区間内に存在するという点だ($t_{\text{earliest}} \le t_{\text{absolute}} \le t_{\text{latest}}$)。通常、GPSレシーバーと原子時計(ルビジウム発振器)をデータセンターごとに配備することで、この誤差幅 $\epsilon$(イプシロン)を数ミリ秒(通常は1〜7ms程度)に抑え込んでいる。
タイムスタンプオラクル(TSO)の役割
このTrueTimeの性質を利用し、書き込み(Commit)トランザクションに対して厳密な順序を持つ一意なタイムスタンプを割り当てるのがタイムスタンプオラクル(TSO)の役割だ。
厳密には、SpannerではTSOの機能は Paxos グループのリーダー(Spannerのシャーディング単位であるSplitsのリーダー)に分散して実装されているが、概念モデルとしてのTSOの動きはこうだ:
1. コミット待ち(Commit Wait)の強制:
トランザクションがコミットを要求すると、リーダーはTrueTimeから現在の時刻区間 $[t_{\text{earliest}}, t_{\text{latest}}]$ を取得する。
ここで、リーダーはタイムスタンプ $s$ を $t_{\text{latest}}$ よりも大きく(あるいは等しく)設定し、かつ、実際の絶対時刻が $s$ を過ぎるまで(つまり $t_{\text{earliest}} > s$ になるまで)コミットを意図的に待機(Commit Wait)させる。
2. 因果律の保証:
トランザクション $T_2$ が トランザクション $T_1$ のコミット完了「後」に開始された場合、TrueTimeの保証により、$T_2$ に割り当てられるタイムスタンプ $s_2$ は、必ず $T_1$ のタイムスタンプ $s_1$ よりも大きくなる ($s_1 < s_2$)。
これにより、「物理的な因果関係」と「データベース上のタイムスタンプの順序」が完全に一致する(外部一貫性)。ロックを使わずに、グローバルなスナップショット読み取りが整合性を保ったまま実行できる所以がここにある。
—
3. 実務へのインプリケーション:コードと設計で何を意識すべきか?
このTSOとTrueTimeの挙動は、日々のアプリケーション設計やクエリチューニングにダイレクトに関係している。実務で嵌りがちなポイントを3つ挙げよう。
① 読み取り専用トランザクション(Stale Reads)の積極活用
Spannerの最大の武器の一つが、過去の特定のタイムスタンプ、あるいは特定の経過時間(Staleness)を指定した読み取りだ。これにはロックが一切かからない。
// Java (Cloud Spanner Client Library) の例
// 10秒前のスナップショットを読み取る(ロック競合ゼロ・高スループット)
TimestampBound bound = TimestampBound.ofExactStaleness(Duration.ofSeconds(10));
try (ReadOnlyTransaction tx = client.readOnlyTransaction(bound)) {
ResultSet resultSet = tx.executeQuery(
Statement.of(“SELECT AccountId, Balance FROM Accounts WHERE Region = ‘APAC'”)
);
// 処理…
}
【設計の勘所】
厳密なリアルタイム性が求められないダッシュボードの集計、レポート生成、監査ログの参照などは、必ずこの `ReadOnlyTransaction`(Stale Read)を使え。TSOのコミット待ちやリーダーのロック競合から完全に解放され、リニアなスケールアウトの恩恵を受けられる。
② アプリケーション側で「現在時刻」を生成してキーにしない
時々、インサートするレコードの主キー(IDやタイムスタンプ)に、アプリケーション側の `System.currentTimeMillis()` を使う設計を見かけるが、これはSpannerにおいてはアンチパターンヌル(最悪の設計)だ。
— ❌ アンチパターン:単調増加するタイムスタンプをキーにする
CREATE TABLE EventLogs (
EventTime TIMESTAMP NOT NULL, — アプリ側で生成した時刻など
EventId INT64 NOT NULL,
Payload STRING(MAX),
) PRIMARY KEY (EventTime, EventId);
なぜダメか?
Spannerのデータは主キーの順序に従って「スプリット(Split)」という単位で物理ノードに分割・配置される。もし現在時刻をキーの先頭(Prefix)にすると、「今この瞬間に書き込まれている全データ」が、たった一つの特定の物理ノード(最新のタイムスタンプを持つスプリット)に集中(ホットスポット化)してしまう。TSOがどれほど優秀でも、物理的なI/Oのボトルネックの前には無力だ。
【正しい設計】
分散キー(UUIDv4や、ハッシュ化されたプレフィックス、あるいは連番の逆転など)をプライマリキーの先頭に据え、時刻情報は通常のカラムとして保持せよ。
— ⭕ 正しい設計:ホットスポットを防ぐ分散キーの採用
CREATE TABLE EventLogs (
ShardId INT64 NOT NULL, — 0〜15などのハッシュ値やランダム値
EventId STRING(36) NOT NULL, — UUIDv4
EventTime TIMESTAMP NOT NULL, — TrueTimeに基づくコミット時刻
Payload STRING(MAX),
) PRIMARY KEY (ShardId, EventId);
③ トランザクションのレイテンシと「Commit Wait(約2〜7ms)」の理解
Spannerの書き込みトランザクションのレイテンシの下限には、TrueTimeの誤差 $\epsilon$ を吸収するための Commit Waitの時間(通常数ミリ秒) が物理的に含まれている。
つまり、RedisやインメモリDBのような「サブミリ秒でのインメモリ書き込み」をSpannerに期待してはならない。Spannerの書き込みは、世界規模の整合性を担保するための「代償」として、数ミリ秒のオーバヘッドを内包している。
【設計の勘所】
高頻度なカウンターのインクリメントや、ミリ秒単位の超低レイテンシが要求される単一キーのセッション管理などをSpanner単体で行うのは避けるべきだ。リレーショナルな整合性とグローバル分散が必要なコアデータ(金融口座、ユーザープロファイル、在庫台帳など)に絞ってSpannerを適用し、エッジ側のキャッシュ層(Memorystoreなど)と適切に役割分担させよ。
—
4. チーフアーキテクトからの総括
Cloud SpannerのタイムスタンプオラクルとTrueTimeの本質は、「ハードウェアの物理的限界(時計のズレ)を、ソフトウェアのアルゴリズム(不確実性の受容とコミット待ち)で美しく包み込み、開発者から分散の複雑性を隠蔽したこと」にある。
しかし、フレームワークがどれほど優秀であっても、下層の物理原則――すなわちホットスポットの回避や、トランザクションの性質理解を怠れば、システムはパフォーマンスの劣化という形で牙をむく。
コードレビューで「なぜこのキー設計にしたのか」「なぜこの読み取りでロックを取っているのか」と問われたとき、TSOとTrueTimeの挙動を脳裏に思い浮かべ、ロジカルに説明できるエンジニアであってほしい。
君たちのアーキテクチャが、グローバル規模で揺るぎない整合性を維持し続けることを期待している。
コメント