【Cloud Spanner深層】ノードリソースの限界とスロットリングの物理学:高負荷をいなす堅牢なアーキテクチャ設計
テックリードの私だ。今日のコードレビュー、あるいは次の大規模リリースに向けた設計レビューで、こんな質問を受けたとしよう。
「Spannerはマネージドだから、トラフィックが急増しても勝手にスケールするよね? なぜCPU使用率を65%未満に抑えなきゃいけないんだ?」
この問いに、公式ドキュメントのコピペで返すようなジュニアエンジニアは、うちのチームにはいらない。なぜなら、彼らは「Spannerが分散ストレージとコンピュートの境界で何をやっているか」「スロットリングが発動した瞬間に何が起きるか」を分かっていないからだ。
今日は、Cloud Spannerのコアアーキテクチャの深淵に潜り、CPU・メモリのハードリミット、そして過負荷時に発動する内部スロットリング(負荷制御)のアルゴリズムについて、実務で使える「生きた知見」を叩き込む。
—
1. Spannerの「リソースの正体」とスプリットの物理限界
まず大前提として、Cloud Spannerの「ノード(あるいは処理ユニット)」は、物理的な箱ではない。Paxosグループによって冗長化されたストレージと、それを叩くコンピュート(リージョン内でのプロセス)の抽象化された単位だ。
データはスプリット(Split)という単位に分割され、これがクラスタ内のノード間をダイナミックに移動する。
[クライアント] —> (RPC) —> [Spanner Frontend]
|
+————–+————–+
| (Spool / Query Execution) |
v v
[Split A (Leader)] [Split B (Leader)]
(Paxos Group 1) (Paxos Group 2)
ここで重要なのは、「1つのホットスポット(特定のスプリットへの集中)」は、クラスタ全体にどれだけCPU余力(ノード数)があっても、そのスプリットをホストしている単一のリーダーレプリカのCPUとメモリを枯渇させるという事実だ。
CPU使用率 65% の呪縛
Googleが「高負荷時のCPU使用率の推奨上限を65%(推論ワークロードの場合はもう少し低く)」とするのには、明確な物理的理由がある。
1. Paxosのクォーラム維持コスト: 書き込みが発生するたびに、リーダーはフォロワーとの間でネットワークラウンドトリップとディスク同期(Paxosログのコミット)を行う。CPUが飽和すると、この合意形成のレイテンシが跳ね上がる。
2. バックグラウンド処理の競合: Spannerのストレージエンジンは、LSMツリーベースの構造を持っており、バックグラウンドでコンパクション(Compaction)やガベージコレクションが走る。ユーザークエリがCPUを100%食い潰すと、コンパクションが追いつかなくなり、最終的にストレージ層で致命的なスローダウンを引き起こす。
—
2. スロットリング(負荷制御)のアルゴリズムと内部挙動
では、CPUやメモリが限界に達したとき、Spannerの内部では何が起きているのか?
システムはクラッシュする前に、アグレッシブなスロットリング(負荷制御)を発動する。
リクエストキューイングとレイアード・バックプレッシャー
Spannerのフロントエンドおよびコンピュート層には、リクエストを制御するための多段のキューとトークンバケットアルゴリズムが存在する。
1. トランスポート層 / 接続制限: gRPCのコネクション単位でのフロー制御。
2. クエリ/トランザクションスロットリング:
CPU使用率が閾値を超過し始めると、システムは内部のプライオリティキューに基づき、incomingなリクエストのadmission control(入場制御)を開始する。
- 低優先度(バッチ処理、アナリティカルクエリ): 即座に弾かれるか、極端に遅延させられる。
- 高優先度(インタラクティブなトランザクション): 可能な限り通そうとするが、キューイングによるレイテンシ増大(Queueing Delay)が発生する。
スロットリング発動時にクライアントが観測するエラー
耐えきれなくなったリクエストは、以下のステータスコードとして返却される。
- `RESOURCE_EXHAUSTED` (gRPC Status 8): 最も頻繁に見るエラーだ。「サーバー側がリソース不足で処理しきれない」ことを示す。
- `DEADLINE_EXCEEDED` (gRPC Status 4): キューに積まれたままタイムアウトを迎えたケース。
—
3. 現場で即座に使える! 堅牢な設計パターン
スロットリングやリソース枯渇を回避し、99.999%の可用性を叩き出すための設計パターンを授ける。
パターンA: モノトニックなキー設計の排除(ホットスポットの根絶)
これがすべての基本だ。インクリメンタルなIDや、現在時刻(`timestamp`)をプライマリキーの先頭に置く設計は、単一のスプリットに全トラフィックを集中させる「自爆テロ」に等しい。
【アンチパターン】
— 最悪の設計。常に最新のレコードに書き込みが集中し、1つのスプリットが即死する。
CREATE TABLE Events (
EventTime TIMESTAMP NOT NULL,
EventId STRING(64) NOT NULL,
Payload BYTES(MAX),
) PRIMARY KEY(EventTime, EventId);
【堅牢な設計(ハッシュプレフィックス / ビット反転)】
— キーの先頭にハッシュのモジュロを置き、書き込みを強制的に複数スプリットへ分散させる
CREATE TABLE Events (
ShardId INT64 NOT NULL, — 0 から 15 程度のハッシュ値
EventTime TIMESTAMP NOT NULL,
EventId STRING(64) NOT NULL,
Payload BYTES(MAX),
) PRIMARY KEY(ShardId, EventTime, EventId);
※テックリードのメモ: クエリ時に `ShardId` を指定し忘れるとフルスキャン(Cross-node scan)になるため、アプリケーション層でのルーティング設計とセットで実装すること。
パターンB: アプリケーション層でのジッター付き指数バックオフ
Spannerから `RESOURCE_EXHAUSTED` が返ってきたとき、クライアントが直ちにリトライ(Thundering Herd現象)を引き起こすと、Spannerの傷口に塩を塗ることになる。
Go言語による、正しいリトライハンドリングの実装例を示せ。
package main
import (
“context”
“math/rand”
“time”
“google.golang.org/grpc/codes”
“google.golang.org/grpc/status”
)
// IsSpannerRetryable はSpannerがスロットリングまたは一時的な競合で返したエラーかを判定する
func IsSpannerRetryable(err error) bool {
st, ok := status.FromError(err)
if !ok {
return false
}
// RESOURCE_EXHAUSTED や ABORTED (トランザクション競合) はリトライ対象
return st.Code() == codes.ResourceExhausted || st.Code() == codes.Aborted
}
// ExecuteWithBackoff はジッター付き指数バックオフで安全に操作を再試行する
func ExecuteWithBackoff(ctx context.Context, op func() error) error {
maxRetries := 5
baseDelay := 50 time.Millisecond
for i := 0; i < maxRetries; i++ { err := op() if err == nil { return nil } if !IsSpannerRetryable(err) || i == maxRetries-1 { return err } // 指数バックオフ + ランダムジッターの計算 (驚異的な負荷集中を防ぐ) // 2^i baseDelay + 乱数(0-50ms) multiplier := 1 << i jitter := time.Duration(rand.Intn(50)) time.Millisecond sleepDuration := time.Duration(multiplier)baseDelay + jitter select { case <-ctx.Done(): return ctx.Err() case <-time.After(sleepDuration): // 次のループへ } } return context.DeadlineExceeded } ---
4. パフォーマンス監視の急所:どこを見るべきか?
Cloud SpannerのCloud Monitoringで、システムがスロットリングの危機にあるかを察知するための「真のKPI」は以下の3つだ。
1. High Priority CPU Utilization:
ユーザーからのトランザクション処理に割り当てられているCPU。これが 65% を超えたら黄色信号、80% を超えたらスロットリングによるレイテンシ劣化が確実に発生している。
2. Transaction Abort Rate:
コンテンション(競合)によるアボート率。CPU負荷とは別に、同じ行に対する同時更新が多すぎるとここが跳ね上がる。
3. Storage Byte Read/Write Amplification とスプリットごとのCPU偏り:
クラスタ全体のCPUが低く見えても、特定のスプリットだけが100%に張り付いていないか(Hotspotting)をCloud Monitoringのディメンション(`database_id`, `table_id`)で必ずドリルダウンして確認しろ。
—
結びに代えて
Cloud Spannerは魔法のデータベースではない。物理法則(ネットワークの速度、ディスクのI/O、CPUの演算能力)の制約を、巧妙な分散アルゴリズムで極限まで隠蔽しているに過ぎない。
その隠蔽の限界点――それが今日解説した「ノードリソース制限とスロットリング」だ。
アーキテクチャの裏側で何が起きているかを理解していれば、不意のトラフィック増にも慌てず、美しくスケールするシステムを設計できるはずだ。
次のコードレビューでは、キー設計の甘さや、リトライ戦略のないコードを見つけたら、容赦なく差し戻してやってほしい。それが、プロのエンジニアの仕事だ。
コメント