【実務・中級編】 TrueTime API – Cloud Spanner

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」として使うのは、フェラーリを近所のスーパーへの買い出しに使うようなものだ。そのアーキテクチャの根底にある物理的な知見を理解し、システムに魂を吹き込め。

次は、君たちがその設計で私を驚かせてくれることを期待している。

コメント

タイトルとURLをコピーしました