【実務・中級編】 スナップショットの整合性保証 – Cloud Spanner

TrueTimeがもたらす神業:Cloud Spannerにおけるスナップショット整合性の裏側と極限の設計パターン

こんにちは。テクニカルリードの私だ。
今日のコードレビュー、あるいはアーキテクチャレビューで、君はこんな質問を受けたことはないか?

> 「SpannerのバックアップやStale Readって、グローバル規模でデータが完全に同期されている保証はどこにあるんですか? どこかでロックを取っているわけじゃないんですよね?」

この問いに、公式ドキュメントのコピペで「Googleのすごい時計があるから大丈夫です」などと答えたとしたら、今日の私のレビューは即座に不合格だ。

Cloud Spannerは、リレーショナルデータベースの「強整合性(ACID)」と、NoSQLの「水平スケーラビリティ」を極限の次元で両立させたモンスタープロダクトだ。その心臓部にあるのが、分散環境における「時間」の不確実性を物理的にねじ伏せるTrueTime APIと、それによって支えられるスナップショットの整合性(Snapshot Isolation)である。

今回は、バックアップ取得や過去時点クエリ(Stale Read)の裏側で何が起きているのか、その物理的・論理的メカニズムを丸裸にし、実務の現場でどうこの特性をハックすべきかを伝授しよう。

—

1. 分散システムの悪夢:なぜ「時間」は一致しないのか?

スナップショット整合性の話を始める前に、分散システムの根源的な課題についておさらいしておこう。

地球規模で分散配置されたノード群において、単一の「現在時刻」を一致させることは極めて困難だ。ネットワークの遅延(レイテンシ)が存在する以上、A地点の時計とB地点の時計を完全に同期させ続けることは、相対性理論の観点からも不可能である。

一般的な分散DBでは、この「時間の不確実性」を回避するために、中央集約的なタイムスタンプサーバーを置いたり、リーダーノードとのラウンドトリップで順序を保証したりする。しかし、これはスケーラビリティの致命的なボトルネック(単一障害点およびレイテンシの増大)を生む。

ここでGoogleは発想を変えた。「時間のズレ(不確実性)を隠すのではなく、その不確実性を数値として受け入れ、システム全体の数学的保証に組み込んでしまえばいい」と。
それが TrueTime だ。

—

2. TrueTimeの正体:時刻を「幅(Interval)」として捉える発想

TrueTimeは、単なる「現在時刻を返すAPI」ではない。返す値は「絶対的な時刻の点」ではなく、幅を持った時間区間(Interval)である。

$$\text{TrueTime.now()} = [t_{\text{earliest}}, t_{\text{latest}}]$$

GPSレシーバーと原子時計を各データセンターに物理配置し、ハードウェアレベルで時刻のズレ(イプシロン: $\epsilon$)を極小化(通常は数ミリ秒以内)している。APIが返す時刻の幅の誤差は、せいぜい $\pm 1 \to 7$ ミリ秒程度だ。

コミット待ちメカニズム(Commit Wait)

Spannerでデータが書き込まれる際、トランザクションマネージャーはこのTrueTimeを利用して、次のようなルールを強制する。

1. トランザクション $T_1$ がコミットタイム $s$ を決定する。
2. $s$ は、その時点の $t_{\text{latest}}$ よりも将来の時刻に設定される。
3. Spannerは、現実世界の時間が確実に $s$ を過ぎたこと($t_{\text{earliest}} > s$ となること)を確認するまで、クライアントに応答を返さない。

この「Commit Wait」というわずかな待ち時間(数ミリ秒)を挟むことで、「現実世界で $T_1$ がコミットした後に開始されたトランザクション $T_2$ は、必ず $T_1$ よりも大きなタイムスタンプを持つ」という絶対的な因果律(External Consistency / Linearizability)をグローバル規模で保証しているのだ。

—

3. バックアップとStale Readのメカニズム:ロックフリーの真髄

このTrueTimeの性質があるからこそ、Cloud Spannerは「特定時点(Point-in-Time)のグローバルスナップショット」を、一切の排他ロックを取得することなく一瞬で切り出すことができる。

バックアップ取得時の裏側

Spannerのバックアップは、ストレージ層であるColossus上で動く分散スナップショット機構と密接に連携している。

