Cloud Spannerのノード拡張で「即座にスケールしない」理由
スケーリング制限の動的再計算とリバランスの深層
「スパイクが来るから、直前にCloud Spannerのノード数を3から10に増やしておきました」
もし君がチームのインフラレビューでこう発言したなら、私はその場でデプロイを止めさせるだろう。そして白板の前に立ち、Spannerの分散アーキテクチャについて問い直すはずだ。
「ノード変更のAPIが `200 OK` を返した瞬間、内部で何が起きているか、本当に理解しているか?」
Cloud Spannerは「無制限にスケールする魔法のDB」と称される。しかし、それは物理法則と分散システムの原理を正しく理解して扱った場合に限られる。ノード数を変更した際、コンピュートリソースの動的再計算とデータ(Split)のリバランスが内部でどう処理されているのか。
今回は、一般的なリファレンスには絶対に書かれていない、Spannerのコアアーキテクチャの真実を解説する。これを理解すれば、本番環境でのパフォーマンススパイクや、スケールアウトしたにもかかわらずレイテンシが改善しないといったトラップを完全に回避できるようになるはずだ。
—
1. コンピュートとストレージの分離:スケーリングの前提知識
まず、Spannerの根幹を成す「コンピュートとストレージの完全分離」から復習しよう。この構造を脳内に描けていないと、以降の議論についてこられない。
+—————————————————————–+
| Compute Layer (Nodes) |
| +——————-+ +——————-+ +————–+ |
| | Compute Server 1 | | Compute Server 2 | | Compute N… | |
| | (Paxos Leader/Foll)| | (Paxos Leader/Foll)| | | |
| +——————-+ +——————-+ +————–+ |
+—————————————————————–+
| (gRPC / Dynamic Mapping)
+——————————-+———————————+
| Storage Layer (Colossus) |
| +———————————————————–+ |
| | Split 1 (Tablet) | Split 2 (Tablet) | Split 3 (Tablet) | |
| +———————————————————–+ |
+—————————————————————–+
- Compute Layer(ノード): SQLの解析、クエリプランの実行、トランザクションの調整、そしてデータ分割単位である「Split」のPaxosレプリカ(Leader/Follower)のタスクを実行する。
- Storage Layer(Colossus): データそのものはGoogleの分散ファイルシステム「Colossus」上に永続化されている。データは「Split」と呼ばれる暗黙的なキーレンジ単位で分割されている。
ここで重要なのは、ノードを追加してもColossus上のデータは1バイトも移動しないという事実だ。移動するのは「どのCompute Serverが、どのSplitの処理(Paxos Groupの役割)を担当するか」という割り当て権限(マッピング)だけである。
—
2. ノード変更時に内部で起きる4つのフェーズ
gcloudコマンドやTerraformでノード数を追加(またはProcessing Units: PUを増加)した際、コントロール平面とデータ平面では以下のステップが同期・非同期で噛み合って進行する。
[API Call: Set Nodes=10]
│
▼
【Phase 1: Comp-Pool Provisioning】── コンピュートノードの割当(即時〜数秒)
│
▼
【Phase 2: Dynamic Quotas Recalculation】── ノード毎のリソース制限再計算(即時)
│
▼
【Phase 3: Split-to-Node Remapping】── Location Serviceによる再配置(数十秒〜数分)
│
▼
【Phase 4: Load-based Split & Compaction】── 負荷に応じたSplit分離(数分〜数十分)
それぞれのフェーズで何が起きているのか、深掘りしよう。
Phase 1: コンピュートノードの割り当て
Googleのバックエンドクラスターから新しいコンピュートプロセスがアタッチされる。この時点では、新ノードは「何も担当していない空っぽのCPU/メモリ」に過ぎない。
Phase 2: リソース制限の動的再計算(Dynamic Quotas)
Spanner内部のレートリミッター(Dynamic Resource Throttling)が、新しいノード数に応じてインスタンス全体のスループット(IOPS/Bps)およびタスクキューのしきい値を動的に再計算する。
- 1ノード(1000 PU)あたり、推奨される高優先度CPU使用率の上限は65%(マルチリージョンは45%)。
- ノード数を増やすとインスタンス全体の「許容CPUキャパシティ」の計算値は即座に跳ね上がる。しかし、これは単なる計算上の枠組みが広がっただけに過ぎない。
Phase 3: Location Serviceによるマッピングの再初期化
ここからが本番だ。Spannerの「Location Service」と呼ばれるコンポーネントが、既存のコンピュートノードに過剰に集中しているSplitのPaxos Role(Leader/Follower)を、新しく追加されたノードへと再割り当てし始める。
このマッピング変更は非常に軽量だ。なぜなら、前述の通りColossus上のデータファイルへのポインタ(メタデータ)を新ノードに引き継ぎ、メモリ上のBlock Cacheを再構築し始めれば完了するからだ。
Phase 4: 負荷分散スプリットとホットスポットの解消
ここが最もエンジニアが誤解しているポイントだ。
マッピングが移動しても、「1つのSplit」は同時に「1つのコンピュートノード(の1コア)」でしか書き込み(Paxos Leader)を処理できない。
もし君のアプリケーションが特定の一意なキー(例: `UUID` ではなく `AUTO_INCREMENT` 風のタイムスタンプ)に書き込みを集中させている場合、データは単一のSplitに閉じ込められている。ノードを100個に増やそうが、そのホットなSplitを担当するノードはたった1つだ。
Location Serviceは負荷を検知し、単一のSplitを2つに分割(Load-based Split)しようとする。しかし、このスプリットの実行とコンパクション、および新ノードへの移送には数分から数十分の実行時間を要する。
> 結論: ノードを増やしても、既存の負荷がSplit分割の閾値を超えて分散されるまでは、スケールアウトの効果は100%発揮されない。
—
3. 実務で遭遇する「スケーリング制限」の罠
コードレビューやアーキテクチャレビューで私が必ず指摘する、Spannerのスケーリングに纏わるアンチパターンを共有する。
罠1: トラフィック急増直前の「駆け込みスケール」
「セールが10分後に始まるからノードを3倍にしよう」――これは失敗する。
ノードの追加自体は数十秒で終わるが、新規ノードのBlock Cache(データキャッシュ)は空(Cold)の状態だ。
さらに、新しいノードにSplitが再配置された直後は、キャッシュミスが多発してColossusへのI/Oが急増するため、一時的にレイテンシが悪化(レイテンシスパイク)する。
正しい設計パターン:
負荷ピークの最低でも30分〜1時間前にスケールアウトを完了させておき、ウォームアップクエリを投げてキャッシュを温めるか、事前スプリットを仕込んでおくこと。
罠2: スケールダウン時の CPU Over-utilization と 優先度倒錯
不要になったノードを減らす「スケールイン」時にも猛烈な罠が存在する。
ノード数を減らした瞬間、Phase 2の「リソース制限再計算」が走り、許容できるCPUキャパシティの分母が急激に縮小する。担当していたSplitは残されたノードへ押し込められる。
この時、バックグラウンドでは「Splitの統合(Merge)」と「ノード間のデータ移動」という高負荷な内部処理が走る。
もし、CPU使用率が80%を超えている状態でノードを減らすとどうなるか?
Spanner内部の「Priority Scheduler」が働き、高優先度(High Priority)のOLTPクエリですら、リソース再計算によるスロットリングの影響を受けてタイムアウトし始める。
—
4. プロのための運用コードとモニタリング技法
実務において、Spannerのリバランスとスケーリングを制御・監視するための具体的なアプローチを示す。
① インフラ自動化におけるスケール処理の冪等性とウェイト
Terraform等でノード数を変更する場合、単にAPIを発行して終わりにしてはならない。`gcloud` や Cloud SDK を使って、リバランスの安定化を待つスクリプトをCI/CDパイプラインに組み込むべきだ。
!/usr/bin/env bash
Spannerのノード変更と安全なスプリット安定化の監視スクリプト
INSTANCE_ID=”my-spanner-instance”
TARGET_NODES=6
PROJECT_ID=”my-gcp-project”
echo “[INFO] Instigating scale-out to ${TARGET_NODES} nodes…”
gcloud spanner instances update ${INSTANCE_ID} \
–nodes=${TARGET_NODES} \
–project=${PROJECT_ID}
echo “[INFO] Node provisioning API complete. Entering warm-up / balance window…”
単純な sleep ではなく、CPUのHigh Priority Utilizationが
安定しきい値(例: 50%以下)に落ちつくまでメトリクスをポーリングする
CHECK_INTERVAL_SEC=30
MAX_ATTEMPTS=10
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
# Google Cloud Monitoring API から 直近5分間の High Priority CPU を取得 (疑似ロジック)
HIGH_PRIO_CPU=$(gcloud monitoring time-series list \
–filter=”metric.type=\”spanner.googleapis.com/instance/cpu/utilization_by_priority\” AND resource.labels.instance_id=\”${INSTANCE_ID}\” AND metric.labels.priority=\”high\”” \
–format=”value(points[0].value.doubleValue)” –limit=1)
echo “[INFO] Current High-Priority CPU: ${HIGH_PRIO_CPU}”
# CPU使用率が閾値(例: 0.5 = 50%)を下回り、かつノード配置が安定したかをチェック
# (実際には 0.5 未満かつ十分な時間経過を確認)
if (( $(echo “${HIGH_PRIO_CPU} < 0.5" | bc -l) )); then
echo "[SUCCESS] Spanner cluster has stabilized after scaling."
exit 0
fi
ATTEMPT=$((ATTEMPT + 1))
sleep ${CHECK_INTERVAL_SEC}
done
echo "[WARNING] Target stabilization time window exceeded. Proceed with caution."
② Split状態を可視化するシステムテーブルのクエリ
現在、自社のテーブルが正しく分散されているか、あるいは特定のSplitに負荷が偏っていないかを調べるには、`INFORMATION_SCHEMA` や `SPANNER_SYS` (Introspect機能)を叩く必要がある。
以下のクエリは、CPU使用率が高いSplitと、その要因となっているキー範囲の傾向を掴むための洞察を与える。
— 特定のテーブルにおけるロックコンテンションや負荷の偏りを検知するクエリ
— (SPANNER_SYS.LOCK_STATS を参照して、特定Split/Keyへの集中を割り出す)
SELECT
row_range_start_key,
lock_wait_seconds,
cumulative_lock_wait_seconds
FROM
SPANNER_SYS.LOCK_STATS_TOP_MINUTE
WHERE
table_name = ‘Orders’
ORDER BY
cumulative_lock_wait_seconds DESC
LIMIT 10;
/
[コメント]
このクエリ結果で特定の `row_range_start_key` にロックウェイトが集中している場合、
ノードを増やしてもそのキー範囲(Split)を担当する単一ノードに負荷が集中する。
対策として、PKの先頭にハッシュ値を付与する(Hash Prefixing)などのスキーマ変更が必要。
/
—
5. まとめ:Spannerを「乗りこなす」ための鉄則
Cloud Spannerは極めて論理的な分散システムだ。ノードスケーリングにおけるリソースの再計算とリバランスの仕組みをまとめると、以下の3点に集約される。
1. ノード追加=コンピュート枠の確保に過ぎない
Colossus上のデータ割り当て(SplitのPaxos Role)が再配置され、キャッシュが温まるまでには物理的なタイムラグが存在する。スパイク対策のスケールアウトは事前(30〜60分前)に行え。
2. ホットスポットはスケーリングを無効化する
1つのSplitの書き込み処理は、物理的に1つのノード(1コア)の限界を超えることができない。ノード数を増やす前に、主キー設計(シャーディング・ハッシュプレフィックス)でSplitが均等に分散する構造を作れ。
3. スケールインはスケールアウトよりも危険である
ノードを減らすと、リソース限界値(Quota)が即座に切り下がり、裏でSplitの統合とデータ移
コメント