【実務・中級編】 スナップショット読み取りタイムスタンプ – Cloud Spanner

Cloud Spannerの真価:「スナップショット読み取りタイムスタンプ」を極限まで使い倒す設計論

こんにちは。テクニカルリードの私だ。
今日のコードレビューで、あるジュニアエンジニアが書いたクエリを見て思わず天を仰いだ。「過去のデータが欲しいから」という理由で、アプリケーション側で監査ログ用の巨大なテーブルを別個に作り、そこに全変更差分を愚直にブチ込んでいたのだ。

待て待て。お前は今、世界最高峰の分散データベースであるCloud Spannerを使っているんだぞ。
Spannerには、Googleの物理時計の不確実性を極限まで打ち消す「TrueTime」という神の技術が組み込まれている。これを利用した「スナップショット読み取り(Snapshot Read)」を使えば、過去任意のミリ秒における世界の状態を、完全な外部一貫性(External Consistency)を保ったまま、一分の狂いもなく復元できる。独自の監査テーブルや複雑な履歴管理ロジックなど、本来は不要なのだ。

今回は、このスナップショット読み取りタイムスタンプのコアアーキテクチャから、現場の設計で絶対に踏んではいけない地雷、そして圧倒的なパフォーマンスを引き出すパターンまで、容赦なく叩き込んでいく。

—

1. コアアーキテクチャ:なぜSpannerは過去を完璧に覗き見れるのか?

まず、大前提としてCloud Spannerのデータモデルを思い出してほしい。Spannerのデータは、内部でタイムスタンプをキーの一部として保持している(MVCC: 多版同時実行制御)。つまり、データは常に「どの時間帯に有効だったか」という軸を持っている。

通常、我々が発行する読み取りクエリは、現在の最新状態(`NOW()`)を対象にする。だが、スナップショット読み取りでは、次のようなリクエストをSpannerに投げる。

> 「おい、3分12秒前の正確な宇宙の状態を俺に見せてくれ」

これが可能なのは、Googleがデータセンター全体にGPS受信機と原子時計を配置し、時刻の不確実性(誤差の幅、いわゆる $\epsilon$)を極小化するTrueTime APIを持っているからだ。

TrueTimeとタイムスタンプの正体

TrueTimeは、絶対的な時刻を返すのではなく、時刻の「範囲(`[earliest, latest]`)」を返す。Spannerはこの仕組みを使い、コミットされたトランザクションに必ず「絶対にこの時刻以降である」という確実なタイムスタンプを割り当てる。

スナップショット読み取りで特定のタイムスタンプを指定するということは、「そのタイムスタンプが示すTrueTimeの境界が確実に確定した過去の瞬間」の状態を切り取る行為に他ならない。ロックを獲得する必要(Pessimistic Locking)が一切ないため、読み取り性能はスケールし、書き込みトランザクションをブロックすることも、ブロックされることもない。

—

2. 実践:タイムスタンプ指定の3つのパターン

スナップショット読み取りには、主に3つのアプローチが存在する。それぞれの特性を理解し、ユースケースに合わせて正確に使い分ける必要がある。

① `exact_staleness`(正確な古さの指定)

「現在時刻から、正確に〇秒・〇分過去の状態を読みたい」場合に使用する。

— 現在時刻から正確に「10分前」の状態を読み取る(Google Cloud Consoleや各言語のクライアントライブラリ)
— ※SQL構文ではなく、各言語のAPIやSpannerのトランザクションオプションで指定する概念に近い

  • ユースケース: バッチ処理や、直近のデータ不整合の調査。

② `max_staleness`(最大許容古さの指定)

「パフォーマンスを最優先するが、最大でも〇秒以内なら古いデータ(Stale Read)でも構わない」という、レイテンシ最適化のためのアプローチ。スプリット(データ分割片)のリーダーレプリカがローカルに保持しているデータのうち、指定した古さの範囲内に収まるものがあれば、リモートへのラウンドトリップすらスキップして超高速にデータを返す。

  • ユースケース: レイテンシが厳しく問われる参照系API、ダッシュボードの描画。

③ `read_timestamp`(特定の絶対時刻の指定)

「202X-10-25T14:30:00.000000Z」のように、厳密なタイムスタンプを直接指定する。

from google.cloud import spanner

def read_historical_data(instance_id, database_id, read_time):
client = spanner.Client()
instance = client.instance(instance_id)
database = instance.database(database_id)

