【実務・中級編】 主キー設計のベストプラクティス – Cloud Spanner

【Spannerアーキテクチャ徹底解剖】ホットスポットの呪縛を断ち切る:極限の主キー設計

テックリードの私だ。コードレビューや設計レビューで、未だに「とりあえずインクリメンタルなIDを主キーにしました」「UUIDv4をそのまま突っ込みました」という設計を見かけては、深き溜息をついている。

Cloud Spannerは、リレーショナルデータベースの強烈な整合性と、NoSQL並みの水平スケーリングを両立させた類まれなるシステムだ。しかし、その圧倒的なスケーリング能力を引き出せるか否かは、「主キー(Primary Key)設計」というたった一つのファクターに依存している。

今回は、Spannerの分散ストレージの内部挙動に踏み込み、真に分散性能を最大化するための主キー選択基準と、単調増加キーがもたらす悲劇(ホットスポット)のメカニズム、そしてそれを回避するための実践的パターンを叩き込む。

—

1. Spannerのストレージ構造:なぜ主キーがすべてなのか

まず、Spannerの足元を支える物理アーキテクチャを理解してほしい。

Spannerはデータをテーブルの主キーの順序に従ってソートし、「スプリット(Split)」と呼ばれる物理的なシャードに分割して複数のストレージサーバー(リーダーおよび投票者レプリカ)に配置する。

[ スプリット A ] [ スプリット B ] [ スプリット C ]
(Key: A 〜 M) (Key: N 〜 Z) (Key: その他)
+———————–+ +———————–+ +———————–+
| サーバー 1 (Leader) | | サーバー 2 (Leader) | | サーバー 3 (Leader) |
+———————–+ +———————–+ +———————–+

このスプリットは、データの増加や負荷(CPU使用率、I/O)に応じて動的に分割・移動する。
ここで重要になるのが、「Spannerの負荷分散単位はスプリット(=主キーの範囲)単位である」という事実だ。

もし主キーの設計を誤ると、この美しい分散機構が完全に沈黙し、単一のサーバーにすべての負荷が集中する「ホットスポット」が爆誕する。

—

2. 単調増加キーの悪夢:なぜホットスポットが発生するのか

多くの開発者がやりがちな最大の過ちが、単調増加キー(Auto Increment、シーケンス、生成時刻ベースのIDなど)の採用だ。

ホットスポットのメカニズム

例えば、次のようなテーブル定義を考えてみす。

CREATE TABLE Events (
EventId INT64 NOT NULL,
UserId INT64 NOT NULL,
Payload STRING(MAX),
) PRIMARY KEY (EventId);

ここで `EventId` に `1, 2, 3, 4…` と単調増加する値を割り振ってインサートし続けたとする。
Spannerのストレージは主キー順でソートされているため、「今まさに書き込まれている最新のデータ」は、常に主キー空間の「一番末尾(右端)」に集中する。

結果として何が起きるか?

1. 最新のデータを含むスプリット(例:`EventId` が最大値付近を担当するスプリット)にのみ、全書き込みトラフィックが集中する。
2. スプリットが動的に分割されようにも、書き込みが「常に末尾の一点」にしか発生しないため、新しいスプリットへトラフィックを綺麗に逃がすことができない。
3. 担当する単一のCPU/ディスクが飽和し、レイテンシが跳ね上がり、最悪の場合はSpannerのノードがダウンする。

「うちはまだデータ量が少ないから大丈夫」という言い訳は通用しない。データ量ではなく「書き込みのヒットする時間軸・空間軸の局所性」が問題なのだ。

—

3. 回避策:分散性能を最大化する3つの主キー設計パターン

では、どうすればよいのか?
答えはシンプルだ。「書き込みトラフィックを主キー空間全体に散らす(スキャッターする)」こと。

実務で即座に使える3つの堅牢なパターンを伝授しよう。

パターンA:ハッシュプレフィックス(Hash Prefixing)

単調増加するIDやタイムスタンプの先頭に、特定のハッシュ値のプレフィックスを付与する方法だ。

CREATE TABLE Events (
ShardedEventId STRING(64) NOT NULL, — 例: “a3f-10001” (Hash + Seq)
EventId INT64 NOT NULL,
Payload STRING(MAX),
) PRIMARY KEY (ShardedEventId);

  • 仕組み: IDの元データからハッシュ(例えばMurmurHashやMD5の一部)を計算し、それをプレフィックスとして結合する。これにより、連続したIDであっても主キー空間全体にランダムに散らばる。
  • 注意点: 範囲スキャン(`WHERE ShardedEventId BETWEEN …`)の効率が落ちるため、単一レコードの取得や、後述するインターリーブ構造と組み合わせるなどの工夫が必要。

