Cloud Spannerの正体:なぜ我々は「水平スケールする強整合性RDB」に震えるのか
テックリードの私だ。設計レビューのたびに「RDBにするか、NoSQLにするか」という不毛な宗教論争に付き合わされるのは、もう終わりにしよう。
「スケールさせたいならNoSQLにして結果整合性を飲め。トランゾクションが必要ならRDBにしてスケールの限界を受け入れろ」——この二者択一は、クラウドネイティブ全盛期のエンジニアにとって最大の足枷だった。
だが、Cloud Spannerはこの常識を物理的に破壊した。Googleが社内インフラとして長年育て上げ、外部に解放したこのモンスターは、「無限の水平スケーラビリティ」と「外部から観測可能な最強の整合性(Serializability)」を、グローバル規模で両立させている。
今回は、Spannerのコアアーキテクチャの深淵を覗き、実務の現場でどう設計し、どう爆速かつ堅牢に動かすべきか、その極意を伝授しよう。
—
1. コアアーキテクチャ:なぜSpannerは「魔法」を具現化できるのか?
表面的な「分散RDBです」という謳い文句に騙されてはいけない。Spannerの本質は、ハードウェアとソフトウェアの限界を物理レベルでハックしたその構造にある。
トゥルータイム(TrueTime)という名のタイムマシン
分散システムにおける最大の敵は「時刻のズレ」だ。各ノードの時計が数ミリ秒ズレるだけで、トランザクションの順序が狂い、データが破壊される。
通常の分散DBはこれを「合意アルゴリズム(Raftなど)」の通信コストで解決しようとして、レイテンシを悪化させる。しかし、SpannerはTrueTime APIを使い、GPSと原子時計を組み合わせることで物理的な「時刻の不確実性($\epsilon$:イプシロン)」をミリ秒以下(通常数ms以内)に抑え込んだ。
[Node A] –(TrueTime: t = [10.000, 10.007])–>
\ 交差する領域を待つ
[Node B] –(TrueTime: t = [10.003, 10.010])–> / (Commit Wait)
Spannerは、コミット時にこの不確実性の幅($\epsilon$)のぶんだけ静かに待つ(Commit Wait)。これにより、「絶対に時系列の矛盾が起きないグローバルな順序制御」を通信なしで実現している。これが、グローバル分散環境でありながら、ANSI SQLの最高峰である `SERIALIZABLE` 分離レベルを担保できる理由だ。
スプリットとダイナミック・リバランス
Spannerのストレージは、テーブルの行(Row)をプライマリキーの順にソートし、「スプリット(Split)」と呼ばれる単位で分割して、世界中の複数のSpannerノード(Paxosグループ)に配置する。
負荷が上がったり、データ量が増えたりすると、Spannerは自動的にスプリットを分裂させ、別のノードへホットスポットごと移動させる。この一連の再配置は、アプリケーションの書き込みを止めることなく、完全にバックグラウンドでライブマイグレーションされる。
エンジニアがシャーディングのキー設計やリシャードのバッチに悩む夜は、これで終わったのだ。
—
2. 実務設計の急所:インターリーブ(Interleave)とプライマリキーの呪法
「Spannerならどうせ勝手にスケールするんだろ?」と思ってスキーマ設計をサボると、痛い目を見る。Spannerのパフォーマンスを極限まで引き出すには、物理ストレージのレイアウトをハックしなければならない。
親子関係の物理的同居:Interleaved Tables
もしあなたが「ユーザー(Users)」と「注文(Orders)」のような親子関係を持つエンティティを設計するなら、通常のRDB感覚で外部キー(FK)を貼るだけでは三流だ。
Spannerにはインターリーブ(Interleaving)という強力な機能がある。これは、子テーブルのデータを親テーブルの行と同一の物理ストレージブロックにインターリーブ(織り込み)して格納する技術だ。
— 親テーブル:ユーザー
CREATE TABLE Users (
UserId INT64 NOT NULL,
UserName STRING(100),
) PRIMARY KEY(UserId);
— 子テーブル:注文(親の物理領域に同居させる)
CREATE TABLE Orders (
UserId INT64 NOT NULL,
OrderId INT64 NOT NULL,
OrderDate DATE,
Amount INT64,
) PRIMARY KEY(UserId, OrderId),
INTERLEAVE IN PARENT Users ON DELETE CASCADE;
【設計の効果】
`UserId = 100` のユーザーとその注文履歴を一撃で取得する際、Spannerは単一の物理ノード・単一のストレージ領域からデータをスキャンできる。分散トランザクションのオーバーヘッドが完全に消失し、NoSQL並みの超高速な局所アクセスが実現する。
ホットスポットを回避せよ:プライマリキーのアンチパターン
Spannerのプライマリキー設計において、「単調増加する値(シーケンス、Auto Increment、現在時刻のミリ秒)」を先頭キーに置くのは万死に値する。
- NGパターン: `PRIMARY KEY (CreatedAt, OrderId)`
- 理由:すべての書き込みが「現在の時刻」を持つ単一のスプリット(最新のデータブロック)に集中し、そのノードが即座にCPU/IOネック(ホットスポット)になってスケーラビリティが崩壊する。
- BESTパターン: ハッシュ化やプレフィックス分散
- 理由:キーの先頭にランダムなUUIDや、テナントID、あるいはユーザーIDのハッシュの上位数ビットを付与し、書き込みを全スプリットに分散させる。
—
3. 実装の極意:トランザクションとパフォーマンス・アンチパターン
コードレビューで私が最も厳しくチェックするのが、Spannerのトランザクション構築方法だ。
読み取り専用トランザクション(Read-Only Transactions)の活用
データを読み込むだけの処理に、わざわざロックを伴うリード・ライト・トランザクションを使っていないか? それは大罪だ。
Spannerの読み取り専用トランザクションは、ロックを一切取得しない。TrueTimeの機能を利用して「過去の特定のタイムスタンプ時点のデータ」を無ロックで安全に読み出す。これを使うことで、競合によるレイテンシの悪化を完全に排除できる。
Python (google-cloud-spanner) によるリード・オンリーの例
def get_user_profile(transaction, user_id):
# ロックなしで高速にスナップショット読み取りを行う
sql = “SELECT UserName FROM Users WHERE UserId = @user_id”
result = transaction.execute_sql(
sql,
params={“user_id”: user_id},
param_types={“user_id”: spanner.param_types.INT64}
)
return list(result)
スナップショットとしてのトランザクション実行
with database.snapshot() as snapshot:
profile = get_user_profile(snapshot, 12345)
print(profile)
読み取り-修正-書き込み(Read-Modify-Write)の罠
逆に、データを更新する場合は注意が必要だ。アプリケーション側でデータを読んで、ゴニョゴニョ加工して、書き戻すというパターン(RDBでよくあるORMのデフォルト挙動)は、Spannerでは競合(Contention)を激発させる。
もしカウンターのインクリメントのような処理であれば、アプリ側で計算するのではなく、Cloud SpannerのDML(Data Manipulation Language)や、計算式を直接データベースに投げろ。
— 悪手:アプリでSELECTして計算してUPDATEする
— 正解:SQL側でアトミックにインクリメントする
UPDATE UserCounters
SET LoginCount = LoginCount + 1
WHERE UserId = @user_id;
これによって、トランザクションのライフサイクルが極限まで短くなり、スループットが劇的に向上する。
—
4. 運用・コストの現実:Spannerを使いこなすための哲学
最後に、インフラのコストと運用の現実について話そう。
Spannerは安くはない。最低構成(ノード=1プロビジョニング、または近年普及したインスタンス設定)でもそれなりのコストがかかる。だが、考えてみてほしい。
- シャーディングのメンテナンストラブルで夜中に叩き起こされるコスト
- データ不整合(二重計上など)のデバッグと修正に費やすエンジニアの人件費
- サービスがスケール限界に達したときのシステムリプレース費用
これらを総合的に換算すれば、Cloud Spannerがいかに「エンジニアの自由とビジネスの安心を買うための安価な投資」であるかが分かるはずだ。
—
最後に:チーフアーキテクトからのメッセージ
Cloud Spannerは、データベースの物理的制約から我々を解放してくれた。しかし、「何もしなくても勝手に動く魔法の箱」ではない。
その背後にあるTrueTimeのメカニズムを理解し、インターリーブを駆使した美しいスキーマを書き、適切なトランザクション境界を引く。その設計の美しさにこだわることこそが、プロフェッショナルなエンジニアの誇りである。
次のコードレビューでは、君たちの手で「スケーラビリティと強整合性を完璧に両立させたコード」が提出されることを期待している。レビューで容赦なく突っ込む準備はできている。さあ、コードを書こう。
コメント