【実務・中級編】 ディレクトリベースの配置 – Cloud Spanner

【Cloud Spanner 伝説のアーキテクチャ講義】第1回:ディレクトリベースの配置(Directory-based Placement)を極める

おい、設計レビューの手を止めろ。そのテーブル定義、本当に何も考えずに `PRIMARY KEY` を打っただけじゃないだろうな?

「Cloud Spannerはリニアスケーラビリティがあるから、適当にシャードキーを切っても勝手に分散してくれる」――そんな甘い神話を信じているなら、今すぐその頭をアップデートしてもらう。

大規模なマルチテナントシステムや、グローバル展開するSaaSの設計で必ずぶぶる壁がある。それが「分散しすぎることの弊害」だ。
Spannerはデフォルトでデータを細かくスプリットし、世界中のノードに分散配置する。だが、ある特定のテナントやユーザーに関連するデータを1つのトランザクションで安全に、かつ爆速で処理したい場合、この「全方位への分散」はパフォーマンスの足枷(ネットワークホップの増大)になり得る。

ここで登場するのが、Cloud Spannerの真髄の一つ、「ディレクトリベースの配置(Directory-based Placement)」だ。
今回は、この機能の物理的メカニズムから、実務で絶対に踏み抜いてはならないアンチパターンまで、チーフアーキテクトの私が容赦なくロジカルに叩き込む。心して読め。

—

1. そもそも「ディレクトリ」とは何か?(物理レイヤーの真実)

Spannerのストレージ層において、データは「スプリット(Split)」と呼ばれる単位で管理され、Paxosグループによってレプリケーションされている。
通常、Spannerはこのスプリットを自動的に管理し、負荷に応じて分割・移動を行う。

しかし、ディレクトリ(Directory)という概念を導入することで、我々はアプリケーションの論理的なデータ群(例:テナントID単位など)を、物理的に同一のスプリット、あるいは極めて近い場所に強制的に集約(Colocation)させることができる。

[Spanner Node]
├── Split A (Directory: Tenant_X) ← 関連データが物理的に同居!
│ ├── Customers (Tenant_X)
│ ├── Orders (Tenant_X)
│ └── OrderItems (Tenant_X)
└── Split B (Directory: Tenant_Y)
├── Customers (Tenant_Y)
…

なぜこれが強力なのか?

1. 局所性の最大化: 同一ディレクトリ内のデータアクセスは、物理的な近接性を保証されるため、ノード間通信(RPC)のオーバーヘッドが劇的に減少する。
2. 効率的なデータムーブメント: テナントのデータ移行や削除(Drop)を行う際、ディレクトリ単位で物理的な処理を最適化できる。

インターリーブ(Interleave)が「親子関係のテーブル行を物理的に隣り合わせる」機能だとすれば、ディレクトリベースの配置は、「より広い範囲の論理的グループ(複数テーブルにまたがるデータ群)を制御するオーバーロード機構」と理解するといい。

—

2. 実践:ディレクトリベースの配置の構築と設計パターン

百聞は一見にしかずだ。実務で使える堅牢な設計パターンを見ていこう。
ここでは、マルチテナントSaaSを想定し、テナントごとのデータを確実に同一ディレクトリに閉じ込めるスキーマ設計の例を示す。

DDL設計例

Spannerでディレクトリベースの配置を有効化するには、テーブル定義の際に `DIRECTORY` キーワードを使用し、配置のルートとなるキーを指定する。

— テナントのルートマスターテーブル
CREATE TABLE Tenants (
TenantId INT64 NOT NULL,
TenantName STRING(MAX),
Region STRING(64),
) PRIMARY KEY (TenantId);

— テナントに紐づくユーザーテーブル(ディレクトリの配下に配置)
CREATE TABLE Users (
TenantId INT64 NOT NULL,
UserId INT64 NOT NULL,
Email STRING(255),
CreatedAt TIMESTAMP,
) PRIMARY KEY (TenantId, UserId),
— ここがポイント:TenantIdをディレクトリの基準とする
INTERLEAVE IN PARENT Tenants ON DELETE CASCADE;

