スキーマレスではない、だからこそ尊い:Cloud Spannerの「無停止DDL」を支えるPaxosと分散合意の深淵
テックリードの君なら、一度は絶望したことがあるはずだ。
「本番環境でのカラム追加。ロック競合によるテーブル全体への波及、そしてサービス停止。夜間バッチのウィンドウをハラハラしながら見つめるあの瞬間……」
現代の分散データベースにおいて、可用性を1秒たりとも落とさずにスキーマを書き換えることは、聖杯(Holy Grail)のようなものだ。多くのRDBがメンテナンスウィンドウを要求し、NoSQLや一部の分散SQLが「スキーマレス」という名の責任逃れでこの課題を回避する中、Cloud Spannerは違う。ACIDトランザクションの厳格さと強一貫性を保ったまま、オンラインでのDDL(Data Definition Language)実行をやってのける。
だが、甘く見てはいけない。「無停止で裏で勝手にやってくれる魔法の機能」など、分散システムの世界には存在しない。Spannerがどうやって数千ノードにまたがるグローバル規模のクラスタで、データの整合性を一瞬たりとも狂わせずにスキーマを更新しているのか。その内部メカニズムを、コードレビューや設計レビューの場で後輩に語るようなロジカルさで紐解いていこう。
—
1. そもそもDDLとは「分散システムへの宣戦布告」である
単体のMySQLやPostgreSQLであれば、DDLはカタログテーブルに排他ロックをかけ、スキーマ定義を書き換えればそれで終わりだ。しかし、Cloud Spannerのデータは、地理的に分散した数々の「スプリット(Split)」という単位に分割され、それぞれがPaxosグループによってレプリケートされている。
例えば、グローバルに展開する `users` テーブルに対して `ALTER TABLE users ADD COLUMN loyalty_tier INT64;` を叩いたとする。
この時、世界中にちらばる何百、何千ものスプリット(リーダー)が一斉に「今日から新しいカラムが生えました」と認識しなければ、システム全体でデータの解釈が崩壊する。あるノードは新カラムを書き込み、別のノードはそんなカラム存在しないとエラーを吐く、なんて地獄絵図は絶対に防がなければならない。
ここで登場するのが、Spannerの神髄である 「スキーマバージョン(Schema Version)」 と 「分散合意(Paxos Consensus)」 だ。
—
2. Paxosを用いたスキーマバージョンの合意形成プロセス
Cloud Spannerのスキーマ変更は、単なるメタデータの書き換えではない。それは 「メタデータに対するPaxosトランザクション」 そのものである。
Spannerの内部アーキテクチャにおいて、スキーマ定義自体が一つの特殊なメタデータとして管理されており、すべてのスプリットやトランザクションは「現在のスキーマバージョン」を参照しながら動作している。
DDLが発行されたとき、内部で何が起きているのか。ステップ・バイ・ステップで分解しよう。
ステップ1: メタデータPaxosグループへの提案(Proposal)
ユーザーが `ALTER TABLE` を発行すると、リクエストはメタデータを管理するルートのPaxosグループに送られる。ここで、新しいスキーマ定義と「アクティベーションタイムスタンプ(いつからこのスキーマを有効にするか)」を含むトランザクションが、Paxosの合意形成アルゴリズムにかけられる。
ステップ2: 全スプリットへの非同期伝播
ルートグループでスキーマのバージョンアップが合意されると、その変更はクラスタ内のすべてのスプリット(データPaxosグループ)に向けて、バックグラウンドで伝播される。
ここで重要なのは、「全ノードが一瞬で同時に書き換わるわけではない」 という現実だ。分散ネットワークにおいて、完全な同時など存在しない。だからこそ、Spannerは 「マルチバージョン(Multi-version / MVCC)」 の仕組みを利用する。
—
3. 段階的適用(Phased Rollout)のメカニズム
スキーマの更新は、以下の厳密なフェーズを経て非同期かつ安全に行われる。
1. Preparing Phase(準備フェーズ):
各スプリットは新しいスキーマバージョンを事前にダウンロードするが、まだ適用はしない。古いスキーマと新しいスキーマが共存できる状態を作る。
2. Commit Wait(コミット待機):
Spannerの分散トランザクションの肝であるTrueTime APIを利用し、「過去のタイムスタンプを持つトランザクションが完全に枯渇した」と確実に断言できるだけの時間(数秒〜十数秒)をあける。これにより、古いスキーマを参照している未完了のトランザクションが新しいスキーマと衝突して矛盾を起こすのを完全に防ぐ。
3. Activation Phase(有効化フェーズ):
指定されたタイムスタンプ以降、すべての新規トランザクションは新しいスキーマバージョンを強制的に使用する。
この仕組みがあるからこそ、数ペタバイトのデータを持つ巨大なテーブルであっても、テーブルロックを取ることなく、裏側で静かに、しかし確実に対応するスキーマへと移行できるのだ。
—
4. 設計レビューで防ぐべき「DDLのアンチパターン」
さて、ここまでSpannerの圧倒的なエンジニアリングを見てきたが、「システムが無停止でDDLを処理できる=どんな雑にDDLを書いてもノーペナルティ」という意味では決してない。
実務の設計レビューにおいて、以下のアンチパターンを持ち込む開発者がいたら、チーフアーキテクトとして即座に差し戻してほしい。
アンチパターンA: 大規模テーブルに対する連続的なスキーマ変更
「とりあえずカラムを追加して、やっぱりこっちも追加して…」と、短い間隔で複数の `ALTER TABLE` を連続発行する行為。
SpannerのDDLは裏でPaxosの合意形成とバックグラウンドでのスプリット更新を走らせるため、連続して発行するとメタデータ層でキューイングや競合が発生し、レイテンシの悪化やエラー(`DEADLINE_EXCEEDED` や `ABORTED`)を招く。
> 【正しい設計アプローチ】
> スキーマ変更は「一つのトランザクション(あるいは一連のまとまったオペレーション)」として計画し、必要なカラムやインデックスの追加は、十分にテストした上で一度のDDL文、または適切なインターバルを空けたバッチとして実行すること。
アンチパターンB: インデックス追加直後の高負荷書き込み
新規にインデックスを追加した直後のテーブルに対して、大量の書き込みを流し込むケース。
インデックスのバックフィル(既存データのインデックス構築)はバックグラウンドで非同期に行われるが、インデックスが完全に「Ready」状態になるまでの間、システムのリソース(CPU/ストレージ)は消費される。
> 【正しい設計アプローチ】
> インデックスを追加する場合は、トラフィックが穏やかな時間帯を選ぶか、インデックスの構築完了(メタデータ上でアクティブになること)を確認してから本格的なデータ投入・クエリ実行を行うフローを構築する。
—
5. まとめ:分散システムの底力を信頼せよ
Cloud Spannerのスキーマ更新プロセスは、分散合意(Paxos)と正確な時間同期(TrueTime)、そしてMVCCが完璧に調跟した芸術品だ。
「オンラインDDLでサービスを止めたくない」
その願いを、単なる力技ではなく、数学的・アルゴリズム的な裏付けをもって実現しているのがCloud Spannerである。
アーキテクトである我々は、この強靭な基盤の上でシステムを構築しているという事実を誇りに思うべきだ。だが同時に、その内部メカニズムの制約(非同期伝播のタイムラグやメタデータの負荷)を正しく理解し、リスペクトした上でスキーマ設計を行わなければならない。
次の設計レビューで、誰かが安易に「ALTERしとけば裏で勝手に動くんでしょ?」と言ってきたら、こう言い返してやってほしい。
「おい、今裏で何個のPaxosグループが合意形成のために動いていると思っているんだ?」 と。
コメント