パターンB:キーの反転(Bit Reversal)

数値のビット列表現を反転させることで、単調増加する数値を完全にシャッフルするテクニックだ。

  • 仕組み: 例えば `1, 2, 3, 4` という連番をビット反転させると、まったく異なる規則性のない数値に変換される。
  • メリット: ハッシュ衝突の心配がなく、数値型のままランダム性を担保できる。ただし、Spannerの標準機能としてビット反転関数が直接提供されているわけではないため、アプリケーション側で実装するか、事前に計算しておく必要がある。

パターンC:自然キーの複合化(Parent-Child Interleaving)

マルチテナント構造やユーザーごとのデータであれば、テナントIDやユーザーIDを主キーの先頭に持ってくるのが最もエレガントかつ実務的だ。

— 親テーブル: ユーザー
CREATE TABLE Users (
UserId INT64 NOT NULL,
Name STRING(100),
) PRIMARY KEY (UserId);

— 子テーブル: ユーザーのイベント(インターリーブ定義)
CREATE TABLE UserEvents (
UserId INT64 NOT NULL,
EventId INT64 NOT NULL, — ここがユーザー内での単調増加であっても問題ない
Payload STRING(MAX),
) PRIMARY KEY (UserId, EventId),
INTERLEAVE IN PARENT Users ON DELETE CASCADE;

  • なぜこれで動くのか?:

`UserId` が十分に分散していれば(例えばユーザー数が数万〜数百万以上いる場合)、書き込みは各 `UserId` の空間(物理的に同じスプリットにまとめられる親子の局所性を持つ)に並行分散される。
子テーブルの `EventId` がそのユーザー内において単調増加(`1, 2, 3…`)であっても、ユーザー全体で見ればトラフィックは完全に分散されているため、ホットスポットは発生しない。

  • アーキテクチャ上のメリット: 親子関係にあるデータが物理的に同じストレージ領域に近づけて配置されるため、JOINのパフォーマンスが劇的に向上する。

—

4. UUIDv4は本当に安全か?

「じゃあ、ランダムなUUIDv4を主キーにすれば完璧だな!」と思ったそこのあなた、少し待ってほしい。

確かにUUIDv4はランダム性が高いため、書き込みのホットスポットは回避できる。しかし、UUIDv4には「ストレージのフラグメンテーション(断片化)」という別の性能劣化リスクが潜んでいる。

Spannerは主キー順にデータを物理ソートして保持しているため、完全ランダムなUUIDv4を主キーにすると、新しいデータが既存のデータの「あらゆる隙間」にランダムに挿入されることになる。これにより、ディスクページのスプリットやキャッシュヒット率の低下を招き、シーケンシャルなキーに比べてスループットが低下する場合がある。

【チーフアーキテクトの推奨】

  • 最高のスループットと範囲検索性能を両立させたいなら:「分散されたプレフィックス + タイムスタンプ/連番」の複合キー(例: `TenantId + CreatedAt + SeqId`)
  • 単純なユニーク性を手軽に担保したいなら:UUIDv4も選択肢だが、インデックスのフットプリントやキャッシュ効率のトレードオフを理解した上で採用すること。

—

5. まとめ:コードレビューで確認すべきチェックリスト

明日からの設計レビューで、チームメンバーのPRに対して以下のチェックリストを突きつけてほしい。

1. 主キーの先頭に単調増加する値(日時やオートインクリメント)をそのまま置いていないか?

  • $\rightarrow$ 置いている場合、書き込みのホットスポットを踏む確率は100%だ。

2. トラフィックが特定のキー範囲に偏る構造になっていないか?

  • $\rightarrow$ テナントID、ユーザーID、あるいはハッシュ化されたプレフィックスで適切に分散されているか確認する。

3. インターリーブ構造を活用できているか?

  • $\rightarrow$ 親子関係にあるエンティティであれば、インターリーブによる局所的なパフォーマンス最適化と分散の両立を検討したか。

Cloud Spannerは正しく飼い慣らせば、これほど頼りになるデータベースはない。しかし、主キー設計を誤ると、その牙を容赦なく剥いてくる。

妥協のない、美しい分散アーキテクチャの設計を期待している。

コメント

タイトルとURLをコピーしました