TrueTimeは「魔法」ではない:Cloud Spannerのトランザクションを支配する物理的制約
Cloud Spannerを単なる「分散RDBMS」と呼ぶのは、ジェットエンジンを「扇風機」と呼ぶのと同じくらい解像度が低い。Spannerが真に革命的なのは、単にSQLが動くからではない。分散環境において、物理法則(光速と時刻同期)を逆手に取り、一貫性を担保したことにこそ、その真価がある。
今回は、Spannerの心臓部である「TrueTime API」を解剖し、我々アプリケーションエンジニアがこの「時刻の不確実性」とどう向き合うべきか、その極意を伝授する。
—
1. TrueTimeの正体:時刻を「幅」として扱う勇気
多くの分散システムが「時刻のずれ」に悩まされ、PaxosやRaftによる合意形成のオーバーヘッドで苦しむ中、Spannerは全く異なるアプローチをとった。それがTrueTimeだ。
TrueTimeの核心は、時刻を単一の点(Point)ではなく、区間(Interval)として定義したことにある。
- 絶対時刻の不確実性($\epsilon$): 各ノードには原子時計とGPS受信機が備わっているが、それでもミリ秒単位の微小な誤差は避けられない。
- $TT.now()$の返り値: `[earliest, latest]` という区間を返す。つまり、「今の時刻は、確実にこの範囲の中に存在する」という保証だ。
Spannerは、この「不確実性の幅($\epsilon$)」が収束するまで待機(Commit Wait)する。これにより、「あるトランザクションの終了時刻が、次のトランザクションの開始時刻よりも確実に前である」という物理的な証拠を掴むのだ。
エンジニアへの教訓:
「時刻は常に正しい」と信じて設計してはならない。Spannerは「時刻が正確ではないかもしれない」という事実を物理的に解決しているのであり、アプリケーション層で時刻を操作しようとする設計は、Spannerの恩恵を自らドブに捨てる行為に等しい。
—
2. 実務上の設計パターン:`COMMIT_TIMESTAMP`の正しい利用
アプリケーション開発で、レコードの更新順序やライフサイクルを管理したい場合、安易にアプリケーション側で `Timestamp.now()` を発行してカラムに突っ込んではいけない。それはTrueTimeの恩恵を無視した「ただのゴミデータ」になり得るからだ。
Spannerの真価を活かすなら、`COMMIT_TIMESTAMP` オプションをフル活用せよ。
— テーブル定義のベストプラクティス
CREATE TABLE UserActivity (
UserId INT64 NOT NULL,
ActivityId INT64 NOT NULL,
— サーバーサイドで確定した時刻を自動付与
UpdatedAt TIMESTAMP OPTIONS (allow_commit_timestamp = true),
) PRIMARY KEY (UserId, ActivityId);
このオプションを使えば、Spannerがトランザクションコミット時にTrueTimeを用いて決定した「グローバルに一貫した時刻」が注入される。
してはいけない設計:
- クライアントサイド生成時刻: クライアントの時計は信用できない。
- シーケンスIDの自前実装: 分散環境でのシーケンス生成はボトルネックの元凶。Spannerには不要だ。
—
3. パフォーマンスの落とし穴:ホットスポットとトランザクションの寿命
TrueTimeの「待機」は、トランザクションのレイテンシに直結する。特に、過度に長大なトランザクションは禁物だ。
避けるべきアンチパターン:
1. Read-Modify-Writeのループ:
トランザクション内で重い計算を行い、その後書き込む。これではTrueTimeの待機時間を含め、ロック保持時間が長くなりすぎて競合が頻発する。
2. シーケンシャルなキー設計:
単調増加するIDをプライマリキーにすると、特定のノードにアクセスが集中する。Spannerは分散DBだが、物理的な書き込み先は特定のノードに偏る可能性がある。UUIDやハッシュ値をプレフィックスに混ぜるのが鉄則だ。
—
4. チーフアーキテクトからの助言:一貫性を「使いこなせ」
Spannerはデフォルトで「強一貫性(Strong Consistency)」を提供する。これは、どのノードから読み込んでも最新のTrueTimeに基づくデータが返ることを意味する。
もし、読み込みのレイテンシを極限まで削る必要がある(例えば、ログ分析やダッシュボード用の読み取り)のであれば、Stale Read(古いデータの読み取り)を検討せよ。
// 15秒前までのデータであれば、最新でなくてもよい場合のクエリ例
Timestamp bound = Timestamp.ofTimeSecondsAndNanos(
Instant.now().getEpochSecond() – 15, 0);
ReadOnlyTransaction tx = dbClient.readOnlyTransaction(
TimestampBound.ofExactStaleness(Duration.ofSeconds(15)));
これはTrueTimeによる「コミット待機」の制約を回避し、最寄りのノードからローカルに読み取ることでパフォーマンスを劇的に向上させる。「どこまで古いデータが許容されるか」をビジネス要件として定義できるかが、一流のエンジニアとそうでないエンジニアの分かれ目だ。
—
結論:技術の本質を見極めよ
TrueTimeは、分散システムにおける「時刻」という名のカオスを、物理学の力で秩序立てたものだ。
君たちがSpannerを扱う際、心に留めておくべきは以下の3点だ。
1. 時刻を自前で管理しようとしない: TrueTimeと`COMMIT_TIMESTAMP`に任せろ。
2. 不確実性を設計に組み込む: 読み取りの整合性要件をビジネス側と握り、Stale Readを使いこなせ。
3. トランザクションを短く保つ: 物理的な待機時間を意識した、疎結合な設計を心がけろ。
Spannerをただの「SQLが動くDB」として使うのは、フェラーリを近所のスーパーへの買い出しに使うようなものだ。そのアーキテクチャの根底にある物理的な知見を理解し、システムに魂を吹き込め。
次は、君たちがその設計で私を驚かせてくれることを期待している。
コメント