— 注文履歴テーブル
CREATE TABLE Orders (
TenantId INT64 NOT NULL,
OrderId INT64 NOT NULL,
UserId INT64 NOT NULL,
TotalAmount NUMERIC,
Status STRING(32),
) PRIMARY KEY (TenantId, OrderId),
INTERLEAVE IN PARENT Tenants ON DELETE CASCADE;

チーフアーキテクトの解説:この設計の美しさ

上記のスキーマでは、`Tenants` テーブルをルートとし、`Users` と `Orders` をインターリーブしている。これにより、特定の `TenantId` に属するすべてのデータが、物理的に同じスプリット(ディレクトリ)に集約される。

アプリケーションから次のようなクエリを発行したとき、Spannerのオプティマイザは「このデータはすべて同一ノード上にある」と即座に判断し、極めて低いレイテンシで結果を返す。

— 特定テナントのユーザーと直近の注文を結合するクエリ
— ディレクトリの局所性が効いているため、クロスノード通信が発生しない
SELECT
u.UserId,
u.Email,
o.OrderId,
o.TotalAmount
FROM Users u
JOIN Orders o ON u.TenantId = o.TenantId AND u.UserId = o.UserId
WHERE u.TenantId = @targetTenantId;

—

3. パフォーマンス上の注意点と「やってはいけない」アンチパターン

ここからが本番だ。技術の裏側を知らずに使うエンジニアは、往々にして障害の元を作る。以下の3点は、コードレビューで私が絶対に弾くポイントだ。

アンチパターン1: ホットスポット(Hotspotting)の看過

ディレクトリベースの配置は、データを物理的に集約するため、「特定のディレクトリにトラフィックが集中した場合、そのディレクトリを保持する単一のPaxosグループ(あるいはスプリット)に負荷が集中する」というトレードオフを抱える。

  • 対策: 単一の巨大なテナント(例:全データの80%を占める超大口顧客)が同じディレクトリに存在する場合、そのテナント自体をさらに細かくシャーディングするか、ハッシュプレフィックスを付与して負荷分散する設計に逃げるべきだ。ディレクトリの粒度はビジネス要件とスケーラビリティのバランスで決めろ。

アンチパターン2: ディレクトリ境界を跨ぐ無茶なトランザクション

「関連データが近くにあるなら、全部まとめて巨大なトランザクションに入れればいいや」――これ犯罪的な発想だからな。
ディレクトリベースの配置はパフォーマンスを最適化するが、ACIDトランザクションの分散範囲の制約を魔法のように消し去るわけではない。異なるディレクトリにまたがる更新を1つのトランザクションで行う場合、結局は2相コミット(2PC)のコストが発生する。

  • 対策: トランザクションの境界は常に単一のディレクトリ(単一テナント)内に収まるよう、アプリケーション層の設計を厳密に統制しろ。

アンチパターン3: スプリットサイズの肥大化放置

ディレクトリ内のデータが無制限に成長し続けると、スプリットのサイズがSpannerの推奨上限(通常数GB〜数十GB)を超え、最終的にSpannerが自動的にそのスプリットを分割せざるを得なくなる。分割が発生した瞬間、それまで保証されていた「完璧な局所性」が崩れる可能性がある。

  • 対策: データ量が増大する時系列データなどは、TTL(Time to Live)やアーカイブ機構を組み合わせ、1ディレクトリあたりのデータフットプリントを常に健全なサイズに保て。

—

4. 総括:プロフェッショナルとしての誇りを持て

Cloud Spannerにおける「ディレクトリベースの配置」は、単なる便利機能ではない。
「分散データベースの強み(無限のスケール)」と「リレーショナルデータベースの強み(局所性と整合性)」の妥協点を見出す、極めて高度なアーキテクチャの武器だ。

お前たちが書くコード、お前たちが組むスキーマ一つで、システムの寿命が決まる。
「なんとなく動く」で満足するな。データが物理的にどこを流れ、どのノードのメモリに乗り、どうやってディスクに落ちているのか――そこまで想像力を働かせるのが、真のプロフェッショナルエンジニアだ。

次の設計レビューでは、今回伝えた知見がコードの隅々にまで反映されていることを期待している。
質問があるならいつでも来い。ロジックで叩き直してやる。

コメント

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