【Cloud Spannerの核心】スプリットとマージの自動化ロジック:真の水平スケーリングを支配する物理と論理
こんにちは。テクニカルリードの私だ。
今日のコードレビュー、あるいはアーキテクチャ設計レビューで、こんな質問を受けたことはないか?
「先生、Cloud Spannerって『無限にスケールする』って言うけど、大量データをインサートし始めた途端に特定のノードが悲鳴を上げたり、逆にスカスカのテーブルがリソースを食ったりしないんですか?」
非常に良い着眼点だ。
この質問が出る時点で、そのエンジニアは単なる「使い方を知っている人」から「分散システムの物理限界に挑むアーキテクト」への階段を登り始めている。
世の中の入門書や公式ドキュメントは、「Cloud Spannerは自動で負荷分散します」と優しく教えてくれる。だが、プロの現場で生きる我々が知りたいのはそこじゃない。「内部のダイナミクスがどう働き、我々のスキーマ設計やクエリがどうそれに干渉するか」という、生々しいリアリティだ。
今日は、Cloud Spannerのコアアーキテクチャの心臓部である「スプリット(分割)とマージ(統合)の自動化ロジック」について、一切の妥協を排して解説しよう。
—
1. スプリットとマージの正体:スプリットプレーンとタブレットの物理
まず前提を合わせよう。Cloud Spannerのデータは、リレーショナルなテーブルとして見えているが、物理層では「Split(スプリット)」と呼ばれる連続したキー範囲の断片に分割されて管理されている。
各スプリットは、Paxosグループによって複数のトランスポート層(Spannerのストレージノード)にレプリケートされ、可用性と一貫性を担保している。
スプリットが発生する2大トリガー
Spannerの分散コーディネーターは、常に以下の2つのメトリクスを監視し続けている。
1. データサイズ(容量):
単一のスプリットが保持するデータサイズが、おおむね 数GB〜数十GB(内部的な閾値) を超えたとき。
2. 負荷(スループット・CPU使用率):
特定のキー範囲に対するQPS(Query Per Second)やCPU消費が跳ね上がり、単一のPaxosグループの処理限界に近づいたとき。
サイズが小さくても、「秒間数万回のホットな書き込み」が集中すれば、Spannerは即座にそのキー範囲を切り出し、別のノードへマイグレーションする。これが「ホットスポットの動的解消」の正体だ。
—
2. スプリットの裏側で起きていること:連続性と境界値のトリック
スプリットが行われる瞬間、システム内部では何が起きているのか?
[ 元の大きなスプリット: Key 0000 〜 Key 9999 ]
↓ (負荷/容量の閾値突破)
[ 分割されたスプリット A: Key 0000 〜 Key 4999 ] ⇄ [ 分割されたスプリット B: Key 5000 〜 Key 9999 ]
↓ (必要に応じて別ノードへ配置転換)
ここで重要なのは、スプリットは「行(Row)」単位ではなく「バイト列(キーの辞書順)」単位でアトミックに切られるという点だ。
もし、君のテーブルのプライマリーキーが以下のような設計になっていたらどうなるか?
— 【アンチパターン例】インクリメンタルIDや現在時刻を先頭に置いた設計
CREATE TABLE AccessLogs (
LogId INT64 NOT NULL, — 採番システム等で単調増加するID
AccessTime TIMESTAMP,
Payload STRING(MAX),
) PRIMARY KEY(LogId);
この設計では、すべての新規インサートが「キー空間の末尾(最大のLogId)」に集中する。
Spannerは優秀だから、末尾が重くなればその部分をスプリットしようとする。しかし、「次に挿入されるキー」が常に新しいスプリットの境界の先へ先へと移動し続けるため、スプリットが追いつかないか、あるいは細切れになったスプリットが次々と作られては片側に負荷が張り付くという「スプリットの暴走(Split Storm)」を引き起こす。
これが、単調増加キーが「Spannerのキラーアンチパターン」とされる物理的な理由だ。
—
3. マージ(統合)の哀しみ:なぜ「縮小」は難しいのか
データが増えればスプリットする。では、データが消えたり(DELETE)、負荷が下がったりしたらどうなるか?
当然、システムは「マージ(統合)」を試みる。隣接するスプリットの合計サイズや負荷が閾値を下回れば、それらは1つに統合される。
しかし、実務においてこの「マージロジック」には注意が必要だ。
- 断片化したデータの残響:
大量のDELETEを実行した後、テーブルの論理サイズはゼロになったとしても、物理的なスプリットの境界やストレージの断片化がすぐに綺麗に消え去るわけではない。
- マージの遅延:
スプリットの生成(スケールアウト)はミリ秒単位の俊敏さで行われるが、マージ(スケールイン)はリソースの揺らぎを防ぐために、より保守的な(慎重な)アルゴリズムで動作する。
そのため、「一時的に数億件入れて全部消した」というようなバッチ処理の後、謎の小規模スプリットが散らばった状態になり、オプティマイザの挙動に微小な影響を与えることがある。
—
4. プロが実践する「スプリットを味方につける」スキーマ設計パターン
では、我々テクニカルリードはこの自動スプリット・マージ機構をどうハックすべきか。結論は一つだ。「最初から均等に負荷とキーが散らばるようにプレースホルダーを制御する」こと。
パターンA:UUID v4 + バイト反転(Bit-Reversal)
単調増加を避けつつ、完全なランダムでもなく、かつ特定のプレフィックスに偏らせない手法として、UUIDやハッシュの利用がある。特にSpannerのベストプラクティスとして推奨されるのが、バイト反転(Bit-Reversal)を用いたID生成だ。
— 【推奨設計】インターリーブやハッシュプレフィックス、あるいはBit-reversed sequenceの活用
CREATE TABLE Users (
— 下位ビットの変動を上位に持ってくることで、キー空間全体にインサートを分散させる
UserId INT64 NOT NULL,
UserName STRING(100),
CreatedAt TIMESTAMP,
) PRIMARY KEY(UserId);
これにより、新規の書き込みがキー空間の全域(0x00…から0xFF…まで)に均等に散らばる。結果として、Spannerの自動スプリット機構はすべてのノードへ綺麗に負荷を分散させ、限界知らずの水平スケーリングが完遂される。
パターンB:ホットスポットをあえて許容し、事後スプリットに備える設計
どうしても時系列データを扱いたい場合(例:IoTデバイスのセンサーログ)、プレフィックスにハッシュを噛ませるのが定石だ。
CREATE TABLE SensorLogs (
— ハッシュ化されたプレフィックス(例: 0〜15のMOD)を先頭に置く
ShardId INT64 NOT NULL,
DeviceID STRING(64) NOT NULL,
Timestamp TIMESTAMP NOT NULL,
MetricValue FLOAT64,
) PRIMARY KEY(ShardId, DeviceID, Timestamp);
この設計であれば、`ShardId`(0〜15)によって物理的なスプリットが最初から16個以上に強制分割され、それぞれのシャードが独立したPaxosグループとしてスケールする。
「システムに勝手にスプリットさせる」のではなく、「こちら側からスプリットの境界をあらかじめデザインしてやる」。これがプロの設計だ。
—
5. チーフアーキテクトからの最終提言
Cloud Spannerのスプリットとマージの自動化ロジックは、神のアルゴリズムではない。あくまで「物理的な限界を隠蔽するための洗練されたヒューリスティクス」に過ぎない。
コードレビューや設計レビューの場で、もしチームメンバーが次のような設計を持ってきたら、即座に差し戻しを命じてほしい。
1. 「プライマリーキーに AUTO_INCREMENT 的な連番を使っています」 ⇒ 論外。今すぐUUIDかBit-reversed sequenceに変えさせろ。
2. 「特定のマスターデータテーブルに、全トランザクションから秒間1万回のUPDATEが集中します」 ⇒ 行ロックの競合と単一スプリットのCPU飽和が起きる。キーのシャーディング(分散)を再設計させろ。
Spannerを使いこなすとは、Spannerの内部エンジンの「息継ぎ(スプリットのタイミング)」を理解し、その波に自らのデータとクエリを美しく乗せることに他ならない。
さあ、今日のレビューに戻るとしよう。君たちの手で、美しい分散システムを組み上げてくれ。健闘を祈る。
コメント