【実務・中級編】 データ局所性最適化 – Cloud Spanner

【Cloud Spanner極限最適化】データ局所性(Data Locality)を支配し、ネットワーク転送を撲滅せよ

こんにちは。テックリードの私だ。
今日のコードレビュー、あるいは設計レビューで、君たちはまた「なんとなくインターリーブ(Interleave)を使いました」「クエリが遅いのでインデックスを追加しました」といった、表面的な回答を用意していないだろうか?

Cloud Spannerは、その無限とも思えるスケーラビリティと強整合性を両立させた化け物のようなデータベースだ。しかし、物理的な制約――「光速の壁」と「ネットワーク帯域の有限性」――を無視することは、いかにGoogleの分散インフラストラクチャといえども不可能だ。

Spannerのパフォーマンスチューニングの極意、それは「データ局所性(Data Locality)の完全な掌握」に他ならない。

今回は、クエリ実行時にネットワークホップを極限まで排除し、データが存在する物理ノードの近傍(あるいは同一メモリ空間)で処理を完結させるための配置戦略と設計パターンを、実務の現場で即使えるレベルで叩き込む。

—

1. 概念の深掘り:なぜ「データ局所性」がSpannerの生死を分けるのか?

Cloud SpannerはデータをPaxosグループ単位でシャードし、世界中のワーカーノードに分散配置している。
基本中の基本だが、リクエストが発行された際、ストレージ(RocksDBベースの分散ファイルシステム)とコンピュート(Spannerのプロセス)が異なるノード、あるいは異なるスプリット(Split)にまたがっている場合、内部でRPC(Remote Procedure Call)が発生する。

想像してほしい。
1つの巨大なトランザクションや集計クエリを処理するために、何十ものスプリットからデータをネットワーク経由でかき集めるアーキテクチャを。――これでは、どれだけCPUやメモリを盛っても、ネットワークの帯域とレイテンシ(Tail Latency)の壁に阻まれ、スループットは確実に頭打ちになる。

最適化のゴール

  • 「Compute meets Data」の原則: 演算処理をデータがある場所へ寄せる、あるいはデータを演算が予測される場所に予め配置する。
  • ネットワークホップの撲滅: スプリット間のデータ転送をゼロ、または最小限に抑え、P99レイテンシを劇的に改善する。

これを実現するための武器が、「インターリーブ(Interleaving)」と「カスタムスプリット(Split)制御」だ。

—

2. 堅牢な設計パターン:インターリーブによる物理的コロケーション

最も強力かつ実務で多用されるデータ局所性の最適化手法が、テーブル間のインターリーブ(親子関係の定義)だ。

よくあるアンチパターンから見ていこう。

❌ アンチパターン:論理的結合に頼った別テーブル配置

— ユーザーテーブル
CREATE TABLE Users (
UserId STRING(64) NOT NULL,
UserName STRING(100),
) PRIMARY KEY(UserId);

— 注文テーブル(単なる外部キー制約のみ)
CREATE TABLE Orders (
UserId STRING(64) NOT NULL,
OrderId STRING(64) NOT NULL,
OrderDate TIMESTAMP,
Amount INT64,
) PRIMARY KEY(UserId, OrderId),
INTERLEAVE IN PARENT Users ON DELETE CASCADE; — おっと、これは正しい例だ

もし、`Users` と `Orders` を別々に(インターリーブせずに)作成した場合、Spannerはこれらを独立したスプリットとして分散配置する。そのため、特定のユーザーの注文履歴を取得するクエリを発行した際、親(User)のノードから子(Order)のノードへネットワーク経由でデータをフェッチする必要が生じる場合がある。

⭕ 正解パターン:物理的インターリーブによる同一ストレージ配置

インターリーブを正しく定義すると、Spannerのストレージ層(RocksDB)において、親行の直下にその子孫行のデータが物理的に隣接して(Colocated)格納される。

— ユーザーマスター
CREATE TABLE Users (
UserId STRING(64) NOT NULL,
UserName STRING(100),
CreatedAt TIMESTAMP,
) PRIMARY KEY(UserId);

— 注文履歴(親と物理的に同一のストレージブロックに配置される)
CREATE TABLE Orders (
UserId STRING(64) NOT NULL,
OrderId STRING(64) NOT NULL,
OrderDate TIMESTAMP,
Amount INT64,
) PRIMARY KEY(UserId, OrderId),
INTERLEAVE IN PARENT Users ON DELETE CASCADE;

なぜこれが神速なのか?

