【実務・中級編】 リソース分離境界の定義 – Cloud Spanner

【Cloud Spanner解体新書】マルチテナントの深淵:物理・論理のリソース分離境界とサイドチャネル防衛戦略

「Google Cloudのマネージドサービスだから、インフラの分離やマルチテナントの競合はGoogle任せでいい」——もし君がプロダクトの技術選定やアーキテクチャレビューでそう考えているなら、今すぐその思考停止を捨ててほしい。

Cloud Spannerは、地球規模の強整合性と99.999%の可用性を誇る驚異的な分散データベースだ。しかし、その裏側では膨大なマルチテナント・インフラストラクチャが高度な制御のもとで蠢いている。

マルチテナント環境において、「リソース分離境界(Isolation Boundary)」を物理・論理の双方でどのように定義・制御し、過小評価されがちな「サイドチャネル攻撃」や「ノイズネイバー(騒音お隣さん)問題」にどう立ち向かうか。本稿では、一般のドキュメントには書かれないSpannerのコアアーキテクチャの内部挙動を踏まえ、実践的な設計パターンとコードレベルの防衛策を叩き込む。

—

1. Cloud Spannerの物理・論理分離アーキテクチャ

まず、我々が普段操作している「Spanner Node」や「Instance」という抽象化された概念を、物理インフラ層まで解像度を上げて分解してみよう。

ノードの正体:Borg Task上の計算クォータ

よくある誤解だが、「1 Spanner Node = 1台の物理サーバー(またはVM)」ではない。

Spannerのアーキテクチャは、Compute(計算層)とStorage(ストレージ層)が完全に分離されている。

+———————————————————————–+
| Cloud Spanner Instance |
| |
| +—————————————————————+ |
| | Compute Layer (Borg Tasks) | |
| | +———————+ +———————+ | |
| | | Spanner Node Task A | | Spanner Node Task B | | |
| | +———————+ +———————+ | |
| +—————————————————————+ |
+———————————————————————–+
| (gRPC / Network)
+———————————————————————–+
| Storage Layer (Colossus / Distributed) |
| +———————+ +———————+ +————–+ |
| | Split 1 (Tablet) | | Split 2 (Tablet) | | Split 3… | |
| +———————+ +———————+ +————–+ |
+———————————————————————–+

  • Compute Layer: Googleのコンテナオーケストレーションシステム「Borg」上で動作する `Spanner Server` プロセス(Borg Task)群。1 Nodeとは、この計算資源(CPUやMemory)の事前割り当て(Quota)を意味する。
  • Storage Layer: 分散ファイルシステム「Colossus」上に保存されたデータファイル(SSTable形式)。データは主キーの範囲ごとに「Split(旧称: Tablet)」として自動分割される。

ここで重要なのは、同一の物理サーバー上で、君のテナントのCompute TaskやStorage Splitと、全く無関係な他社のTask/Splitが同居(Co-location)し得るという事実だ。Spannerは本質的にマルチテナント・インフラストラクチャの上で成立している。

SplitとPaxos Groupの分散配置

Spannerのデータレプリケーションは、SplitごとのPaxos Groupによって行われる。

リージョン間、あるいはゾーン間で分散されたPaxos Leader/Followerのコンテナプロセスは、常にワークロードに応じて動的に再配置(Rebalance)される。この物理的動性を前提としながら、いかに安全な分離境界を構築しているかがアーキテクチャの焦点となる。

—

2. マルチテナント分離の3つの設計パターン

SaaSなどのマルチテナントシステムを構築する際、Spanner上で取れる分離パターンは大きく3つある。それぞれの分離境界とトレードオフを頭に叩き込んでほしい。

| 分離パターン | 物理的隔離性 | 論理的隔離性 | コスト効率 | 最大テナント数限界 |
| :— | :— | :— | :— | :— |
| 1. Instance分離 | 高(Borg Task分離) | 完全 | 低(固定費高) | 低(Quota制限) |
| 2. Database分離 | 中(Colossus層共有) | 高(IAM境界) | 中 | 中(100 DB/Instance) |
| 3. Schema/Row分離 | 低(物理同居) | 中(Row-Level) | 極めて高 | 無制限 |

実務において、90%以上のエンタープライズSaaSで解となるのは「3. Schema/Row分離(Interleaved Tablesを用いたアンカー設計)」だ。しかし、セキュリティ要件が極めて厳しい金融・医療顧客のみ「1. Instance分離」へ昇格させるハイブリッド設計が常套手段となる。

Row分離パターンにおける厳格なDML設計例

Row分離を採用する場合、テナント間のデータリークを防止するために、スキーマレベルでの `Interleaved Table` 結合と、すべてのクエリに対する `Tenant ID` の強制が絶対条件となる。