1. タイムスタンプの確定: バックアップコマンドが発行されると、Spannerは現在のTrueTimeから、過去の安全なグローバルタイムスタンプ $T_{\text{backup}}$ を指定する。
2. メタデータの凍結: $T_{\text{backup}}$ 時点におけるPaxosグループの状態(リーダーの位置やログのメタデータ)を論理的に固定する。
3. 非同期ストレージコピー: Colossusレベルで、データファイル群のその時点における参照(ポイントインタイム・スナップショット)を確立する。

ここで重要なのは、バックアップ中も通常のトランザクション処理は一切ブロックされないという点だ。読み書きのトランザクションは通常通り進みながら、裏側では「あの瞬間($T_{\text{backup}}$)」の状態が完全に保全される。これが、24時間365日無停止でペタバイト級のデータバックアップを可能にする技術的背景である。

Stale Read(過去読み取り)の活用

この仕組みは、バックアップだけでなく、アプリケーションからのクエリ(Stale Read)にも直結している。

— 1時間前のスナップショットを指定してクエリを実行する例
SELECT
FROM Users@{FORCE_TIME_WINDOW=1h}
WHERE country = ‘JP’;

このクエリを発行した際、Spannerはロックを獲得しない。指定されたタイムスタンプ時点のデータを保持しているストレージノードから、無言で、かつ競合なしでデータを読み出す。
「他のトランザクションが書き込み中だから待たされる」という概念が、この世界には存在しないのだ。

—

4. 実務で活かす設計パターンとアンチパターン

さて、このスナップショット整合性の仕組みを理解したシニアエンジニアなら、設計において何を意識すべきかが見えてくるはずだ。実務で使える設計パターンと、やってはいけないアンチパターンを提示しよう。

✅ 設計パターン:監査ログと「タイムトラベル・クエリ」の業務実装

金融やECのシステムでは、「3日前、あのユーザーの残高はどうなっていたか?」を厳密に監査・再現する必要がある。通常のRDBでこれをやろうとすると、変更履歴テーブル(Audit Table)を別途設計し、トリガーやアプリケーション層で差分をせっせと保存する地獄のような実装が必要になる。

Spannerであれば、Stale Readを標準機能として業務ロジックに組み込める。

from google.cloud import spanner

def get_account_balance_at_audit_time(database, account_id, audit_timestamp):
with database.snapshot(read_timestamp=audit_timestamp) as snapshot:
row = snapshot.read(
table=”Accounts”,
keys=[[account_id]],
columns=[“balance”]
)
for val in row:
return val[0]
return None

メリット: アプリケーション側で履歴管理の複雑なコードを書く必要が一切ない。Spanner自体がタイムマシンとして機能する。

❌ アンチパターン:スナップショットの「古すぎる参照」によるレイテンシ悪化

Stale Readやバックアップ・リストアを行う際、「あまりにも古いタイムスタンプ」を指定するとどうなるか?

Spannerは古いバージョンから順次ガベージコレクション(GC)を行っている。デフォルトでは、データの保持期間(Version Retention Period)は最大1時間である。もし1時間以上前のタイムスタンプを指定してStale Readを行おうとすると、データがすでにGCで消去されているため、エラー(`FAILED_PRECONDITION`)となるか、極端なパフォーマンス劣化を招く。

> 警句: 「過去のデータがいつでも無限に見られる」と勘違いしてはならない。バージョン保持期間(デフォルト1時間、最大7日まで拡張可能だがストレージコストに直結する)の制約を意識したアーキテクチャにすること。

—

5. チーフアーキテクトからの提言

Cloud Spannerのスナップショット整合性は、単なる「便利な機能」ではない。それは、「ハードウェア(原子時計・GPS)とソフトウェア(TrueTimeとPaxos)の融合によって、分散システムにおける『時間と空間の不確実性』を極限までハックした芸術品」だ。

このメカニズムを理解していれば、

  • 「なぜロックを取らずに安全にバックアップが取れるのか」
  • 「なぜStale Readがスケールアウト性能を落とさずに実行できるのか」
  • 「どこまで過去のデータを信用してよいのか」

これらすべての疑問に論理的に答え、説得力のあるデータベース設計をレビューで下すことができるはずだ。

次の設計レビューでは、ただ「動くコード」を書くだけでなく、こうした分散原理に基づいた優美なアーキテクチャを描いてみせてほしい。健闘を祈る。

コメント

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