ディレクトリシャーディングの極意:Spannerの分散プリミティブをハックし、真の極限スループットを引き出す方法
こんにちは。チーフアーキテクトの私だ。
これまでのコードレビューや設計レビューで、こんな絶望的なクエリやスキーマ定義に何度直面したことか。
「なんでマルチテナントのデータを単にUUIDでバラ撒いてんだ?」
「数百万件の子レコードを更新するたびに、なんで分散トランザクションが発火してレイテンシが跳ね上がってんだよ」
Cloud Spannerは「無限にスケールするRDB」だ。だが、それは魔法の箱ではない。物理的な制約——ネットワークの速度、ディスクシーク、そしてスプリット(Split)という物理的境界——の上に成り立っている。
今回は、Spannerのコアアーキテクチャの根幹をなす「ディレクトリシャーディング(Directory Sharding)」について、実務で明日から使えるレベルを超え、システムを破滅から救うための「極限の知見」を授けよう。
—
1. なぜSpannerは「普通に書く」と遅くなるのか?
Cloud Spannerの背後にあるストレージエンジンは、キー範囲(Key Range)ごとにデータをソートし、「スプリット(Split)」と呼ばれる物理的な単位でマルチプルなPaxosグループに分散・配置している。
デフォルトでは、主キー(Primary Key)の先頭カラムのハッシュやプレフィックスに基づいてデータがスプリットに分割される。ここに罠がある。
もし、関連するエンティティ(例えば、`Tenant` とその `User`、さらにその `Log`)をバラバラの主キー設計で配置するとどうなるか?
- 現実: `Tenant A` のデータが、物理的に異なるスプリット(場合によっては異なるノード、異なるラック)のあちこちに散らばる。
- 結果: 1つのトランザクションで `Tenant A` の一連の処理を行おうとした瞬間、2相コミット(2PC)とネットワークを跨ぐ分散トランザクションのオーバーヘッドが直撃する。
ここにメスを入れるのが、Spannerの隠し武器、いや、正しく使いこなすべき最強のプリミティブ「ディレクトリ(Directory)」なのだ。
—
2. ディレクトリシャーディングの正体
Cloud Spannerの内部では、ストレージ層の最適化単位として「ディレクトリ」という概念が存在する。これは、関連する行のグループを論理的、そして物理的に同じスプリット内に「接着剤で固める」ように配置する仕組みだ。
インターリーブ(Interleave)テーブルは、このディレクトリシャーディングを最も美しく、かつ宣言的に実現するシンタックスシュガーに他ならない。
誤った設計(アンチパターン)
— すべてのテーブルが独立した主キーを持ち、物理的に散らばる最悪のパターン
CREATE TABLE Tenants (
TenantId INT64 NOT NULL,
Name STRING(MAX),
) PRIMARY KEY (TenantId);
CREATE TABLE Users (
UserId INT64 NOT NULL,
TenantId INT64 NOT NULL,
Email STRING(MAX),
) PRIMARY KEY (UserId); — TenantIdが先頭にない!
この設計では、あるテナントのユーザー一覧を取得するだけで、グローバルなインデックススキャンや、複数スプリットを跨ぐ遠隔RPCが発生する。プロポーサル段階でリジェクトだ。
正しい設計(ディレクトリシャーディングの極意)
インターリーブを使用することで、親行(Tenant)と子行(User)が物理的に同じストレージブロック(ディレクトリ)に共存することをSpannerに強制する。
— 親テーブル
CREATE TABLE Tenants (
TenantId INT64 NOT NULL,
Name STRING(MAX),
CreatedAt TIMESTAMP,
) PRIMARY KEY (TenantId);
— 子テーブル(ディレクトリシャーディングの適用)
CREATE TABLE Users (
TenantId INT64 NOT NULL,
UserId INT64 NOT NULL,
Email STRING(MAX),
) PRIMARY KEY (TenantId, UserId),
INTERLEAVE IN PARENT Tenants ON DELETE CASCADE;
何が起きているか?
`TenantId = ‘company-A’` の `Tenants` 行と、それに紐づく数百万件の `Users` 行は、ストレージ上で完全に隣接して配置される。これにより、物理的なディスクシークが最小化され、同一テナント内の操作は単一のPaxosグループ内で完結するようになる。
—
3. 実務における設計パターンと限界値の制御
では、実務の現場でこのディレクトリシャーディングをどう適用すべきか。私のプロジェクトで採用している鉄則を共有しよう。
鉄則 1: 「ホットスポット」との戦いを制せよ
ディレクトリシャーディングにはトレードオフがある。特定のディレクトリにデータが集中しすぎると、そのスプリットがホットスポットとなり、CPU使用率が100%に張り付く。
- 対策: シャーディングキー(プレフィックス)の選定がすべてだ。例えば、単に `Date` をプレフィックスにすると、今日のデータに書き込みが集中して死ぬ。
- 正解: テナントIDや、十分に分散するハッシュプレフィックスを主キーの最上位(ディレクトリの根)に据えること。
鉄則 2: 深すぎる階層の呪縛
インターリーブは最大6階層まで定義できるが、実務上の限界は「3階層」だ。これ以上深くすると、スキーマ変更(DDL)のロック競合や、クエリプランナーの最適化が複雑怪奇になり、かえってパフォーマンスが劣化する。
— 限界ギリギリの美しい階層構造(3階層)
Tenants (親)
└── Projects (子)
└── Tasks (孫)
この構造であれば、特定のテナントにおける「プロジェクトとそれに紐づくタスクの一括削除(`ON DELETE CASCADE`)」が、驚異的な速度で、かつアトミックに実行できる。分散トランザクションのオーバーヘッドはほぼゼロだ。
—
4. パフォーマンス上の注意点(実戦からのフィードバック)
コードレビューで必ず指摘するポイントを挙げておく。これを破ると、本番障害の夜間コールに怯えることになる。
1. セカンダリインデックスの罠
インターリーブされたテーブルに対してグローバル・セカンダリインデックスを切る場合、インデックス側はディレクトリの局所性を失う場合がある。検索クエリがインデックス経由になるのか、ベーステーブルの局所スキャン(Local Index)になるのか、必ず `EXPLAIN` で実行計画を確認しろ。
2. バルクインサートの限界
初期データ移行などで、単一の親に対して数百万件の子レコードを爆発的にINSERTする場合、同一スプリットへの書き込み集中によるレイテンシ上昇が発生する。移行時のみバッチサイズを適切に絞り(例:1トランザクションあたり数千行)、適度なウェイトを入れる配慮が必要だ。
—
5. チーフアーキテクトからのメッセージ
Cloud Spannerの本質は、「分散データベースであることを意識させないこと」ではなく、「分散の物理法則を理解した上で、極限のパフォーマンスを引き出すこと」にある。
ディレクトリシャーディングは、そのための最も強力な武器だ。
データがどこにあり、どう流れるのか。ストレージの物理配置まで頭の中でイメージできるようになれば、あなたも真のSpanner使いだ。
次の設計レビューでは、美しくシャーディングされた、完璧なスキーマを持ってくることを期待している。
健闘を祈る。
コメント