— テナントの親テーブル (親データ構造)
CREATE TABLE Tenants (
TenantId STRING(36) NOT NULL,
Name STRING(256) NOT NULL,
CreatedAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (TenantId);

— テナント配下のデータテーブル (Interleave配置)
— 親テーブルと物理的にデータを近く(同一Split内)にコロケーションさせる
CREATE TABLE Orders (
TenantId STRING(36) NOT NULL,
OrderId STRING(36) NOT NULL,
Amount NUMERIC NOT NULL,
CreatedAt TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true)
) PRIMARY KEY (TenantId, OrderId),
INTERLEAVE IN PARENT Tenants ON DELETE CASCADE;

【アンチパターンと正解コードの比較】

クエリを発行する際、アプリケーション層でのパラメータ渡し漏れによるデータリークは絶対に避けねばならない。

❌ [BAD CODE] テナントIDの絞り込みが不十分、または動的SQL構築による危険性
def get_orders_bad(db_client, tenant_id, order_id):
# DDLや直書きクエリでTenantIdの検証を怠ると、他テナントのデータにアクセスする致命的バグを生む
query = f”SELECT FROM Orders WHERE OrderId = ‘{order_id}'”
# これでは全Splitをスキャンし、最悪の場合他人のデータが引っかかる
return db_client.single_use().execute_sql(query)

✅ [GOOD CODE] 強固なパラメータバインドとTenantIdプレフィックスの徹底
from google.cloud import spanner

def get_orders_secure(db_client, tenant_id: str, order_id: str):
“””
TenantIdを複合主キーの第一打鍵として必ず検索条件に含める。
これによりSpannerは不要なSplitスキャンを回避(Point Read化)し、
他テナントのデータへの物理的・論理的アクセスをシャットアウトする。
“””
query = “””
SELECT TenantId, OrderId, Amount, CreatedAt
FROM Orders
WHERE TenantId = @tenant_id AND OrderId = @order_id
“””
params = {
“tenant_id”: tenant_id,
“order_id”: order_id
}
param_types = {
“tenant_id”: spanner.param_types.STRING,
“order_id”: spanner.param_types.STRING
}

with db_client.single_use() as snapshot:
results = snapshot.execute_sql(
query,
params=params,
param_types=param_types
)
return list(results)

—

3. サイドチャネル攻撃とマイクロアーキテクチャ防御

物理リソースをマルチテナントで共有している以上、理論上懸念されるのがマイクロアーキテクチャ攻撃(Spectre, Meltdown, L1TF, MDS等)や時系列解析によるサイドチャネル攻撃だ。

Spannerはこれらに対し、ハードウェア・ハイパーバイザ・データベースエンジン・時空(TrueTime)の4層で防衛線を敷いている。

+———————————————————————–+
| [Layer 4] TrueTime API Jitter & Resolution Limit (時間計測の無効化) |
+———————————————————————–+
| [Layer 3] Tenant Encryption Isolation (CMEK by Tenant) |
+———————————————————————–+
| [Layer 2] Process / Address Space Isolation (KPTI, Memory Cleansing) |
+———————————————————————–+
| [Layer 1] Hardware / CPU Level Mitigation (Core Scheduling, IBRS) |
+———————————————————————–+

1. 物理CPUキャッシュと投機的実行の隔離(Layer 1 & 2)

Googleのインフラ層では、同一の物理CPUコア上で異なるマルチテナントのハイパースレッディング(SMT)を同時に走らせないCore Schedulingが徹底されている。また、Borg Taskが物理マシン間を移行、またはタスク切り替えを行う際、L1/L2キャッシュのフランクなフラッシュとメモリ領域のクリア処理(Sanitization)がカーネルレベルで自動実行される。

2. TrueTimeを用いたクロック・サイドチャネルの無効化(Layer 4)

サイドチャネル攻撃(特にFlush+Reload等のキャッシュタイトな攻撃)の多くは、高精度な高解像度タイマーに依存してメモリレイテンシの微小な差を測定する。

Spannerの核であるTrueTime API(GPSと原子時計による分散時計)は、外部の任意ユーザーコードから直接高精度にミリ秒未満の差分を計測できないよう遮断されている。コミットタイムスタンプの割り当てロジックはカーネルおよびSpannerプロセスの内部でのみ閉じられており、攻撃者が「クエリの処理時間から他テナントのデータ存在の有無をミリ秒以下で割り出す」といったタイマースキャン攻撃は物理的に成立しない。

3. Customer-Managed Encryption Keys (CMEK) による暗号境界

物理ストレージ(Colossus)上での完全隔離を保証したい場合、データベースごとにKMS(Key Management Service)の鍵を分離するCMEKを採用する。

