Cloud Spannerのインデックスは「ただの索引」ではない:物理分散を制する者がスループットを制す
Cloud Spannerを触り始めて数年、あるいは大規模プロジェクトの火消しに回った経験があるなら、一度は直面するはずだ。「なぜ、特定のインデックスだけがホットスポットになるのか?」「なぜ、インデックスの追加が書き込みスループットをここまで削るのか?」と。
多くのエンジニアは、RDBの感覚でインデックスを「検索を高速化する魔法の杖」と捉えている。だが、Spannerにおけるインデックスは、実体としては「独自のテーブル」であり、物理的に独立してパーティショニングされる分散オブジェクトだ。
本稿では、Spannerのインデックスが裏側でどう分割(スプリット)され、どう物理的に分散されるのか。その「極限の制御術」を伝授する。
—
1. インデックスの「物理的独立性」という前提を理解せよ
Spannerのインデックスは、テーブルの行とは物理的に異なる場所に格納されることがある。重要なので繰り返すが、インデックス行とベーステーブル行は、物理的に異なるスプリット(Split)に配置されるのがデフォルトだ。
- ベーステーブル: 主キーによって分散される。
- インデックス: インデックスキーによって分散される。
これが意味するのは、インデックスへの書き込みは、ベーステーブルへの書き込みとは独立した物理サーバ(スプリット)で処理されるということだ。つまり、インデックスを追加するということは、システム全体のスループット負荷を物理的に「横」に広げる作業に他ならない。
2. スプリットの分割:自動化の裏側と、エンジニアが介入すべき境界線
Spannerは、データ量が一定(デフォルトで約2GB以上)になると、自動的に「スプリット」を分割し、別のノードへ移動させる。この「Split Partitioning」こそがSpannerのスケールアウトの根幹だ。
しかし、ここでエンジニアが陥る罠がある。「単調増加するキー」だ。
悲劇:シーケンシャルなキーによるホットスポット
例えば、`created_at TIMESTAMP` をインデックスの先頭に持ってきた場合、新しいデータは常にインデックスの「末尾」に書き込まれる。
このとき、インデックスの末尾を担当する単一のスプリットに負荷が集中する。Spannerは自動分割を試みるが、分割しても結局新しいデータは「新しい末尾」に集中するため、ノード間負荷分散が効かない。
解決策:キーのインターリービングとハッシング
もし高頻度で書き込まれるテーブルにインデックスを貼るなら、以下のパターンを強く推奨する。
— 悪い例:単調増加する値が先頭
— ホットスポットが発生し、書き込み性能が頭打ちになる
CREATE INDEX UsersByCreatedAt ON Users(CreatedAt);
— 良い例:シャーディングキーを付与する
— 負荷を物理的に分散させるために擬似的なシャードを付与
CREATE INDEX UsersByShardAndCreatedAt ON Users(ShardId, CreatedAt);
※ `ShardId` には 0-N 程度の値をランダム(またはハッシュ)に付与し、物理スプリットを意図的にバラけさせる。これが「Spannerアーキテクチャを理解しているエンジニア」の設計だ。
3. インデックス・スプリットの「物理配置」をハックする
Spannerのインデックスは、`STORING` 句を使うことで、ベーステーブルからのデータ取得を回避(Index Only Scan)できる。これは極めて強力だが、物理的なサイズに注意が必要だ。
— 賢い設計:必要なカラムだけを持ち込む
CREATE INDEX UsersByEmail ON Users(Email) STORING (DisplayName, LastLoginAt);
ここで重要なのは、「STORINGされたカラムもインデックスのサイズを肥大化させる」という点だ。サイズが肥大化すれば、インデックスのスプリット分割が頻発し、クラスタ全体のネットワークI/Oを圧迫する。
実務上の鉄則は以下の通りだ。
1. Read-Modify-Writeの回数を減らす: インデックスが多いほど、1回の書き込み操作で更新すべきスプリット数が増える。分散トランザクションのオーバーヘッドを意識せよ。
2. スプリット境界を意識したデータ配置: 巨大なインデックスを持つ場合、そのスプリットの物理位置を意識し、頻繁に参照されるクエリが同じノード内のスプリットに収まるように設計する(いわゆるデータ局所性の最適化)。
4. 堅牢な設計パターンのための「チェックリスト」
レビューで以下の点を確認していないなら、あなたはまだSpannerのポテンシャルを引き出せていない。
- [ ] 単調増加キーの排除: インデックスの先頭がシーケンシャルな値になっていないか?
- [ ] 不要なインデックスの削除: 読み取り速度向上のために貼ったインデックスが、書き込みスループットを犠牲にしていないか?
- [ ] STORING句の精査: 本当にそのカラムをインデックスに含める必要があるか?(ベーステーブルのJoinで十分ではないか?)
- [ ] スプリット負荷のモニタリング: Cloud Monitoringで `spanner.googleapis.com/instance/cpu_utilization_by_leader` を確認し、特定のインデックススプリットが極端に高いCPUを消費していないか?
—
最後に:エンジニアへのメッセージ
Cloud Spannerは「魔法のDB」ではない。物理的な制約を理解し、その上でデータの流れを意図的に制御する者が、最強のパフォーマンスを手に入れる。
「とりあえずインデックスを貼る」という思考停止は、Spannerにおいては「物理的なボトルネックを自ら生成する」ことと同義だ。インデックスのパーティショニングを意識し、スプリットの挙動をコントロールする。その先にこそ、真にスケールするシステムが待っている。
さあ、コードを開いて、設計を見直そう。君の設計が、Spannerの真の力を解放するはずだ。
コメント