【実務・中級編】 スプリット (Split) – Cloud Spanner

Cloud Spannerの真髄:「スプリット(Split)」を支配する者がSpannerを支配する

こんにちは。テクニカルリードの私だ。
これまでに数々の大規模分散データベースを設計・運用してきたが、Cloud Spannerほど「物理法則をソフトウェアで美しく抽象化した」プロダクトは他にない。

しかし、レビュー会でこう言うジュニアエンジニアに何度も遭遇した。
「Spannerはフルマネージドだから、データの配置なんて気にしなくていいんですよね?」

……甘い。非常に甘い。
Cloud Spannerは魔法の箱ではない。背後では厳格な物理法則(分散と合意形成)が働いており、その最小管理単位である「スプリット(Split)」の挙動を理解していないシステムは、高負荷時に必ず崩壊する。

今回は、Spannerの心臓部である「スプリット」の正体を剥き出しにし、実務で絶対に踏んではいけない地雷と、極限までパフォーマンスを引き出す設計パターンを伝授しよう。

—

1. スプリットとは何か?:分散の最小原子

まず、教科書的な定義を軽くおさらいしておこう。
スプリットとは、テーブルの行範囲(Row Range)に基づいて動的に分割されたデータの単位であり、Paxosグループ(複数のストレージノード)によって管理される。

Spannerのストレージは、無限の海のようにシームレスに広がっているわけではない。実際には「バイト列のソート済みマップ」であり、これが一定のサイズ(一般的には数GB〜数十GB)や負荷を超えると、システムが自動的に真っ二つに「スプリット」する。

[ Row A ~ Row M ] –> ひとつのスプリット (Node 1)
——————– <-- スプリット境界 [ Row N ~ Row Z ] --> ひとつのスプリット (Node 2)

このスプリットこそが、Spannerがペタバイト級のデータ量と数百万QPSを同時に捌ける理由だ。しかし、この「自動分割」という特性こそが、設計を誤った瞬間に悪夢へと変わる原因になる。

—

2. 実務で遭遇する「最悪のスプリット設計」:ホットスポットの罠

コードレビューをしていて、最も背筋が凍るスキーマ定義がこれだ。

— 【アンチパターン】典型的なホットスポット製造機
CREATE TABLE Transactions (
TransactionID STRING(36), — UUID (ランダム) だが……?
CreatedAt TIMESTAMP, — タイムスタンプ
UserID STRING(64),
Amount INT64,
) PRIMARY KEY(CreatedAt, TransactionID); — ★最悪のプライマリキー構成

何が問題か分かるかね?
`CreatedAt`をプライマリキーの先頭(第1キー)に置いた瞬間、すべての書き込みは「現在時刻」という単一のスプリットに集中する。

スプリットの分断と「追従」の悲劇

  • `CreatedAt`の範囲でスプリットが切られているとする。
  • 今まさに書き込まれているデータは、「現在(いま)」を保持する最新のスプリットにしか流し込まれない。
  • Spannerは負荷に応じてスプリットを分割しようとするが、「常に末尾(現在時刻)にのみ書き込みが集中する」というワークロードでは、スプリットを分割しても新しいスプリットの末尾に負荷が移動するだけであり、負荷分散(ロードバランス)が機能しない。
  • 結果として、たった1つのスプリット(ノード)がCPU限界を迎え、レイテンシが跳ね上がり、エラー(`DEADLINE_EXCEEDED` や `RESOURCE_EXHAUSTED`)が連鎖する。これがホットスポットの正体だ。

—

3. 堅牢な設計パターン:スプリットを「意図的に誘導する」

では、どう設計すべきか。
スプリットの特性をハックし、書き込みを物理的に異なるノード(スプリット)へ強制的に分散させる設計パターンを授けよう。

パターンA:プレフィックス・シャドーイング(ハッシュ分散)

キーの先頭に数ビットのランダムなプレフィックス(あるいはハッシュ値)を付与し、強制的にスプリットの範囲を散らせる手法だ。

— 【推奨設計】マルチテナントや高スループット書き込み向け
CREATE TABLE Transactions (
ShardID INT64, — 0から7程度のハッシュ値(プレフィックス)
TransactionID STRING(36),
CreatedAt TIMESTAMP,
UserID STRING(64),
Amount INT64,
) PRIMARY KEY(ShardID, CreatedAt, TransactionID);

なぜこれが効くのか?
`ShardID` が `0` から `7` まで存在する場合、データは最初から最低8つの異なるスプリット範囲に分割される。書き込みが時系列であっても、`ShardID` によって異なるスプリット(=異なるPaxosグループ)にルーティングされるため、ハードウェアの限界を超えるスケールアウトが可能になる。

—

4. パフォーマンス上の注意点:スプリット管理のコスト

「じゃあ、全部のテーブルを細かくシャードして、スプリットを大量に作れば最強だな?」と思ったそこの君。それはエンジニアの悪手だ。

スプリットには管理コスト(メタデータのオーバーヘッドとPaxosの合意形成コスト)が伴う。

1. スプリットの数とメタデータ
数百万個もの過剰に細かいスプリットが存在すると、Spannerの内部メタデータ管理に負荷がかかり、スキーマ変更(DDL)やバックアップ、リカバリの速度が低下する。
2. スプリットの移動(スプリット・マイグレーション)
リーダーノードの負荷偏在やオートスケーリングに伴い、スプリットは裏側で別の物理ノードへ移動する。この瞬間、ごくわずかだがルーティングのオーバーヘッドやレイテンシの揺らぎが発生する。
3. インターリーブ(Interleave)テーブルとスプリットの局所性
親子関係を持つテーブルを `INTERLEAVE IN PARENT` で定義した場合、子テーブルの行は親テーブルの行と同じスプリット(あるいは極めて近傍)に物理的に配置される。これはJOINや範囲検索のレイテンシを劇的に下げるが、親レコードへの偏りがそのまま子レコードのホットスポットに直結することを意味する。

— 親子インターリーブの例:Parentのキー設計が生死を分ける
CREATE TABLE Customers (
CustomerID INT64,
Name STRING(100),
) PRIMARY KEY(CustomerID);

CREATE TABLE Orders (
CustomerID INT64,
OrderID INT64,
OrderDate DATE,
) PRIMARY KEY(CustomerID, OrderID),
INTERLEAVE IN PARENT Customers ON DELETE CASCADE;

もし `Customers` の `CustomerID` がシーケンシャルな連番であれば、親も子もすべて同じ末尾のスプリットに吸い寄せられ、見事なホットスポットが完成する。`CustomerID` にも UUIDv4 やハッシュ化されたIDを採用すべきだ。

—

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

Cloud Spannerを使ったシステムのパフォーマンスチューニングにおいて、スプレッドシートやモニタリングツール(Cloud Monitoring)で見るべき指標は一つだ。

> 「CPUUtilization(CPU使用率)のノード間偏り」

もし特定のCPUコアやノードだけが悲鳴を上げていたら、それはコードのバグではない。「スプリットの配置と、キースマホ(Key Space)の設計ミス」だ。

設計レビューの際は、常にこう自問自答してほしい:

  • 「このプライマリキーの先頭カラムは、書き込み時に値が単調増加、あるいは集中していないか?」
  • 「想定されるピークQPSに対して、データは適切に複数のスプリットに分散するスキーマになっているか?」

スプリットの物理的挙動を脳内に描けるようになった時、君はもう、ただの利用者ではない。真のDistributed Systems Engineerだ。

次の設計レビューで、その甘いスキーマが修正されていることを期待している。さあ、コードを書こう。

コメント

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