Cloud SpannerのPITR(Point-In-Time Recovery)を極限まで理解する:バージョン管理されたデータストアの深層
設計レビューの席で、こんな質問を受けたことはないか?
> 「SpannerのPITRって、裏側でどうやってデータ保持しているんですか? 毎日フルバックアップ取るのと何が違うんですか? ストレージ容量が爆発してコストが吹き飛んだりしません?」
ふっ、良い着眼点だ。
適当に「GCPがよしなにやってくれます」などと答えた日には、チーフアーキテクトとしての信用が一瞬で地に落ちる。
Cloud SpannerのPoint-In-Time Recovery(PITR)は、単なる「バックアップツール」ではない。Spannerのコアアーキテクチャである「バージョン管理された分散ストレージ(Multi-version Concurrency Control: MVCC)」そのものを巧みにハックした、分布式ストレージの芸術品なのだ。
今回は、このPITRの内部構造を剥ぎ取り、現場の設計にどう活かすべきか、その「極限の知見」を叩き込もう。
—
1. PITRの正体:バックアップではなく「過去のタイムスタンプへのタイムトラベル」
まずマインドセットを変えてほしい。PITRとは、「過去のデータを復元するためのアーカイブファイル生成機能」ではない。
Cloud Spannerは、本質的にすべての書き込み(Mutation)に対してタイムスタンプを付与し、過去のバージョンをストレージ層で保持し続けるMVCCデータベースである。
通常、この過去バージョンはガベージコレクション(GC)によって一定時間(デフォルトで最大1時間だが、PITR有効時は最大7日前まで拡張される)経過すると物理削除される。
PITRの本質:
> 「GCのレーザーメスから、指定したウィンドウ(最大7日間)内の古いデータバージョンを保護するフラグ」であり、「任意の過去のマイクロ秒(Microsecond)単位のタイムスタンプを指定して、当時のスナップショットリードやデータ復元を行える機能」である。
つまり、バックアップのリストアにありがちな「数時間のダウンタイムとダンプデータのインポート」という苦行は存在しない。過去のタイムスタンプを指定したクエリ、あるいは別インスタンスへの超高速データエクスポート(あるいはインプレースリストア)が、数分で完了する。
—
2. 内部アーキテクチャ:ColossusとPaxosグループの裏側
では、このマジックが分散ストレージ層でどう実装されているのか、その深層を覗いてみよう。
分散ファイルシステム(Colossus)とバージョン管理
Spannerのデータは、Googleの分散ファイルシステム「Colossus」上に、SSTable(Sorted String Table)形式でイミュータブル(不変)なファイルとして書き込まれる。
1. 追記型(Append-only)のイミュータブル性:
Spannerは既存のデータをその場で書き換えない(In-place updateをしない)。行の更新や削除は、常に「新しいタイムスタンプを持つレコードの追加」として処理される。
2. タイムスタンプの伝搬(TrueTime API):
Googleの原子時計とGPSによって同期された`TrueTime` APIにより、グローバルに一意かつ厳密な因果関係を持つタイムスタンプ($t_{now}$)が割り振られ、データと不可分に保存される。
3. GCとPITRウィンドウのせめぎ合い:
PITRが無効な場合、Spannerは約1時間前のタイムスタンプよりも古いバージョンをバックグラウンドのGCプロセスで容赦なくマージ・削除する。
しかし、PITRを有効にすると、このGCの保持期限が「最大7日間」まで強制的に延長される。 つまり、Colossus上の古いSSTableブロックがガベージとして回収されず、保持され続けるのだ。
[クライアントの書き込み]
│
▼
[TrueTime API] ──(グローバル時刻を付与)
│
▼
[Paxos グループ] ──(分散合意)
│
▼
[Colossus ストレージ (イミュータブル SSTable)]
├── Version @ T-6 days <-- ★PITRによりGCから保護される領域
├── Version @ T-3 days <-- ★PITRによりGCから保護される領域
└── Version @ T-now <-- 最新データ
この構造により、ストレージ層では「過去の任意の時点のスナップショット」を、物理的なコピーを増殖させることなく(差分管理とイミュータブルなブロックの保持によって)効率的に維持している。
---
3. 実務で使う:PITRを活用したリストアの作法
システム開発の現場で最も恐ろしいのは、「バグを含んだバッチ処理が本番データを破壊した瞬間」だ。
従来のRDBであれば、深夜のバックアップからリストアして、トランザクションログ(WAL)をロールフォワードして…と何時間も冷や汗をかくことになる。
SpannerのPITRを使えば、これを「事件発生の1秒前のタイムスタンプ」を指定して、綺麗にリカバーできる。
A. 過去の特定の瞬間へのデータ復元(Point-in-time recovery APIの活用)
Google Cloud Console、あるいはgcloudコマンドを使って、特定のタイムスタンプ(RFC 3339形式)を指定し、別データベースへ復元を実行する。
202X年10月24日 12:00:00 UTC の状態にデータベースを復元する
gcloud spanner databases restore \
–instance=production-instance \
–source-database=core-db \
–destination-database=core-db-recovered-202X1024 \
–restore-timestamp=202X-10-24T12:00:00Z
このコマンドが叩かれた瞬間、Spannerは物理的な全データコピーを裏でチート級の速度(メタデータの操作とストレージのハードリンク的参照)で実行し、指定したミリ秒の状態で新しいデータベースを立ち上げる。数テラバイトのDBであっても、数分で完了する。
B. アプリケーションコードからのタイムトラベル・クエリ(スナップショットリード)
リストアするまでもない。「ユーザーが誤って削除した直前のデータを参照して、サクッと戻したい」という要件であれば、リストア不要でクエリレベルでタイムトラベルが可能だ。
以下は、Java(JDBC / Cloud Client Library)を用いたスナップショットリードの実装例だ。
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.;
public class PitrTimeTravelExample {
public static void recoverDeletedUser(DatabaseClient dbClient, String userId, Timestamp targetTime) {
// 指定した過去のタイムスタンプ(例:事故発生の1分前)でスナップショットを取得
try (ReadOnlyTransaction tx = dbClient.singleUseReadOnlyTransaction(
TimestampBound.ofExactStaleTimestamp(targetTime))) {
// 過去の時点のデータを参照
ResultSet resultSet = tx.executeQuery(
Statement.newBuilder(“SELECT name, email FROM Users WHERE userId = @userId”)
.bind(“userId”).to(userId)
.build()
);
if (resultSet.next()) {
String name = resultSet.getString(“name”);
String email = resultSet.getString(“email”);
System.out.println(String.format(“復元対象データ発見: name=%s, email=%s @ %s”,
name, email, targetTime.toString()));
// 現在のデータベースへ書き戻すトランザクションを実行
dbClient.readWriteTransaction().run(transaction -> {
transaction.buffer(Mutation.newInsertOrUpdateBuilder(“Users”)
.set(“userId”).to(userId)
.set(“name”).to(name)
.set(“email”).to(email)
.set(“updatedAt”).to(Value.COMMIT_TIMESTAMP)
.build());
return null;
});
System.out.println(“データのレスキューに成功しました。”);
} else {
System.out.println(“指定されたタイムスタンプ時点でもデータが存在しませんでした。”);
}
}
}
}
このコードの美しさは、本番のライブトラフィックをブロックすることなく、完全に一貫性のある過去の断面を安全に覗き見できる点にある。これがMVCCとTrueTimeの真骨頂だ。
—
4. 堅牢な設計パターンと「パフォーマンス上の罠」
チーフアーキテクトとして、君たちに最も強く伝えなければならないのは「コストとストレージ肥大化のトレードオフ」だ。
PITRは魔法の杖ではない。物理法則(とGoogleのクラウド利用料の請求書)には逆らえない。
罠1:更新頻度の高い(Churn Rateが高い)テーブルでのストレージ爆発
PITRは「過去のバージョンを保持する」仕組みである。したがって、以下のようなワークロードではストレージ使用量が劇的に跳ね上がる。
- 頻繁にUPDATEやDELETEが走るテーブル(例:セッションストア、リアルタイムカウンター、頻繁にステータスが変わるジョブキュー)
- 1日に何度も全件ロード&上書きされるマスタデータ
【設計プラクティス】
- ホットデータの分離: 更新頻度が異常に高いテーブルと、比較的イミュータブルな基幹データを別のデータベース、あるいは別インスタンスに分離せよ。PITRはインスタンス単位ではなく、データベース単位で有効化・無効化の制御、あるいは保持期間(最大7日間の範囲でカスタム可能)の設定を行うべきだ。
- 保持期間の最適化: 「本当に7日間必要か?」をビジネス要件と突き合わせろ。監査要件や法規制で7日必須なら甘受すべきだが、単なる開発ミスへの備えであれば、3日や4日に短縮することでストレージコスト(およびそれに伴うバックグラウンドのGC負荷)を大幅に抑制できる。
罠2:GC遅延とストレージI/Oの競合
PITRを有効にすると、保持期限内の古いバージョンをスキャン・維持するために、ストレージ層でのコンパクション(整理統合)の挙動が変わる。
極端に書き込み負荷(Write QPS)が高いシステムでPITRの保持期間を延ばしすぎると、ストレージのI/OリソースがGCやバージョン管理の維持に消費され、通常の書き込みレイテンシにジッター(揺らぎ)が生じる原因になり得る。
【設計プラクティス】
- ピーク時のWrite QPSが数万件/秒を超える超高負荷システムでは、ストレージのメトリクス(Storage Utilization)を厳しく監視アラートに組み込め。
- PITRを有効にしたデータベースのストレージ容量は、「実データサイズ + (1日あたりの平均書き込み量 × PITR保持日数)」の公式で概算し、キャパシティプランニングに組み込むこと。
—
5. チーフアーキテクトからの総括
Cloud SpannerのPITRは、「万が一の保険」として思考停止でポチる機能ではない。
「Spannerの分散バージョン管理システム(MVCC)を、ビジネスのタイムマシンとしてどう戦略的に組み込むか」という設計思想そのものだ。
- 構造を理解し、
- コストとリスクのバランスを見極め、
- いざという時のリカバリ手順(あるいはタイムトラベルクエリによる自動修復スクリプト)までをデザインに落とし込む。
ここまでやって初めて、「Spannerを使いこなしている」と言える。
次の設計レビューでは、単に「PITRを有効にします」ではなく、「更新頻度とストレージコストの試算に基づき、保持期間を〇日間に設定し、障害時のタイムトラベル復旧フローをこのように定義しました」とドヤ顔で語ってくれ。期待している。
コメント