【実務・中級編】 分散実行エンジンの仕組み – Cloud Spanner

【Cloud Spannerの核心】分散実行エンジンのメカニズムと、オプティマイザを味方につけるクエリ設計の極意

こんにちは。テックリードの私だ。
コードレビューや設計レビューの場において、Cloud Spannerを「無限にスケールするマネージドRDB」というふんわりとした理解のまま使い倒そうとし、痛い目を見ているエンジニアを後を絶たない。

「なぜこのクエリはレイテンシが大きいのか?」
「なぜCPU使用率が特定のノードだけ跳ね上がるのか?」

その答えの9割は、Spannerの心臓部である「分散実行エンジン(Distributed Query Execution Engine)」の挙動を理解しているかどうかにある。

今回は、Spannerが裏側でどのようにクエリを砕き、分散させ、組み立て直しているのか。そのコアアーキテクチャの深淵を覗き、実務で絶対に守るべき設計パターンを授けよう。

—

1. 分散実行エンジンのアーキテクチャ:クエリはいかにして「砕かれ」、並列化されるか

リレーショナルデータベースの多くは、単一のノード(あるいは単一のリーダー)上でクエリの最適化と実行を行う。しかし、数テラ・数ペタバイトのデータをグローバルに分散配置するCloud Spannerにおいて、単一ノードでクエリを処理するなど物理的に不可能だ。

Spannerのクエリは、「分散パイプライン・アーキテクチャ」によって処理される。そのライフサイクルは以下の4つのフェーズで構成されている。

[Client] —> (SQL) —> [Root Coordinator / Mixer]
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
[Split 1 (Node A)] [Split 2 (Node B)] [Split 3 (Node C)]
│ │ │
└───────────────────────┼───────────────────────┘
▼
[Distributed Merge/Reduce]
│
▼
[Client (Result)]

① パースと分散オプティマイザ(Distributed Query Planner)

クライアントから送られたSQLは、まずRoot(あるいはRootに近いノード)のクエリコーディネーターでパースされる。Spannerのオプティマイザは、データの物理的な配置(Splitの境界、リーダーノードの位置)を把握しており、「データを動かすな、計算をデータに寄せろ(Compute to Data)」という原則に基づき、実行計画(Execution Plan)をツリー構造として構築する。

② クエリの分割とサブタスク化

オプティマイザは、単一の巨大なクエリを「サブクエリ(分散タスク)」に分解する。テーブルが「Split(スプリット)」と呼ばれる数GB単位の物理的なデータ断片に分割されているのに対し、Spannerは各Splitを保持するストレージ/コンピュートノードに対して、スキャンやフィルタリング、部分的な集約(Partial Aggregation)の命令を並列でディスパッチする。

③ パイプライン化された分散実行(Distributed Pipeline Execution)

ここがSpannerの真骨頂だ。各ノードは、受け取ったサブタスクをメモリ上でパイプライン処理し、結果を待たずにストリーミング形式で上位のノードへ送り出す。
MapReduceのようなバッチ指向ではなく、非同期かつストリーミングベースの分散データフローであるため、数百万行のレコードであっても極めて低いレイテンシで処理が開始される。

④ 分散マージとリダクション

各ノードから送られてきた断片的な結果は、ツリーの中間ノードやルートノードでマージされる。`ORDER BY` や `GROUP BY`(Final Aggregation)、`LIMIT` などの処理は、分散環境で効率よく再結合(Reduce)される。

—

2. 【実務の現場から】分散実行をブチ壊す「アンチパターン」と、その処方箋

この美しい分散エンジンの挙動を理解していれば、レビュー時に「やってはいけないクエリ」が直感的に分かるはずだ。

アンチパターン①:分散クロスアグリゲーション(全ノードへの負荷集中)

何百万行ものトランザクションログに対し、非効率な結合や不適切なグループ化を行った場合、ルートノードで最終的なマージを行う前に、ネットワーク帯域とメモリが枯渇する。

❌ 悪い例:分散を無視したフルスキャン・結合

— 巨大な orders テーブルと items テーブルを、インデックスなしで結合しつつ全件集計
SELECT
i.category,
SUM(o.amount)
FROM
orders o
JOIN
items i ON o.item_id = i.item_id
GROUP BY
i.category;

何が起きているか:
`orders` と `items` のスプリット配置が最適化されていない場合、全てのデータをネットワーク経由で結合ノードに寄せようとするため、「Distributed Cross-Apply(分散クロスアプライ)」が発生し、CPUとネットワークのボトルネック(Hotspotting)を引き起こす。

⭕ 正しい設計:インターリーブ(Interleave)と適切なプレフィックス

Spannerの真価を発揮させるには、物理的なデータ配置の段階でデータを近づけておく必要がある。

— 親子関係をインターリーブ定義し、同じSplit(あるいは近傍)に物理配置する
CREATE TABLE Customers (
CustomerID INT64,
— …
) PRIMARY KEY(CustomerID);

CREATE TABLE Orders (
CustomerID INT64,
OrderID INT64,
Amount INT64,
— …
) PRIMARY KEY(CustomerID, OrderID),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;

なぜこれが良いのか:
`CustomerID` をプライマリキーのプレフィックスに持ち、インターリーブされたテーブル設計にすることで、`Customers` とその子である `Orders` は物理的に同じノード(あるいは連続したSplit)に格納される。
これにより、分散エンジンはネットワークを跨ぐ高コストなシャッフル結合(Distributed Hash Join)を回避し、ローカルな局所結合(Locality-based Join)へと最適化できるのだ。

—

3. パフォーマンス劣化を見抜く:`EXPLAIN` との対話

設計レビューでは、必ず `EXPLAIN`(または `EXPLAIN ANALYZE`)の実行計画を出させること。Spannerのオプティマイザがどのようにクエリを分散させているかは、そこに全て現れる。

EXPLAIN
SELECT FROM Users WHERE Country = ‘JP’;

注目すべき実行計画のオペレーター:

  • Distributed Union: 複数のスプリット/ノードに対して並列でクエリをディスパッチし、結果をマージしている証拠。これが多段に深くなっている場合は注意が必要。
  • Full Scan vs Index Scan: 分散エンジンはインデックスがない場合、すべてのスプリットに対して愚直にフルスキャン(Distributed Full Scan)をかける。データ量が増えた瞬間にレイテンシが線形(あるいはそれ以上)に悪化するシグナルだ。
  • Distributed Cross Apply: 最も警戒すべきオペレーター。外側のクエリの1行ごとに、内側の分散クエリが走るため、N+1問題の分散版がデータベース内部で発生する。

—

4. チーフアーキテクトからの提言:設計レビューのチェックリスト

明日からのコードレビューで、君たちが部下や同僚に投げかけるべき質問をリスト化しておこう。

1. 「このクエリの実行計画(`EXPLAIN`)を確認したか?意図しない `Distributed Union` や `Full Scan` が起きていないか?」
2. 「結合するテーブル同士は、適切にプライマリキーが設計されているか、あるいはインターリーブ構造を活用できているか?」
3. 「ホットスポット(特定のノードに書き込みや読み込みが集中するキー設計)を生むような単調増加するキーを、分散クエリの条件に指定していないか?」

Cloud Spannerは魔法の箱ではない。しかし、その分散実行エンジンのメカニズムをリスペクトしたデータモデリングとクエリ設計を行えば、リレーショナルデータベースの堅牢性と、NoSQLをも凌駕する水平スケーラビリティを同時に手に入れることができる。

さあ、次の設計書を開き、そのクエリが美しく分散されているか厳しく見極めてくれ。

コメント

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