1. 単一シーク: `UserId = ‘u-123’` のユーザーとその全注文履歴を取得する際、Spannerは単一のストレージ領域をスキャンするだけで済み、ノード間通信が発生しない。
2. プレフィックス圧縮の恩恵: 物理的にデータが連続して並ぶため、プレフィックス圧縮効率が跳ね上がり、メモリキャッシュヒット率が劇的に向上する。

—

3. 実務的アプローチ:クエリ実行時の局所性を最大化する書き方

いくらテーブル設計で局所性を担保しても、発行するSQLがそれを台無しにしているケースをレビューでよく見かける。

悪い例:非効率な結合とバラバラのキー指定

— 最悪の例:異なるシャードに散らばるデータを無理やり結合
SELECT u.UserName, o.Amount
FROM Users u
JOIN Orders o ON u.UserId = o.UserId
WHERE u.CreatedAt > ‘2023-01-01’;

これ自体はインターリーブが効いていればSpannerのオプティマイザがローカルジョイン(Local Join)を選択してくれる可能性が高い。しかし、条件が複雑化したり、グローバルセカンダリーインデックスを挟むと、途端に分散クエリ(Distributed Query)へと格下げされる。

良い例:常に出発点を「親キー」に固定する

インターリーブされたテーブルに対するクエリは、必ず親のプライマリキーをWHERE句の最優先条件(プレフィックス)に含めること。

— 推奨されるクエリ
— Spannerは「どのノードのどのスプリットを見に行けばいいか」を瞬時に特定できる
SELECT
o.OrderId,
o.OrderDate,
o.Amount
FROM Orders@{FORCE_INDEX=Users_AllOrders} o — 状況に応じたヒント(基本はオプティマイザにお任せだが)
WHERE o.UserId = ‘u-123’
AND o.OrderDate >= ‘2023-10-01’;

Spannerの分散クエリエンジンは、`WHERE o.UserId = ‘u-123’` を見た瞬間、そのユーザーデータを持つ単一のスプリット(あるいはリーダーレプリカ)に直接クエリをルーティングする。ネットワークホップは「ゼロ」だ。

—

4. パフォーマンス上の注意点と落とし穴

データ局所性を追求するあまり、初心者が陥りがちなしこたま厄介なアンチパターンについても触れておこう。

1. ホットスポット(Hotspotting)の罠

データ局所性を高めようとして、時系列データ(ログやイベント)のプライマリキーの先頭に「日付」や「常に増加するID」を配置すると、単一のノードに書き込みが集中するホットスポットが発生する。

— ⚠️ 危険な設計(書き込みの局所性が高すぎてノードが死ぬ)
CREATE TABLE Events (
EventDate DATE NOT NULL, — 先頭キーが常に今日の日付
EventId STRING(64) NOT NULL,
Payload STRING(MAX),
) PRIMARY KEY(EventDate, EventId);

  • 結果: 今日担当のノードのCPU使用率が100%に張り付き、他のノードは暇を持て余すという、Spannerの分散メリットを完全に殺す事態になる。
  • 対策: ハッシュプレフィックス(Hash Prefix)や逆転IDを用いて書き込みを意図的に分散させよ(Read時の局所性とWrite時の分散のトレードオフをコントロールする)。

2. 深すぎるインターリーブ階層

「親 > 子 > 孫 > ひ孫」と、4階層以上のインターリーブを組む開発者がいるが、これも注意が必要だ。

  • 弊害: 行の削除や更新時にロックのスコープが複雑化し、トランザクションの競合(Contention)が発生しやすくなる。実務上、階層は最大でも2階層(親・子)、深くて3階層にとどめるのが無難である。

—

5. チーフアーキテクトからの提言

Cloud Spannerにおけるデータ局所性の最適化は、単なる「クエリの高速化テクニック」ではない。それは「データ構造を物理配置から逆算してデザインする」という、分散システムエンジニアとしての本質的なアプローチだ。

レビューの場で、もし次のようなコードや設計を見かけたら、即座に差し戻しを命じてほしい。

1. 関連性の高いエンティティが、何の根拠もなくバラバラの非インターリーブテーブルとして定義されているとき。
2. 巨大なバッチ処理や集計クエリが、キーの局所性を無視して全スプリットをスキャン(Full Table Scan)しているとき。

我々が扱うのは、地球規模でスケールするトランザクションエンジンだ。その底力を引き出すも殺すも、君たちのスキーマ設計とSQLの書き方一つにかかっている。

次のデプロイでは、ネットワークのトラフィックグラフが美しく静まり返り、レイテンシが床を這うような、極限まで洗練されたシステムを見せてくれることを期待している。

コメント

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