# スナップショットの作成(特定タイムスタンプを指定)
with database.snapshot(read_timestamp=read_time) as snapshot:
results = snapshot.execute_sql(
“SELECT account_id, balance FROM accounts WHERE region = @region”,
params={“region”: “jp-east”}
)
for row in results:
print(f”Account: {row[0]}, Balance at {read_time}: {row[1]}”)

  • ユースケース: 「監査証跡の完全な再現」「特定のインシデント発生瞬間のデバッグ」「タイムトラベルクエリの実装」。

—

3. 堅牢な設計パターン:イベントソーシングと監査の「アンチパターン脱却」

ここで、実務でよくある設計のアンチパターンと、Spannerのタイムスタンプを活用した正しい設計を比較しよう。

❌ アンチパターン:アプリケーション層での履歴テーブルの構築

  • やり方: `users` テーブルの他に、`user_history` テーブルを作り、UPDATEのたびにトリガーやアプリのコードで履歴をINSERTする。
  • 弊害:

1. 書き込みトランザクションの肥大化(2重書き込みによるレイテンシ増大)。
2. アプリのバグで履歴が欠損するリスク。
3. 「ある時点の全マスタの状態」を復元するためのクエリが極めて複雑になる。

◯ 正しい設計パターン:Spannerのガベージコレクション(GC)期間を活用したタイムトラベル設計

Spannerはデフォルトで過去1時間(最大7日まで拡張可能)のデータバージョンを保持し続けている。つまり、過去1時間以内であれば、通常のテーブルに対して `read_timestamp` を指定するだけで、追加の履歴テーブルなど一切作らずに、完全に過去の状態を復元できる。

もし1時間以上の長期的な監査証跡が必要な場合でも、アプリで履歴テーブルを作るのではなく、Cloud Dataflow等を用いて定期的にCloud StorageやBigQueryへ過去スナップショットの差分(またはフルエクスポート)を吐き出せばよい。SpannerのOLTPデータベースとしての責務を汚染せずに済む。

—

4. パフォーマンス上の注意点と「踏んではいけない地雷」

チーフアーキテクトとして、現場のエンジニアに必ず警告しておかなければならない「罠」が2つある。

罠1: ガベージコレクション(GC)の制限時間を超えたタイムスタンプを指定する

Spannerのデフォルトのガベージコレクションウィンドウは60分(1時間)だ。もし開発者が「昨日のこの時間に何があったか調べたい」と、24時間前のタイムスタンプを指定してスナップショット読み取りを実行すると、以下のエラーに直面する。

> `FAILED_PREPRECE: 4 OUT_OF_RANGE: The transaction timestamp is out of range`

データはすでにガベージコレクションによって物理削除されているため、復元は不可能だ。もし1時間よりも前のデータをスナップショット読み取りしたい場合は、データベースの `version_retention_period` を明示的に最大7日まで引き上げる必要がある。ただし、ストレージコストが増加するため、ビジネス要件とトレードオフになることを忘れるな。

— 例: バージョン保持期間を3日(72時間)に拡張する場合
ALTER DATABASE YourDatabase SET OPTIONS (
version_retention_period = ’72h’
);

罠2: ステーブルリード(Stale Read)でのインデックス設計の不備

`max_staleness` や `read_timestamp` を使った読み取りであっても、適切なセカンダリインデックスやインターリーブ(Parent-Child)構造が組まれていなければ、フルスカン(全件走査)が発生してレイテンシが跳ね上がる。
「過去のデータを読むから遅くてもいい」というのは言い訳にならない。スナップショット読み取りであっても、クエリプランは通常の読み取りと同様に最適化されている必要がある。必ず `EXPLAIN` コマンドで実行計画をレビューし、プレフィックスが正しくヒットしているか確認しろ。

—

5. チーフアーキテクトからの総括

Cloud Spannerのスナップショット読み取りタイムスタンプは、単なる「おまけの機能」ではない。これは、「整合性と時系列の矛盾」という分散システムの永遠の課題に対するGoogleからの回答だ。

コードレビューで「過去のデータを保持する仕組みを作りました」というプルリクエストを見かけたら、こう問いかけろ。

> 「それ、Spannerの `read_timestamp` で3行で書けるよね?」

車輪の再発明はやめよう。Spannerのアーキテクチャの根幹を理解し、その能力を極限まで引き出す設計こそが、プロフェッショナルの仕事だ。次の設計レビューでは、洗練されたスナップショット活用のコードを見せてくれることを期待している。

コメント

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