Cloud Spannerの核心:タブレット管理の物理学と、分散データベースを限界まで使い倒す設計論
こんにちは。テックリードの私だ。
今日のコードレビュー、あるいはアーキテクチャレビューで、「とりあえずSpanner使っとけばスケールするんでしょ?」という甘い認識に遭遇して頭を抱えていないか?
Cloud Spannerは魔法のブラックボックスではない。背後で何万という「タブレット(Tablet)」が呼吸し、分裂し、ノード間をダイナミックに移動している分散ストレージの巨大なエコシステムだ。この物理的実体を理解せずして、真にスケーラブルなスキーマ設計などあり得ない。
今回は、Spannerのコアアーキテクチャの心臓部である「タブレット管理とライフサイクル」について、実務の現場で即座に使える知見を叩き込む。教科書的な公式ドキュメントの斜め上を行く、修羅場をくぐり抜けた者たちの知見を共有しよう。
—
1. タブレットとは何か:概念と物理的実体のギャップ
まず言葉の定義を正す。Spannerにおける「タブレット(Tablet)」は、RDBの「テーブル」とは全く別物だ。
- 論理的には: キー範囲(Key Range)によってソートされた行の集合。`[StartKey, EndKey)` のバイト列のレンジで切り出される。
- 物理的には: Googleの分散ファイルシステム(Colossus)上に置かれたGFSファイルのセットであり、それを操作するPaxosグループのインスタンスそのものだ。
Spannerのテーブル設計において、すべてのデータは主キーの昇順で物理的に並ぶ。そして、この巨大なキー空間が自動的に一定サイズ(数百MB〜数GB)ごとに「タブレット」という単位に切り刻まれる。
[Key Space] ========================================================>
| Tablet A | Tablet B | Tablet C | …
| (Node 1担当) | (Node 2担当) | (Node 1担当) |
========================================================
ここで重要なのは、「1つのテーブルが1つのタブレットに収まるとは限らないし、1つのタブレットに複数テーブルのデータ(Interleave構造)が混ざり合うこともある」という点だ。
—
2. タブレットのライフサイクルと動的再配置の裏側
タブレットは静的な存在ではない。トラフィックやデータ量の変化に応じて、絶えず生まれ、分裂し、移動し、消滅している。このライフサイクルを支配しているのが、バックグラウンドで稼働するSplitterとLoad Balancerだ。
① スプリッティング(分裂)
あるタブレットに書き込みが集中し、サイズが閾値(通常数GB)を超えた瞬間、あるいはホットスポット化した瞬間、Spannerはそれを2つのタブレットに物理的に分割する。
この分裂は無停止(Zero-downtime)で行われる。キー空間の境界をアトミックに引き直し、新しいPaxosグループをスピンアップする。
② マイグレーション(移動とロードバランス)
スプリットされたタブレットや、特定ノードに負荷(CPU使用率やQPS)が偏った場合、Load Balancerはノード間でタブレットを移動させる。
ここで感動的なのは、タブレットの移動が「データのコピー」ではなく「Paxosグループのリーダー権の移譲と、ストレージ(Colossus)の参照先の切り替え」に近い形で行われる点だ。巨大な実データをごっそりネットワーク経由でコピーするわけではないため、移動コストは驚異的に低い。
—
3. 現場で頻発するアンチパターン:なぜあなたのSpannerはスケーリングしないのか?
このタブレット管理のメカニズムを知っていれば、現場でよくある以下の障害の原因が手に取るようにわかるはずだ。
🚨 アンチパターン1:単調増加キーによる「ホットタブレット」
タイムスタンプやインクリメンタルなシーケンスIDを主キーの先頭(第一キー)に置いていないか?
— 最悪な設計例:すべての新規挿入がキー空間の「最末端」に集中する
CREATE TABLE Events (
EventId INT64 NOT NULL, — タイムスタンプや連番だと仮定
UserId INT64 NOT NULL,
Payload STRING(MAX),
) PRIMARY KEY(EventId);
何が起きるか?
新しいデータはすべて「現在の最大キー」を持つタブレットにしか書き込まれない。つまり、システム全体で何百ノード用意していようが、たった1つのタブレット(=たった1つのPaxosグループ=たった1つのリーダーノード)にすべての書き込みが集中する。 スプリットが追いつかず、CPU使用率が100%に張り付いてレイテンシが爆発する典型的な「ホットスポット障害」だ。
💡 堅牢な設計パターン:ハッシュ化によるキーの分散
もし連番や時系列データを主キーにする必要があるなら、プレフィックスにハッシュを噛ませるか、ビット反転(Bit-reversal)させてキー空間全体に書き込みを「散らす」べきだ。
— 改善された設計例:ハッシュプレフィックスでキー空間全体に負荷を分散
CREATE TABLE Events (
ShardId INT64 NOT NULL, — 0〜15程度のハッシュ値
EventId INT64 NOT NULL,
UserId INT64 NOT NULL,
Payload STRING(MAX),
) PRIMARY KEY(ShardId, EventId);
これにより、書き込みは15個の異なるタブレット(異なるノード群)に並列分散され、Spannerの真の水平スケールが解放される。
—
4. インターリーブ(Interleave)とタブレットの局所性
リレーショナルデータベースとしてのSpannerの真骨頂が Interleaved Tables(インターリーブテーブル) だ。親テーブルと子テーブルのレコードを、物理的に同じタブレット(または隣接するタブレット)に「共存」させる。
— 親テーブル
CREATE TABLE Customers (
CustomerId INT64 NOT NULL,
Name STRING(1024),
) PRIMARY KEY(CustomerId);
— 子テーブル:親のタブレットの中に物理的に埋め込まれる
CREATE TABLE Orders (
CustomerId INT64 NOT NULL,
OrderId INT64 NOT NULL,
OrderDate DATE,
) PRIMARY KEY(CustomerId, OrderId),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;
アーキテクチャ上のメリット
- 物理的局所性(Locality): `Customers` の行と、それに紐づく `Orders` の行は、物理的に同じストレージ領域に近く配置される。
- 高速なJOIN: 内部結合の際、ネットワークを跨いだシャッフル処理が最小限に抑えられ、タブレット内でのローカルなスキャンで完結するため、レイテンシが劇的に改善する。
⚠️ レビュー時の注意点:
子側のデータ量が爆発的に増える場合、親子をまとめたタブレット全体のサイズ肥大化を招く。子テーブルのデータ増加トレンドを見越したキー設計と、タブレット分割のシミュレーションを怠るな。
—
5. チーフアーキテクトからの実践的提言
Cloud Spannerを使いこなすための鉄則を最後にまとめておく。設計レビューではこれらを厳しくチェックしてほしい。
1. 「シーケンシャルなキー」を排除せよ
UUIDv4、またはハッシュ化されたプレフィックスを使用し、書き込みが特定のタブレットに偏らない(トースティングされない)構造を作れ。
2. スキーマ変更(DDL)のコストを意識せよ
巨大なテーブルに対するインデックス追加やカラム追加は、バックグラウンドでタブレット群を走査する。ピークタイムを避け、段階的に実行する計画を立てろ。
3. モニタリングをサボるな
Cloud Monitoringで `CPU Utilization` や `Tablet Count` のメトリクスを監視しろ。特定のノードだけCPUが高い場合、それは「タブレットの偏り(ホットスポット)」が発生している動かぬ証拠だ。ロードバランサーが再配置を完了するまで耐えられる設計になっているか、常に自問せよ。
Spannerは、その下層にある物理レイヤー(タブレットとPaxos)の挙動と対話しながら設計して初めて、真の怪物的なパフォーマンスを発揮する。
次の設計では、この「タブレットの動的挙動」を頭の中に描きながらスキーマを引いてほしい。健闘を祈る。
コメント