【実務・中級編】 MVCCガベージコレクション – Cloud Spanner

Cloud Spanner MVCCガベージコレクションの深層:ストレージ膨張を防ぎ、レイテンシを支配する実務設計

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

  • 「Spannerのストレージコストが想定以上に膨らんでいるんだけど、原因はなんだ?」
  • 「バッチ処理の直後に、なぜかクエリのレイテンシが一時的に跳ね上がるのはなぜだ?」
  • 「古いデータを物理削除するために、DELETE文を大量発行しているのにストレージが減らない」

もし、これらの問いに「分かりません」「インデックスの断片化ですかね?」と答えたとしたら、黄信号だ。
Cloud Spannerのエンジン内部では、MVCC(マルチバージョン同時実行制御)とガベージコレクション(GC)が絶え間なく駆動している。この裏側の物理メカニズムを理解していなければ、大規模システムのコスト最適化も、高スループットなトランザクション設計も、すべて砂上の楼閣となる。

今回は、Spannerのコアアーキテクチャの心臓部である「MVCCガベージコレクション」について、実務で直面する課題と解決のための設計パターンを徹底的に解説しよう。

—

1. 原理:SpannerのMVCCとGCのメカニズム

まず、教科書的な説明は最小限にする。SpannerのMVCCがどう動いているか、ストレージのレイヤーで何が起きているかを正確に把握してほしい。

タイムスタンプとバージョンの物理構造

Cloud Spannerのストレージ(Colossus上のLSMツリーベースの構造)では、データは単なるキー・バリューではない。すべての行は、コミットタイムスタンプ($T_c$)という不可変の次元を持っている。

[Row Key] + [Commit Timestamp] —> [Column Values (or Delete Marker)]

データをUPDATEまたはDELETEすると、古いデータがその場で上書きされるわけではない。新しいタイムスタンプを持った新しいバージョンが書き込まれる(あるいは削除マーカー(Tombstone)が置かれる)。
これにより、過去の任意の時点(最大1時間前まで)におけるスナップショット読み取り(Stale Read)や、実行中の長いトランザクションが一貫性のあるデータを読めるようになっている。

ガベージコレクション(GC)が走る条件

では、不要になった古いバージョンはいつ消えるのか?
ここで重要なのが、SpannerのGCは「アプリケーションが不要と言ったとき」ではなく、「もはやどのトランザクションからも参照される可能性がなくなったとき」にバックグラウンドで物理的に削除されるという点だ。

SpannerのGCを駆動するトリガーは主に以下の2つ。

1. ホールドタイム(Retention Period)の経過:
デフォルトで、Spannerは過去最大1時間分のバージョンを保持する(Stale Readの許容範囲)。したがって、直近1時間以内に書き込まれた古いバージョンは、たとえ最新データに上書きされていても絶対に物理削除されない。
2. リーダーシップとバックグラウンドウォーカー:
各スプリット(Split)のPaxosリーダー上で動作するバックグラウンドプロセスが、定期的に古いタイムスタンプのバージョンをスキャンし、ストレージから物理的にパージ(purge)していく。

—

2. 実務の現場で起きる「罠」とアンチパターン

このメカニズムを理解していないと、次のような深刻なトラブルに見舞われる。

罠1:「大量DELETE=即座のストレージ空き」という誤解

夜間バッチで「不要になった1億件のログデータを`DELETE`した!」と喜んでいないか?
Spannerにおいて、`DELETE`は単なる「削除マーカー(Tombstone)の書き込み」に過ぎない。

  • 削除された瞬間から、そのデータは通常のクエリにはヒットしなくなる。
  • しかし、1時間(Retention Period)が経過し、GCがその領域を物理回収するまで、ストレージ使用量は1バイトも減らない。
  • さらに、インデックスやテーブルの断片化(Fragmentation)が発生し、GCが完了してスペースが再利用可能になるまでの間、スキャン性能が一時的に劣化することさえある。

罠2:ロングラン・トランザクション(Long-running Transaction)によるGCの「窒息」

