【Spannerの深層】バックグラウンド処理の呪縛を断つ:リソース優先順位付けとマルチテナント設計の極意
テックリードの私だ。コードレビューや設計レビューで、「なぜこのクエリが突然レイテンシのスパイクを起こしたのか?」という質問をよく受ける。
Cloud Spannerは「ほぼ無限のスケール」「99.999%の可用性」「外部整合性(Serializable)」という、データベースエンジニアの夢を具現化したようなプロダクトだ。しかし、この魔法のようなシステムも、物理法則からは逃れられない。CPU、メモリ、ディスクI/O、そしてネットワーク帯域という有限のリソースの上で動いている。
特に、SLAの厳しいユーザーリクエスト(OLTP)が走る同じノード上で、分散ガベージコレクション(GC)、ストレージのコンパクション(Compaction)、Paxosグループの再平衡化(Rebalancing)、そしてバックアップやスキーマ変更といったシステム起因のバックグラウンドメンテナンスが同時に実行されているとしたらどうだろう?
今回は、Cloud Spannerのコアアーキテクチャの心臓部である「リソース優先順位付け(Resource Prioritization)」のメカニズムを解剖し、高負荷な実務環境においてシステムを破綻させないための設計パターンを伝授する。
—
1. アーキテクチャの真実:なぜバックグラウンド処理が勝手に暴走しないのか?
Cloud Spannerのストレージ層(SplitsとCallistoと呼ばれる分散ストレージスタック)は、マルチテナントかつ高密度なワークロードを前提に設計されている。1つの物理ノード(あるいはノードプール)上で、数千もの異なるSplit(データの断片)がホストされ、それぞれが独立してリーダーやフォロワーの役割を担う。
ここで発生するのが、「ユーザーリクエスト vs バックグラウンド処理」の資源闘争だ。
Spannerの内部エンジンは、この闘争を解決するために高度なプリエンプション(横取り)機構と帯域制御(Throttling)を実装している。
優先度制御のレイヤー
1. ユーザーインタラクティブ・リクエスト (最高優先度)
- クライアントからの読み取り・書き込みトランザクション。レイテンシ(p99/p99.9)がビジネスの直結する指標であるため、リソースの割当において圧倒的な優先権を持つ。
2. 内部レイテンシセンシティブ・タスク (中優先度)
- Paxosのハートビートや、レプリカ間のキャッチアップなど、可用性と一貫性を維持するための制御トラフィック。
3. バックグラウンド・メンテナンス (最低優先度)
- LSMツリー(Log-Structured Merge-tree)ベースのストレージにおける不要領域の回収(GC)、SSTableのコンパクション、およびバックアップ・エクスポート処理。
バックグラウンド処理は、CPUやI/O帯域の「余剰分(Headroom)」を日和見的に(Opportunistically)貪るように設計されている。もしユーザーリクエストが急増した場合、バックグラウンド処理は即座にスロットリングされ、リソースを譲る。
……だが、設計を誤ると、この「譲り合いのメカニズム」が破綻する。 その典型例が、悪意ある(あるいは無知な)データモデリングによる「ホットスポット」と「巨大なトランザクション」だ。
—
2. 現場で起きる悲劇:なぜ優先順位付けが機能しなくなるのか?
理論上はバックグラウンド処理が控えめにしてくれるはずのSpannerだが、実務の現場ではしばしば「予期せぬレイテンシの跳ね上がり」に直面する。その原因の多くは、リソース優先順位付けの前提条件を開発者が破壊していることにある。
悲劇のパターン:モノリス的バッチと高頻度スキーマ変更のコンボ
深夜帯に大規模なバッチ処理(大量のINSERT/UPDATE)を流しつつ、同時に日中の蓄積データのパージ(古い行の削除)をバックグラウンドGCに任せたとする。
Spannerのストレージ構造上、大量の行削除は即座にディスク容量を空けるわけではなく、墓場(Tombstone)マークが付与され、その後のバックグラウンドコンパクションによって初めて物理領域が回収される。
この「削除フェーズ」において、バックグラウンドのI/O消費が跳ね上がり、たとえ優先順位付けが働いていたとしても、物理ディスクのI/OPS(Input/Output Operations Per Second)やメモリのページキャッシュが飽和すると、ユーザーリクエストのレイテンシが直接的なダメージを受ける。
—
3. 堅牢な設計パターン:リソース競合を回避する実務アプローチ
では、テックリードとして我々はコードやスキーマをどう設計すべきか? 3つの実践的なパターンを提示する。
パターンA:削除は「バッチ」ではなく「パーティション単位のdrop」で制する
数千万行のデータを消すとき、`DELETE FROM table WHERE timestamp < ...` をトランザクションでループさせてはいけない。これはストレージに膨大なコミットログとコンパクション負荷を強制し、バックグラウンド処理を過労死させる。
【推奨設計】
データ構造を日付やテナントIDごとにテーブルを分割(あるいはテーブル・グループを活用)し、不要になったデータはテーブルの切り離し、またはパーティション単位の操作(Spannerでは直接的なパーティションドロップはないが、効率的な範囲削除戦略を用いる)で処理する。
どうしても行単位で消す場合は、スループットをスロットリングし、ストレージのコンパクションキューが溢れないようにレート制御をクライアント側で実装せよ。
パターンB:ホットスポットを作らない主キー設計(バックグラウンドの再平衡化を誘発させない)
意外に見落とされがちなのが、「データの偏りが引き起こすバックグラウンド負荷」だ。
連番IDや、常に増加するタイムスタンプを主キーの先頭に置くと、特定のSplitに書き込みが集中する(ホットスポット)。Spannerは自動的にSplitの分割と移動(Load-based Splitting)を行うが、このリバランス処理自体がバックグラウンドのネットワークとCPUを大量消費する。
— 【悪手】常にインクリメントする値が先頭にある主キー
— 書き込みが単一のSplitに集中し、Splitの分割・移動(バックグラウンド処理)が頻発する
CREATE TABLE BadOrders (
OrderId INT64 NOT NULL,
CustomerId INT64 NOT NULL,
OrderData STRING(MAX),
) PRIMARY KEY(OrderId);
— 【正解】ハッシュプレフィックスやUUIDv4(またはビット反転)による分散
— 書き込みが全ノード・全Splitに均等に散らばり、リソースの偏りが消える
CREATE TABLE GoodOrders (
ShardedId STRING(64) NOT NULL, — 例: Hash(OrderId) の一部をプレフィックスに付与
OrderId INT64 NOT NULL,
CustomerId INT64 NOT NULL,
OrderData STRING(MAX),
) PRIMARY KEY(ShardedId, OrderId);
解説: 負荷が均等化されれば、不必要なリバランス(バックグラウンド処理)が走らなくなり、結果としてユーザーリクエストのためのリソースが常に確保される状態を維持できる。
パターンC:クエリのコスト見積もりとプレースメントの意識
Cloud Spannerのクエリプランナーは優秀だが、フルテーブルスキャンを伴う複雑な集計クエリ(OLAP的なワークロード)がOLTPと同一のノード群で実行されると、メモリとCPUのキャッシュが汚染される。
これを防ぐためには、Stale Read(古いタイムスタンプを指定した読み取り)を積極的に活用し、バックグラウンドのレプリカ(リードオンリーレプリカ)に負荷を分散させることだ。
// Go言語でのStale Readの実装例(リーダーノードへの負荷をバイパスする)
// 過去10秒時点のスナップショット読み取りを行うことで、プライマリの優先リソースを消費しない
staleBound := spanner.ExactStaleness(10 time.Second)
iter := client.Single().ReadUsingIndex(ctx, “GoodOrders”, “ByCustomer”, keys, columns, &spanner.QueryOptions{
// 冗長なリソース消費を防ぎ、バックグラウンドのレプリカ群の余剰リソースを利用する
})
defer iter.Stop()
—
4. パフォーマンス上の注意点:モニタリングすべき真の指標
最後に、Cloud Spannerの運用において「どこを見るべきか」を明確にしておこう。CPU使用率(`CPU Utilization`)をただ眺めているだけでは、プロフェッショナルとは言えない。
1. 高優先度CPU vs 低優先度CPUの比率
- Google Cloudコンソールのモニタリングで、CPU使用率の内訳(User vs Background)を確認する。バックグラウンドの割合が意図せず長期にわたって高止まりしている場合、スキーマ設計(特にインデックスやGCの発生源)に欠陥がある。
2. Transaction Latency (p99/p99.9)
- ユーザーリクエストのレイテンシがスパイクした瞬間と、ストレージのI/O遅延、あるいはバックグラウンドのオペレーションが重なっていないかを相関分析する。
3. Storage Utilization と Fragmentation
- 論理容量と物理容量の乖離を確認する。コンパクションが追いついていない場合、ストレージ効率が落ち、I/Oのオーバーヘッドが増大する。
—
結びに代えて
Cloud Spannerのリソース優先順位付けは、システムが自律的に身を守るための素晴らしい防衛機構だ。しかし、それは魔法の杖ではない。開発者がアンチパターン(ホットスポット、巨大トランザクション、過剰なGCを誘発するデータ構造)を放置すれば、いかに優れたプリプレンプション機構も限界を迎える。
「データベースに優しく、ロジカルに設計する」。
これが、大規模分散システムの限界を引き出し、真の可用性を手に入れる唯一の道だ。次のコードレビューでは、君たちの手でこの知見をチームに還元してほしい。健闘を祈る。
コメント