—
Spannerの深淵:ノード数とPaxosグループのダイナミクス
「Cloud Spannerは、ノード数を増やせばリニアにスケールする」
あなたがもし、この言葉を言葉通りにしか理解していないとしたら、大規模システムにおいてSpannerの真のポテンシャルを引き出すことはおろか、本番環境で予期せぬレイテンシスパイクに頭を抱えることになるだろう。
Spannerは魔法ではない。極めて厳密な分散アルゴリズムと、洗練されたアーキテクチャの結晶だ。ノード数(またはProcessing Unit: PU)を増減させるという行為は、単に「仮想マシンのスペックを上げる」ような単純な話ではない。それは、裏側で蠢く何千ものPaxosグループのトポロジーを再構成し、データの担当範囲(Split)をダイナミックに再配置する一大イベントなのだ。
本稿では、Spannerのチーフアーキテクトの視点から、ノード数や処理能力(PU)の変更が、内部のリソース割り当てとPaxosグループの配置にどのような影響を与えるのか、その「極限の知見」を共有する。
—
1. コアアーキテクチャ:コンピュートとストレージの「完全分離」
Spannerの挙動を理解する大前提として、コンピュート(計算資源)とストレージ(データ実体)の完全な分離を理解しなければならない。
+—————————————————————–+
| Client Request |
+—————————————————————–+
|
v
+—————————————————————–+
| [Compute Layer] Spanner Tasks (Nodes / PUs) |
| – Serves Paxos Leaders/Followers |
| – Executes Queries, Coordinates Transactions |
| – Node Scale-up/down happens here |
+—————————————————————–+
| | |
(gRPC) (gRPC) (gRPC)
v v v
+—————————————————————–+
| [Storage Layer] Colossus (Distributed File System) |
| – Stores SSTables (Data & Logs) |
| – Decoupled from Compute Nodes |
+—————————————————————–+
私たちが「ノード」や「Processing Unit (PU)」としてプロビジョニングしているのは、上図の Compute Layer(Spanner Task) である。
一方、データそのものは Google の超堅牢な分散ファイルシステムである Colossus に、暗号化されたSSTableとして格納されている。
ノードを増やすとは、Colossusからデータをコピーすることではない。「データをサービングするコンピュートリソース(Spanner Task)を増やし、担当するSplit(データの分割単位)の割り当てを再シャッフルすること」を意味する。
Split と Paxos グループの関係
Spannerのデータは、主キーの範囲(Range)に基づいて Split と呼ばれる単位に自動分割される。
そして、それぞれのSplitは、地理的に分散された複数のレプリカ間で一貫性を保つため、個別の Paxosグループ を構成する。
- 1 Split = 1 Paxos Group
- 各レプリカ(Leader、Follower、Read-Only)は、それぞれ異なるゾーンのCompute Node上で動作するSpanner Taskに割り当てられる。
—
2. ノード変更時に内部で起きていること:ダイナミック・リバランシングの真実
インスタンスのノード数を `3` から `10` にスケールアップしたとしよう。管理コンソール上では数分で完了するように見えるが、その裏側では壮絶なメタデータの再配置(ダイナミック・リバランシング)が行われている。
内部ステップのタイムライン
1. コンピュートタスクの起動:
新しいSpanner Task(コンピュートインスタンス)がクラスターに投入される。
2. ロケーションプロキシへの通知:
新ノードの存在がグローバルなディレクトリサービス(Location Proxy)に登録される。
3. Splitの分散配置(Load Rebalancing):
既存のノードで稼働していた多数のPaxosレプリカ(特に書き込み負荷の高いPaxos Leaderや、読み取り負荷の高いFollower)が、新ノードへ「担当替え」される。
ここで最も重要なのは、「データの物理的なコピーは発生しない」という点だ。データはColossusにある。発生するのは、「どのSpanner Taskが、どのColossus上のファイルを読み書きするか」という所有権(メタデータ)の移転である。
【リバランシングのイメージ】
[Node A (Heavy Load)] [Node B (New Node)]
- Split 1 (Paxos Leader) ========> – Split 1 (Paxos Leader) Ownership Transfer
- Split 2 (Paxos Follower) (Reads Colossus metadata)
- Split 3 (Paxos Follower)
なぜノード追加直後にレイテンシが一時的に跳ねるのか?
「負荷が上がったからノードを追加したのに、逆にレイテンシが悪化した」という経験はないだろうか。
これはSpannerの設計ミスではない。以下のメカニズムによる必然的な挙動だ。
1. メタデータのハンドオーバー:
Paxos Leaderの移行時、一瞬だけそのSplitに対する書き込みがブロックされ、新しいLeaderへのリース(Lease)の移転が完了するのを待つ。
2. ローカルキャッシュの冷え(Cold Cache):
新ノードのメモリ上には、担当することになったSplitのデータ(データブロックキャッシュ)が存在しない。そのため、移転直後のクエリはColossusへの物理I/O(リモートリード)を強制され、メモリヒット率が低下する。
現場へのプロのアドバイス
> 「本番環境での急激なスケールアップ・ダウンは避けること。スパイクが予想される数時間前に、段階的(1回につき最大2倍程度まで)にスケールアウトさせ、キャッシュを馴染ませる時間を確保せよ」
—
3. Processing Unit (PU) の罠:100PU と 1000PU(1ノード)の決定的な違い
Spannerでは、1ノード未満の細かい単位である Processing Unit (PU)(100 PU 単位、1000 PU = 1ノード)でのスケールが可能だ。コスト最適化の観点から非常に優れた機能だが、アーキテクトとしてはその「物理的なリソース制限」を冷徹に見極める必要がある。
リソース割り当ての現実
| プロビジョニング単位 | 内部的なコンピュート共有度 | 割り当てられるPaxosグループ数(目安) | 限界レイテンシ特性 |
| :— | :— | :— | :— |
| 100 PU | 複数顧客/別インスタンスと物理マシンを共有 | 非常に制限される | CPU割り当ての競合(Steal Time)により、スパイクが発生しやすい |
| 1000 PU (1 Node) | 物理マシン(または専用コンテナ)を占有 | 制限なし(ノードの処理限界まで) | 非常に安定。CPUクォータのノイズから解放される |
100 PU で運用する場合、Spannerは内部的に1ノード分のリソースを10分割した「極小のタスク」として動作する。
この状態では、Paxosのハートビートや合意形成のオーバーヘッド自体が、割り当てられたわずかなCPUリソースを食いつぶす割合が高くなる。
鉄則:プロダクション環境におけるPUの選択
- 開発・テスト・検証環境: `100 PU` 〜 `500 PU` で十分。
- プロダクション(本番)環境: 最低でも `1000 PU (1ノード)` 以上を強く推奨する。 1ノード未満のプロダクション運用は、一時的なクエリ増加やスキーマ変更(DDL)による負荷で、Paxosグループの合意形成が遅延し、システム全体がストールするリスクを孕む。
—
4. 実戦設計パターン:Paxosグループのトポロジーを支配する
Spannerの内部動作をハックし、パフォーマンスを極限まで高めるための設計パターンを2つ伝授する。
パターン1:インターリーブ(Table Interleaving)によるコロケーション
Spannerで最も強力なスキーマ設計が「インターリーブ」だ。これは、親子関係にあるテーブルのデータを、物理的に同じSplit(=同じPaxosグループ)に配置することを保証する。
— 親テーブル:顧客
CREATE TABLE Customers (
CustomerID STRING(36) NOT NULL,
Name STRING(100),
) PRIMARY KEY (CustomerID);
— 子テーブル:注文(Customersにインターリーブ)
CREATE TABLE Orders (
CustomerID STRING(36) NOT NULL,
OrderID STRING(36) NOT NULL,
OrderDate TIMESTAMP,
) PRIMARY KEY (CustomerID, OrderID),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;
この設計がPaxosに与える影響:
- `CustomerID = ‘usr_123’` に関連する `Customers` のレコードと `Orders` の全レコードは、全く同じSplit、すなわち同じPaxosグループ(同じ物理ノード)に配置される。
- これにより、顧客とその注文情報を跨ぐトランザクションを実行する際、2相コミット(2PC: Two-Phase Commit)が不要になり、単一のPaxosグループ内での1相コミットで完結する。書き込みレイテンシは劇的に低下する。
パターン2:ハッシュ化主キーによる Split 均等分散
連続する値(例:AUTO_INCREMENTのIDや、ミリ秒精度のタイムスタンプ)を主キーの先頭に持ってくると、特定のSplitに書き込みが集中し、そのSplitを担当する特定のノードだけが過熱する(ホットスポット)。
これを防ぐため、主キーは必ず分散される値(UUIDv4や、自然キーのSHA256ハッシュのプレフィックスなど)にする。
— 推奨:UUIDv4またはハッシュ値などを主キーにする
CREATE TABLE DeviceMetrics (
DeviceIDHash STRING(8) NOT NULL, — DeviceIDのハッシュ先頭数文字
DeviceID STRING(36) NOT NULL,
LogTimestamp TIMESTAMP NOT NULL,
Payload BYTES(MAX)
) PRIMARY KEY (DeviceIDHash, DeviceID, LogTimestamp DESC);
なぜこれが効くのか?
主キーの先頭がランダムに分散されることで、SpannerのSplitマネージャーはデータを全ノードに均等に割り振ることができる。
ノード数を3から10に増やした際、すべてのノードが均等にPaxos Leaderの役割を分担できるため、ハードウェアのスケールアップ効果が100%発揮される。
—
5. アンチパターン:本番環境で絶対にやってはいけないこと
1. DDL(スキーマ変更)実行中のノード急減
SpannerのDDL変更は、バックグラウンドで全Splitのメタデータを書き換えるヘビーな処理だ。
DDLの実行中にコスト削減のためにノード数を減らすと、残されたノードにPaxosの再配置とDDL処理の負荷が一気に集中し、データベース全体が応答不能(タスクタイムアウト)に陥る。
2. 負荷に応じた秒単位でのオートスケーリング
Spannerのノード追加は高速だが、前述の通り「キャッシュの温まり」や「メタデータのハンドオーバー」には数分から数十分の時間を要する。
KubernetesのHPA(Horizontal Pod Autoscaler)感覚で、「CPU使用率が上がったから1分後にノードを追加し、下がったらすぐ減らす」というような超短期のオートスケーリングを行うと、常にリバランシングのオーバーヘッドが発生し、システムは自滅する。
スケーリングの閾値は、最低でも20〜30分以上のスパンで評価するように設計すべきだ。
—
まとめ:アーキテクトとしてSpannerと対話せよ
Cloud Spannerは、開発者に「無限のスケール」という夢を見せてくれる。しかし、その夢を支えているのは、Colossusという冷徹なストレージと、Paxosという厳密な分散合意プロトコル、そして日々変化する負荷を監視してSplitを最適配置し続ける「ダイナミック・リバランシング」という泥臭い制御システムだ。
ノード数を決める時、スキーマを設計する時、頭の中で以下の問いを常に投げかけてほしい。
- 「この設計は、Paxosグループを綺麗に均等分散できるか?」
- 「このトランザクションは、2相コミットを誘発していないか?」
- 「今増やそうとしているノードは、裏側のSplitたちに歓迎されているか?」
これが理解できて初めて、あなたはSpannerを「真に手懐けた」と言えるのだ。
コメント