Cloud Spannerのスプリットポイント:無限スケーラビリティの裏側にある「動的シャーディング」の真実
こんにちは。テクニカルリードの私だ。
きみたちはCloud Spannerを使うとき、「勝手にスケールする魔法のデータベース」として扱っていないだろうか?「とりあえずノード数を増やせば秒間数百万リクエストをさばける」――そう思っているなら、今日のレビューでその認識を根底から覆そう。
Spannerが真に恐ろしいのは、その無限にも思えるスケーラビリティの裏側で、物理的なストレージと演算の境界線をミリ秒単位で動かし続けているという事実だ。今回は、その心臓部である「データシャーディングとスプリットポイント(Split Points)」のアーキテクチャに直球で焦点を当てる。
コードレビューで「なぜそのプライマリキー設計ではホットスポットを踏むのか」「なぜそのクエリはスプリットを跨いでレイテンシが跳ねるのか」を論理的に叩き込めるよう、本質的な知見を授けよう。
—
1. スプリットポイントとは何か?(分散アーキテクチャの根幹)
Cloud Spannerは、単一の巨大なRDBMSではない。それは、何千もの商用Linuxマシン上で協調動作する分散ストレージ・エンジンと、Paxos分散合意アルゴリズムによって束ねられた「マルチプルなPaxosグループの集合体」だ。
Spannerのテーブルデータは、論理的にはプライマリキーでソートされた単一の巨大なツリー構造(LSMツリーベースのストレージ)だが、物理的にはいくつかの「スプリット(Splits)」と呼ばれる単位に分割され、世界中のノード(厳密にはリーダーとフォロワーのレプリカセット)に分散配置されている。
このスプリットの境界線を決定するのが「スプリットポイント(Split Points)」だ。
[ Table: Orders ] (Lexicographically ordered by Primary Key)
+——————————————————————-+
| A0000 – C9999 | D0000 – F9999 | G0000 – M9999 | N0000 – Z9999 | <-- スプリット
+-------------------------------------------------------------------+
| | | |
v v v v
[Split A] [Split B] [Split C] [Split D] <-- Paxosグループ単位で配置
スプリットは静的なものではない。Spannerはバックグラウンドで常にトラフィックとデータ量を監視し、「動的に」スプリットを分割(Split)したり、統合(Merge)したりしている。
—
2. スプリットポイントの決定アルゴリズム:何がトリガーになるのか?
では、Spannerはどのような基準でスプリットポイントを決定し、分割を実行しているのか。ここが実務設計において最も重要なポイントだ。
スプリットの発生トリガーは主に以下の2つ。
1. データサイズ(Volume-based Splitting)
- 単一のスプリットが保持するデータサイズが一定の閾値(一般的に数GB程度)を超えた場合。
2. 負荷(Load-based Splitting)
- データサイズが小さくても、特定のキー範囲に対してCPU使用率やQPS(Read/Writeのトラフィック)が急増し、単一のノード(Paxosグループのリーダー)の処理能力限界に近づいた場合。
負荷分散のメカニズム(ホットスポットへの動的適応)
もし君が `ORDER BY timestamp` のような単調増加するキー(タイムスタンプやAuto Increment的なID)でテーブルを設計したとしよう。
この場合、すべての書き込みは常に「最後のキー(現在の時刻)」に集中する。
Spannerの動的シャーディングは、この状況をどう検知してどう動くか?
1. ホットスポットの検知: スプリットのリーダーノードが、特定の部分キーレンジ(例: `2023-10-27T15:00:00` 付近)でのCPU負荷の高騰を検知する。
2. スプリットポイントの移動・細分化: Spannerの分散トランザクション・コーディネーターは、その高負荷なキーレンジを周囲から切り離すように、新しいスプリットポイントを即座に引く。
3. 負荷の分散(Load Shedding / Sharding): 切り離された高負荷の極小スプリットは、クラスター内の別の(暇な)ノードへとリーダー権限ごとダイナミックに移動させられる。
……と、言葉にすれば美しく聞こえるが、実際の現場ではここに大きな罠がある。 次の「設計上のアンチパターン」でその正体を暴こう。
—
3. 現場で即死するアンチパターン:シーケンシャルキーの悲劇
コードレビューで以下のスキーマを見つけたら、即座に差し戻しを命じてほしい。
— 【アンチパターン】典型的なホットスポット製造機
CREATE TABLE Transactions (
TransactionId INT64 NOT NULL, — アプリケーション側で生成された連番、または単調増加ID
UserId INT64 NOT NULL,
Amount FLOAT64,
CreatedAt TIMESTAMP NOT NULL,
) PRIMARY KEY(TransactionId);
なぜこれがダメなのか?(スプリットの限界)
「いや、Spannerは動的にスプリットを細分化して負荷分散してくれるって言ったじゃないか」と思うかもしれない。
確かにSpannerは賢い。だが、物理の制約には逆らえない。
単調増加するキーに対して書き込みが集中する場合、Spannerはスプリットを細かく切り刻むが、「最新のデータが存在する末尾のスプリット」にしか書き込みのトラフィックが集中するという物理的真実は変えられない。
結果として何が起きるか:
- スプリットは極限まで小さく分割されるが、書き込みのリーダーノードは常に1つ(最新のキーを持つスプリットの担当ノード)に固定される。
- クラスター全体のCPUノードを100ノードにスケールアウトしても、その中のたった1ノードだけがCPU 100%で張り付き、他の99ノードは遊んでいる状態(ホットスポット)になる。
- スプリットの分割・移動のオーバーヘッドが逆にシステム全体のレイテンシを悪化させる。
—
4. 堅牢な設計パターン:ハッシュ分散と複合プライマリキー
では、どう設計すべきか。
Spannerで秒間10万件以上の書き込みをさばくシステムを構築する場合、「書き込みトラフィックを空間的に均一に散らす(Scatter)」プライマリキー設計が必須となる。
対策1: プレフィックスハッシュ(Bit-Reversal / Salt)
キーの先頭にハッシュ値や、ビット反転させた値をプレフィックスとして付与する手法だ。
— 【推奨パターン】ハッシュプレフィックスによる負荷分散
CREATE TABLE Transactions (
— 負荷分散用のシャードID(例: 0〜15のハッシュ値)
ShardId INT64 NOT NULL,
TransactionId INT64 NOT NULL,
UserId INT64 NOT NULL,
Amount FLOAT64,
CreatedAt TIMESTAMP NOT NULL,
) PRIMARY KEY(ShardId, TransactionId);
アプリ側で `ShardId = MurmurHash(UserId) % 16` のように計算してインサートする。
これにより、データは16個以上の異なるスプリットポイントに綺麗に空間分割され、書き込みトラフィックはクラスター内の全ノードへ美しく並列分散される。
対策2: インターリーブ(Interleave)の適切な活用
親子関係にあるデータ(例: `Customers` と `Orders`)において、親のキーを子のプライマリキーのプレフィックスに含めることは、Spannerの基本中の基本だ。
CREATE TABLE Customers (
CustomerId INT64 NOT NULL,
Name STRING(MAX),
) 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;
この設計により、特定の `CustomerId` に紐づく `Orders` は、親である `Customers` のデータと物理的に同一のストレージブロック(スプリット)の極めて近傍に配置される。これにより、親子を結合するクエリのレイテンシが劇的に短縮される。
ただし、ここでも「特定の巨大顧客(テナント)に注文が集中する(マルチテナントのノイジーバイヤー問題)」が発生しうる点には注意が必要だ。
—
5. パフォーマンス・チューニングの極意:スプリット移動(Split Migration)のコストを意識せよ
スプリットが動的に移動するプロセスを、インフラエンジニアの視点で深掘りしよう。
スプリットの移動(あるノードから別のノードへのリーダーの移行、あるいはストレージの再配置)が発生するとき、裏では何が起きているか?
1. Paxosグループのメンバシップ変更: 新しいノード群がそのスプリットのPaxosレプリカとしてアタッチされる。
2. ステートのキャッチアップ: ログとデータの同期が行われる。
3. リーダーの切り替え: トラフィックのルーティングが切り替わる。
この「スプリット移動の最中」は、一時的にレイテンシがミリ秒単位でスパイクすることがある。
大量のバッチ処理を突然流し込んだり、急激なオートスケーリングを行ったりすると、Spanner内部でスプリットの分割と移動が嵐のように発生し、P99レイテンシが悪化する原因になる。
チップス:ウォームアップ(Pre-splitting)
もし事前に「明日、大規模なキャンペーンで1000万件のデータをロードする」ことが分かっている場合、データを一気に流し込むのではなく、事前にスキーマ設計やダミーデータでスプリットを十分に成長させておく(あるいは、キー空間をあらかじめ分散させておく)「ウォームアップ」の思想が、大規模システムを救うこともある。
—
まとめ:チーフアーキテクトからのメッセージ
Cloud Spannerのスプリットポイントと動的シャーディングは、開発者がインフラの複雑性を意識しなくてよくするための「究極の抽象化」だ。
しかし、その抽象化のベールを剥がせば、そこには冷徹な「物理とアルゴリズムの現実」が存在する。
- 単調増加するキーはホットスポットの悪魔を召喚する。
- プライマリキーの設計こそが、スプリットの切り方を支配し、パフォーマンスの9割を決める。
- データベース任せにするな。データの「散らし方」をデザインするのがお前たちの仕事だ。
次のコードレビューでは、プライマリキーの定義を見た瞬間に、頭の中でスプリットポイントがどう割れるかをシミュレーションできるようになっていてほしい。
健闘を祈る。
コメント