— KMSでテナント固有の鍵(Key Ring)を指定してDBを生成する例
CREATE DATABASE tenant_db_001
ALTER DATABASE tenant_db_001
SET OPTIONS (
kms_key_name=’projects/my-project/locations/asia-northeast1/keyRings/my-ring/cryptoKeys/tenant-001-key’
);

これにより、仮にColossusのストレージデバイスが物理的に持ち出されたり、他テナントのBorg Taskがバグによって生のディスクブロックを読み出すような破天荒な脆弱性(ゼロデイ)が発生したとしても、暗号解読キーがKMSレベルで完全に分離されているため、データの物理的解読は不可能となる。

—

4. ノイズネイバーを抑殺する「パフォーマンス分離」の実践

マルチテナント環境で最も頻繁に発生する現実的な問題は、攻撃ではなく「特定テナントの暴走クエリが、他テナントのレスポンス速度を破壊する」というノイズネイバー(騒音お隣さん)現象だ。

SpannerはCPUリソースをノード数に応じて配分するが、同一インスタンス内の全DB・全テーブルは同一のCompute Node資源を奪い合う。これ防ぐためのテクニックを伝授する。

① Priority(優先度)の明示的コントロール

Spannerのセッションやリクエストには `PRIORITY` を付与できる。バッチ処理や分析系のクエリが、OLTPトランザクションのCPUを奪う事態を防止せよ。

from google.cloud.spanner_v1 import RequestOptions

def run_background_batch(db_client):
“””
夜間バッチや集計処理など、遅延が許容される処理には
PRIORITY_LOW を明示的に指定して実行する。
これにより、OLTP(PRIORITY_HIGH)のCPUリソース割り当てが優先される。
“””
query = “””
SELECT TenantId, SUM(Amount) as TotalAmount
FROM Orders
GROUP BY TenantId
“””

# リクエストオプションで優先度をLOWに設定
request_options = RequestOptions(
priority=RequestOptions.Priority.PRIORITY_LOW
)

with db_client.single_use() as snapshot:
results = snapshot.execute_sql(
query,
request_options=request_options
)
for row in results:
process_aggregate_data(row)

② クエリタイムアウトとキャンセル機構の絶対適用

1つのテナントがインデックスの効いていないフルスキャンを発行し、CPUを100%に張り付かせる事故を防ぐため、アプリ側で厳格な `timeout` を設定する。

def execute_user_search(db_client, tenant_id: str, keyword: str):
query = “””
SELECT OrderId, Amount FROM Orders
WHERE TenantId = @tenant_id AND Amount > @min_amount
“””
params = {“tenant_id”: tenant_id, “min_amount”: 1000}

# 3秒で強制カットオフ。Spanner側にもCANCELリクエストが飛び、
# Borg Task上の計算リソースが解放される。
try:
results = db_client.single_use().execute_sql(
query,
params=params,
param_types={“tenant_id”: spanner.param_types.STRING, “min_amount”: spanner.param_types.INT64},
timeout=3.0 # 超重要: タイムアウトの明記
)
return list(results)
except Exception as e:
# ログ記録とエラーハンドリング
raise ScalabilityException(“Query timed out, potential noisy neighbor or unindexed scan.”) from e

—

5. テクニカルリードとしての設計チェックリスト

今後、Spannerを用いたマルチテナントシステムの設計レビューを行う際は、以下のポイントをチームに問いかけてほしい。

1. 複合主キーの第一打鍵に `TenantId` が配置されているか?

  • 単なるUUIDではなく、ハッシュ分散やデータコロケーション(Interleave)を意識したスキーマになっているか。

2. 直書きクエリやバインド漏れによって、Splitのフルスキャン(Cross-Split Scan)が発生していないか?

  • Execution Plan(実行計画)で `Scan` 範囲が特定テナントのPrefixに絞られているか確認したか。

3. バッチ処理とWebリクエストで `PRIORITY` を使い分けているか?

  • 全てのクエリを `PRIORITY_HIGH` で投げるような雑な実装になっていないか。

4. コンプライアンス要件が高いテナントに対し、CMEKによる物理暗号境界の分離を行っているか?

—

おわりに

Cloud Spannerは、正しくアーキテクチャを理解して使いこなせば、運用コストを限りなくゼロに保ちながら、地球規模でスケールする強固なマルチテナント基盤を提供してくれる。

しかし、「マネージドだから」と内部の分散原理やリソース分離境界に対する思考を放棄した瞬間、ノイズネイバーによるパフォーマンス障害や、意図せぬデータ共有のバグという手痛い洗礼を受けることになる。

物理(Borg/Colossus/TrueTime)と論理(IAM/Schema/Interleave)の境界線を意識し、圧倒的に堅牢でエレガントなシステムを組み上げてほしい。レビューで君たちのコードを見るのを楽しみにしている。

コメント

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