Cloud Spannerホットスポット検出の深層:スプリットの物理学と「詰み」のないスキーマ設計
こんにちは。テックリードの私だ。
今日のコードレビュー、あるいはアーキテクチャ設計レビューで、こんなテーブル定義を見て思わず頭を抱えたことはないか?
— 良くある「思考停止」のオートインクリメント型プライマリキー
CREATE TABLE Transactions (
TransactionID INT64 NOT NULL,
UserID STRING(64),
Amount INT64,
CreatedAt TIMESTAMP,
) PRIMARY KEY(TransactionID);
一見、何の問題もない美しいRDBのテーブル定義に見えるだろう。だが、これをCloud Spanner上で動かした瞬間、君のシステムは数百万トランザクションの負荷に耐えきれず、特定のノードがメルトダウンを起こす。いわゆる「ホットスポット(Hotspot)」の完成だ。
Spannerは「無限にスケールするRDB」として喧伝されているが、それは物理法則を無視してスケールする魔法の箱ではない。Spannerの内部動作、特に「スプリットの動的分割アルゴリズム」と「キー範囲の物理配置」を理解していないエンジニアが書いたコードは、必ず本番環境で牙をむく。
今日は、Cloud Spannerのコアアーキテクチャの心臓部である「ホットスポット検出とスプリット分散のメカニズム」を解体し、実務で絶対に踏み抜いてはいけない設計パターンを伝授する。覚悟してついてきてほしい。
—
1. Spannerの裏側:なぜ単調増加キーは「詰む」のか?
Cloud Spannerのデータは、プライマリキーの辞書順でソートされ、「スプリット(Split)」と呼ばれる連続したキー範囲の単位に分割されて物理ノード(Tablet)に配置される。
通常の分散データベースであれば、ハッシュ値を使ってデータをランダムにバラ撒く(ハッシュシャード)ところだが、Spannerはレンジベース(範囲ベース)のパーティショニングを採用している。これは「範囲クエリ(`WHERE CreatedAt BETWEEN …` など)」を高速に処理するためには不可欠なアーキテクチャだ。
しかし、ここに最大の罠がある。
先ほどの `TransactionID` が `1, 2, 3, 4…` と単調増加(Monotonically Increasing)する値だった場合、どうなるか?
すべての新しい書き込み(INSERT)は、常に辞書順の「一番ケツ(末尾)」に集中する。
[ スプリット A ] [ スプリット B ] [ スプリット C (現在のホットスポット) ]
Keys: a… – m… Keys: m… – z… Keys: z… – (無限大)
🔥 すべての書き込みがここに集中!
Spannerのバックエンドにあるストレージノードは、特定のキー範囲の負荷を検知すると、自動的にスプリットを半分に分割(スプリット)し、別のノードに負荷を逃がす仕組みを持っている。
「おっ、じゃあ自動で分散してくれるから放置でいいじゃん」と思ったそこの君、甘い。
単調増加キーの場合、スプリットが分裂しても、「新しく生まれた右側のスプリット」にしか新しい書き込みが来ない。結果として、スプリットの分裂が追いつかないほどの高スループット(例: 数万 QPS 超え)が流し込まれると、そのスプリットを担当している単一のCPU/ディスクが飽和し、レイテンシが跳ね上がり、最終的にエラー(DEADLINE_EXCEEDED や RESOURCE_EXHAUSTED)の雪崩を引き起こす。これが「ホットスポットによるスプレッドネック(頭打ち)」の正体だ。
—
2. 自動スプリット分割アルゴリズムの裏側と限界
Spannerのインフラストラクチャは、継続的にCPU使用率、IOPS、ストレージサイズなどのメトリクスを監視し、ホットスポットを検出しようとしている。
1. 負荷の検知: あるキー範囲へのトラフィック(読み取り/書き込み)が閾値を超えると、Spannerのバランサーが検知。
2. スプリットの分割 (Split): キー空間を分割し、別のトランスポートノードへデータの一部をマイグレーションする。
3. ロードバランス (Load Balancing): ノード間の負荷が均等になるように配置を再最適化する。
この仕組みは非常に強力だが、「リアクティブ(事後対応型)」であるという致命的な限界がある。
つまり、「急激なトラフィックの爆発(Flash Saleや秒間数万件のログ書き込みなど)」が起きた瞬間、Spannerがスプリットを分割して負荷を分散させるまでの数秒〜数十秒の間、ピンポイントでノードが耐えきれず死んでしまうのだ。
真のプロフェッショナルエンジニアなら、この自動分散機能に頼るのではなく、「最初からホットスポットが発生しようのないキー設計」を施さなければならない。
—
3. 実務で実践すべき堅牢な設計パターン
では、どう設計すべきか?
実務のコードレビューで合格点を出せる、代表的な2つの設計パターンを授けよう。
パターンA: プレフィックス・ハッシュ化(Bit-Reversal / Hash Prefixing)
単調増加するIDやタイムスタンプを使わざるを得ない場合(例:時系列データ、連番ID)、キーの先頭に「ハッシュのプレフィックス」を付与し、キー空間全体に書き込みを強制的に分散させる。
— 【推奨設計】プレフィックスにハッシュ(またはランダム値)を付与するパターン
CREATE TABLE Transactions (
— ハッシュ化されたプレフィックス(例: 0〜Fの16分割、あるいはモジュロ演算)
ShardID INT64 NOT NULL,
TransactionID INT64 NOT NULL,
UserID STRING(64),
Amount INT64,
CreatedAt TIMESTAMP,
) PRIMARY KEY(ShardID, TransactionID);
- 実装のポイント:
アプリケーション側で `ShardID` を `ABS(MOD(FARM_FINGERPRINT(CAST(TransactionID AS STRING)), 16))` のように計算し、0〜15の範囲で散らせる。これにより、書き込みが16個の異なるスプリットに綺麗に分散される。
- トレードオフ:
範囲クエリ(全期間の集計など)を行う際に、16個のシャードすべてをスキャンするようなクエリ設計(あるいはParallel Scanの活用)が必要になる。しかし、「書き込みの可用性とスケーラビリティ」を担保するためには、この代償は安い。
パターンB: ビット反転(Bit-Reversed Sequences)
Spannerには、単調増加するシーケンスの値をビット単位で反転させ、見かけ上の値をランダムに分散させる `BIT_REVERSED_POSITIVE` という強力な機能が備わっている。これを使わない手はない。
— Spannerネイティブのビット反転シーケンスを活用したテーブル定義
CREATE SEQUENCE TransactionSeq
OPTIONS (
sequence_kind = ‘bit_reversed_positive’
);
CREATE TABLE TransactionsWithSeq (
TransactionID INT64 NOT NULL DEFAULT (GET_NEXT_SEQUENCE_VALUE(MAP
UserID STRING(64),
Amount INT64,
CreatedAt TIMESTAMP,
) PRIMARY KEY(TransactionID);
- なぜこれが強いのか?
シーケンス自体は一意な値を保証しつつ、生成される値が数学的にビット反転されているため、キー空間全体に美しく均等に値が散らばる。つまり、「一意性と連番のメリットを維持しながら、書き込みのホットスポットを完全に回避」できる。Spannerでオートインクリメント的なIDが必要な場合は、まず真っ先にこれを採用すべきだ。
—
4. パフォーマンス上の注意点と監視の勘所
どれほど完璧な設計をしても、運用フェーズでの油断は禁物だ。以下のポイントを必ず監視のオートルートに組み込んでおけ。
1. Cloud Monitoringでのスプリット統計の監視
- `storage/used_bytes` や `cpu/utilization` だけでなく、Spannerのコンソールやメトリクスにある 「Top Keys(ホットキーの検出)」 を定期的にチェックしろ。特定のキーにアクセスが集中している場合、Cloud Monitoringでアラートを飛ばす仕組みが必須だ。
2. インデックス(Secondary Index)のホットスポット
- メインテーブルだけでなく、セカンダリインデックスも同様にレンジベースで分割される。
- 例えば、`CREATE INDEX Idx_CreatedAt ON Transactions(CreatedAt)` のようなインデックスを作ると、時系列でデータが流れ込むため、インデックス自体が凄まじいホットスポットになる。時系列データにインデックスを貼る場合も、`ShardID` を先頭に含める複合インデックスにするのが鉄則だ。
—
最後に:チーフアーキテクトからのメッセージ
Cloud Spannerは、正しく使えばこれ以上ないほど強力で信頼性の高いデータベースだ。しかし、RDBの古い常識(「とりあえずオートインクリメントのIDを振っておけばいいや」)を持ち込んだ瞬間、その牙を剥く。
コードレビューで「なぜこのプライマリキーを選んだのか?」「スプリットの分散はどう担保されているか?」を論理的に説明できないコードは、本番環境へ通すな。
君たちの手で、美しく、スケールし続ける堅牢なアーキテクチャを作り上げてくれ。健闘を祈る。
コメント