リソースクォータの要塞:Cloud Spannerの内部制御と「スケールの罠」を避ける設計哲学
テックリードの私だ。コードレビューや設計レビューで、こんな質問をしてくる若手が後を絶たない。
- 「Spannerってオートスケーリングするんですよね? じゃあストレージもノードも無限に増やしていいんですか?」
- 「バッチ処理で一気にデータを流し込んだら、なぜかクォータ制限で弾かれたんですが、どうしてですか?」
……甘い。Google Cloudのマネージドサービスだからといって、リソースが魔法のように無限に湧き出るわけではない。Cloud Spannerの背後には、厳格で、しかし極めて合理的な「リソースクォータ管理システム」が稼働している。
今回は、Spannerのコアアーキテクチャの深層に潜り込み、プロジェクト単位でのノード数やストレージ容量の制限がどのように管理され、スケーリング時にどのようなチェックプロセスが走っているのかを徹底的に解説する。これを理解していなければ、本番リリース直前の負荷試験で致命傷を負うことになる。覚悟して読み進めてほしい。
—
1. Cloud Spannerリソース管理の全体像:分散世界を統べる「メタデータ・オーケストレーター」
まず大前提として、Cloud Spannerは単一の巨大なデータベースではない。数千、数万の商用マシーンにまたがる「分散ストレージ&コンピューティングの要塞」だ。
データを保持する最小単位であるスプリット(Split)は、Paxosグループによって冗長化され、複数ノード(Compute Capacity)上でダイナミックにルーティングされる。この動的な世界において、誰がリソースの限界を管理しているのか?
それが、グローバルなメタデータ・プレーン(Metadata Plane)と連携するクォータ・コントローラー(Quota Controller)だ。
[Client Application]
│
▼ (gRPC / SQL)
[Spanner Frontend]
│
├──► [Metadata Plane / Quota Controller] (制限チェック & 予約)
│ │
│ ├─ ノード数上限 (Compute Capacity Quota)
│ ├─ ストレージ容量上限 (Storage Quota)
│ └─ スプリット数上限 (Split Quota)
│
▼
[Spanner Storage / Compute Nodes (Paxos Groups)]
クォータの管理は、単に「現在の使用量をカウントする」という単純なものではない。「将来のスケーリングやリバランスを見越した予約・検証システム」として機能している。
—
2. ノード数(Compute Capacity)とストレージ容量の強制メカニズム
リソースクォータの制限値は、GCPプロジェクトおよびリージョン(またはマルチリージョン)単位で厳格に定義されている。
ノード数 / 処理能力(Processing Units)の制限
Spannerでは、従来の「ノード」単位に加え、「処理ユニット(Processing Units: PU)」単位(100 PU単位、最小100 PU)での細粒度な割り当てが可能になった。
このコンピューティングリソースの変更(スケールアップ・スケールダウン)を行う際、内部では以下のプロセスが走る。
1. プリ・チェック(Pre-check): 変更リクエストが来ると、Quota Controllerがプロジェクト全体のPU上限(Soft Limit / Hard Limit)に対して検証を行う。
2. キャパシティ確保(Capacity Reservation): 指定されたPUを収容できる物理/仮想ホスト群(ColossusのストレージノードとSpannerのコンピュートノード)の空き状況を確認し、リソースをアトミックに予約する。
3. トランスポーテーション(Rebalancing): 予約が成功すると、スプリットの配置がバックグラウンドで再計算され、新しい処理能力へと徐々に移行していく。
ここで重要なのは、「スケールアウトには物理的な制限(物理クラスタの空き容量やハードウェアリミット)が存在する」という点だ。特にホットスポットが発生している高負荷な状態で急激にノードを追加しようとすると、リバランス処理自体がスパイクを引き起こす原因になる。
ストレージ容量の制限と「見えないコスト」
ストレージについても同様だ。Spannerのストレージは、Googleの分散ファイルシステム「Colossus」上に構築されており、実質的な容量上限は極めて高い。しかし、プロジェクトごとのクォータは厳しく制限されている。
現場で最もよくある勘違いがこれだ。
> 「テーブルのデータサイズが100GBだから、ストレージも100GB分しか消費していないはず」
大間違いだ。 Cloud Spannerのストレージクォータには、以下の「見えない容量」がすべて含まれる。
- データ本体のサイズ
- セカンダリインデックスのサイズ(インデックスを張れば張るほどストレージを消費する)
- マルチバージョン同時実行制御(MVCC)による古いバージョンのデータ(トランザクションが長引いたり、バックアップ・ポイントインタイムリカバリ(PITR)を有効にしている場合、古い世代のデータがストレージを圧迫する)
- Paxosログのオーバーヘッド
特にPITR(最大7日間)を有効にしている場合、「実データの数倍のストレージがバックグラウンドで維持される」ことを忘れてはならない。ストレージクォータの監視を怠ると、ある日突然、書き込みが一切できなくなる(`RESOURCE_EXHAUSTED` エラーの頻発)という悪夢を見る。
—
3. スケーリング時の制限チェックプロセスと「アトミック性」
では、ユーザーがインスタンスを拡張(スケール)させたり、データ量を爆発的に増やしたりするとき、内部の制限チェックはどのようなステップで行われるのか。
その裏側では、2段階コミット(2PC)に類似したリソース確保のトランザクションが実行されている。
[ユーザーの変更リクエスト]
│
▼
[Step 1: グローバル・クォータ・アサーション]
- プロジェクトのクォータ枠内か?
- リージョンごとのハードリミットに抵触していないか?
│ (OK)
▼
[Step 2: リソース・ロックと予約]
- 対象インスタンスのコンピュート/ストレージ領域のロック
- 物理ノードプールでのリソース確保
│ (OK)
▼
[Step 3: コミット & 動的再構成]
- メタデータの更新
- スプリットのマイグレーション開始
もし、このプロセスの途中でプロジェクトのクォータ上限に達した場合、操作は即座にロールバックされ、APIはエラーを返す。
具体例:Terraform等でのインフラ構成管理における罠
IaC(Terraformなど)でSpannerのノード数を動的に変更する際、次のようなコードを書くことがあるだろう。
resource “google_spanner_instance” “production” {
config . = “regional-asia-northeast1”
display_name = “core-db”
processing_units = 2000 # いきなり2000 PUにスケールアップ!
}
このコードを実行したとき、もしプロジェクトのPU上限が「1000 PU」であれば、Terraformは容赦なくエラーを吐いて停止する。
さらに厄介なのは、「オートスケーリング機能(推奨されていないがカスタム実装する場合など)」だ。負荷が高まった瞬間に自動でノードを増やそうとしても、プロジェクトのクォータ上限に阻まれてスケールアウトに失敗し、システム全体が雪崩式にダウンする「スケーリング・デッドロック」が発生し得る。
—
4. 堅牢な設計パターン:クォータ制限を突破し、生き残るためのアーキテクチャ
この厳格なリソース管理の仕組みを踏まえ、我々テックリードは設計段階でどう立ち回るべきか。実務で使える3つのデザインパターンを授けよう。
パターンA: プロジェクト・分離の原則(Quota Isolation)
巨大なシステムを単一のGCPプロジェクトに押し込んではならない。
マイクロサービスごとにプロジェクトを分ける、あるいは「トランnザクション系」「バッチ系」でSpannerインスタンスのプロジェクトを完全に分離せよ。
これにより、「夜間の巨大バッチ処理がストレージとコンピュートを食い潰し、昼間のフロントエンド用オンラインDBがクォータ枯渇で死ぬ」という最悪の障害を防ぐことができる。
パターンB: プロアクティブなクォータ監視とアラート(Capacity Monitoring)
「エラーが出てから気づく」のではプロ失格だ。Cloud Monitoringを使い、以下のメトリクスを厳しく監視しろ。
- `container/cpu/utilization` (CPU使用率)
- `storage/used_bytes` (ストレージ使用量)
- クォータ使用率(Service Quotasのダッシュボードからのアラート設定)
特にストレージは、PITRの保持期間(最大7日)を考慮し、「現在の増加ペースで何日後にクォータの80%に達するか」を予測するアラートを仕込んでおくこと。
パターンC: スパイクを想定したバッチ設計(Throttling & Batch Sizing)
大量データの一括インサート(Bulk Ingestion)を行う際、制限なくデータを流し込むのは愚行だ。Spannerのストレージクォータと書き込みスループットの制限(Hotspottingの回避を含む)に配慮し、アプリケーション層でスロットリングを実装せよ。
// Goによる適切なレートリミットを考慮したバルクインサートの概念コード
package main
import (
“context”
“time”
“cloud.google.com/go/spanner”
)
func BulkInsertWithThrottling(ctx context.Context, client spanner.Client, mutations []spanner.Mutation, batchSize int) error {
// クォータ急増とホットスポットを防ぐため、適切に分割してミューテーションを投入
for i := 0; i < len(mutations); i += batchSize {
end := i + batchSize
if end > len(mutations) {
end = len(mutations)
}
currentBatch := mutations[i:end]
_, err := client.Apply(ctx, currentBatch)
if err != nil {
// エラーハンドリング(RESOURCE_EXHAUSTEDの場合は指数バックオフでリトライ)
return err
}
// ストレージやスプリットの急速な肥大化を緩和するためのウェイト
time.Sleep(50 time.Millisecond)
}
return nil
}
※実務では、単なるスリープだけでなく、Cloud Spannerのクライアントライブラリが持つバックオフ機構や、エラーコード `RESOURCE_EXHAUSTED`(gRPC Status 8)を検知した際のリトライ戦略(Jitter付きExponential Backoff)を必ず組み込むこと。
—
5. チーフアーキテクトからの最終提言
Cloud Spannerのリソースクォータ管理の仕組みは、単なる「利用制限のお巡りさん」ではない。それは、世界中に分散した巨大なストレージとコンピュートの整合性を保ち、グローバルスケールのトラフィックからインフラストラクチャを守るための「極めて洗練された安全弁」だ。
この安全弁が機能する原理(メタデータ・プレーンでの予約、MVCCやPITRを含めた実ストレージの肥大化、スケーリング時のアトミックなチェック)を理解していれば、もはや「なぜエラーが出たのか分からない」と慌てることはなくなるはずだ。
設計とは、制限を知った上で、その境界線の内側でいかに美しく堅牢なシステムを構築するかである。
次のコードレビューでは、この知見をもとに後輩たちの設計書の不備を論理的に指摘してやってほしい。健闘を祈る。
コメント