これが一番恐ろしい。
もし、君たちのアプリケーションやバッチ処理の中で、1時間を超えるような巨大な読み取りトランザクション(Stale Readや強い整合性を持つ長大なスキャン)を実行し続けたらどうなるか?

SpannerのGCは、「現在実行中の最古のトランザクションが参照しているタイムスタンプよりも古いバージョン」をパージすることができない。
つまり、1時間以上かかるバッチが走っている間、そのスプリットのGCは事実上の「停止(フリーズ)」状態に追い込まれる。
結果として何が起きるか?

  • 古いバージョンが山のように蓄積され、ストレージ容量が急増する。
  • GCが追いつかなくなることで、その後の通常の読取/書込パフォーマンス(LSMツリーのコンパクション効率)が劇的に悪化する。

—

3. 堅牢な設計パターンとベストプラクティス

では、我々テクニカルリードはこのGCの挙動を前提に、どうシステムを設計すべきか。具体的なプラクティスを授けよう。

パターンA:大量データの削除は「DELETE文」ではなく「テーブルの再作成/パーティション分割」を検討せよ

もし数千万〜数億単位のデータを日常的にパージする必要があるなら、`DELETE FROM table WHERE …` をループさせるのは最悪の悪手だ。

【推奨アプローチ】タイム・トゥ・リブ(TTL)機能の活用
Cloud SpannerにはネイティブのTTL(Time-to-Live)機能が実装されている。これを使えば、バックグラウンドで安全かつ効率的に、パフォーマンスへの影響を最小限に抑えながら古いデータが削除される。

— テーブル定義の例:created_at から30日経過したレコードを自動削除
CREATE TABLE UserEvents (
UserId INT64,
EventId INT64,
EventData STRING(MAX),
CreatedAt TIMESTAMP,
) PRIMARY KEY(UserId, EventId),
ROW DELETION POLICY (OLDER_INTERVAL_OF(CreatedAt, INTERVAL 30 DAY));

自前でバッチを組んで`DELETE`を発行するよりも、Spannerの内部アーキテクチャに最適化されたネイティブTTLに任せるのが、運用コスト・パフォーマンスの両面で圧倒的に正しい。

パターンB:ロングラン・トランザクションの徹底排除

コードレビューでは、トランザクションの生存期間に厳しく目を光らせろ。

  • 読み取り専用トランザクションであっても、数分、数十分とコネクションを維持するような設計は即座に差し戻しだ。
  • もし大規模なデータエクスポートや分析クエリが必要な場合は、トランザクションを張るのではなく、Cloud Spannerの「Point-in-Time Recovery (PITR)」や「Dataflow等を用いたStale Read(古くなったタイムスタンプを指定した並列読み込み)」を使い、トランザクションログを圧迫しない設計に逃がすこと。

—

4. パフォーマンス上の注意点と監視メトリクス

最後に、運用フェーズで見るべきCloud Monitoringの重要メトリクスを挙げておく。これらをGrafanaやCloud Operationsのダッシュボードに必ず常駐させろ。

1. Storage Bytes Used:
ストレージの物理使用量。バッチ削除の後に「なぜ減らないのか」とパニックになる前に、GCのタイムラグ(最大1時間+α)を考慮に入れたトレンドを見る。
2. Transaction Latency / Commit Latency:
GCが滞り、ストレージのコンパクション(LSMツリーの整理)が追いつかなくなると、書き込みレイテンシがじわじわと悪化する。レイテンシのスパイクを見たら、裏でロングラン・トランザクションや異常な量の書き込み・削除が走っていないか疑え。

—

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

Cloud SpannerのMVCCとガベージコレクションは、マジックではない。物理的な制約と、分散合意(Paxos)に基づく厳密な時間管理の上で動いている機械だ。

「データを消したのにストレージが減らない」
「なぜこのバッチはレイテンシの波が大きいのか」

その背後には必ず、このMVCC GCの呼吸がある。
アーキテクチャの底流にある物理法則を理解した者だけが、真にスケールするロバストな分散システムを組み上げることができる。次の設計レビューでは、ぜひこの視点をチームに持ち込んでほしい。

コメント

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