【実務・中級編】 タブレットサーバーアーキテクチャ – Cloud Spanner

【Cloud Spanner深層】タブレットサーバーアーキテクチャの真実:分散RDBの極限を支配する「見えざる実体」

こんにちは、チーフアーキテクトの私だ。
これまでのコードレビューや設計レビューで、こんな質問を何度も受けてきた。

  • 「Spannerって勝手にスケールするんでしょ?じゃあテーブル設計でシャードキーとか意識しなくていいよね?」
  • 「CPU使用率が急に跳ね上がったんだけど、何が起きているのかブラックボックスで分からない」

――甘い。これだから「マネージドだから何もしなくていい」という幻想を抱いたエンジニアは危うい。
Cloud Spannerは魔法の箱ではない。Googleが物理的制約の限界を超越するために構築した、極めて緻密な分散システムの結晶だ。その核心にあるのが「タブレットサーバー(Tablet Server)」と「タブレット(Tablet)」のアーキテクチャである。

今回は、このSpannerの心臓部を丸裸にし、実務の設計でどう立ち回るべきかをロジカルに伝授しよう。

—

1. コアアーキテクチャ:タブレットサーバーとは何か?

Spannerのストレージとコンピュートは、論理的には単一の巨大なリレーショナルデータベースだが、物理実体は数千、数万の「タブレットサーバー」の群れ(Fleet)だ。

タブレット = 構造化されたログの塊

Spannerのテーブルは、主キー(Primary Key)の順序に従って物理的にソートされ、「タブレット(Tablet)」と呼ばれる連続したキー範囲の断片に分割される。
勘違いしやすいが、タブレットはRDBの「テーブルそのもの」ではない。「キー範囲で垂直・水平にスライスされたデータのサブセット」だ。

[ Table A ]
├── [Tablet 1: Key (A ~ H)]
├── [Tablet 2: Key (I ~ P)]
└── [Tablet 3: Key (Q ~ Z)]

そして、このタブレットをメモリ上で動かし、読み書き要求を処理するプロセスが「タブレットサーバー」である。1台のタブレットサーバーは、数百から数千のタブレットをホストしている。

Paxosグループによる高可用性の担保

各タブレットは単一のサーバーに閉じこじておけば消滅リスクがある。そのため、Spannerではタブレットごとに複数のレプリカ(通常は5つ、あるいは3リージョンにまたがる構成)が作成され、Paxosグループを形成している。

  • リーダー(Leader): 読み書きトラフィックを処理し、トランザクションのコーディネートを行う。
  • フォロワー(Follower): リーダーからログ(Paxos Log)を受け取り、自身の状態を同期する。

つまり、あなたがSpannerに対して発行するSQLやDMLは、すべて「該当データを保持するタブレットのPaxosリーダー」へとルーティングされているのだ。

—

2. 実行時ダイナミクス:負荷分散とタブレットの「分裂・統合・移動」

Spannerの真骨頂は、トラフィックやデータ量の変化に応じて、タブレットが動的にその姿を変える点にある。ここを理解していないと、ホットスポットを踏み抜いた時に痛い目をみる。

スプリット(Split)とマージ(Merge)

  • スプリット: 特定のタブレットに書き込みが集中し、サイズまたは負荷が閾値を超えると、Spannerは自動的にタブレットを2つに分裂(Split)させる。キー範囲が半分になり、管理コストが分散される。
  • マージ: データが削除されたりアクセスが減ったタブレットは、隣接するタブレットと統合(Merge)され、リソースの断片化を防ぐ。

ムーブ(Move)

ホットスポット(特定サーバーへの負荷集中)を検知すると、Spannerのバランサーは負荷の高いタブレットを、別の負荷の低いタブレットサーバーへと動的に移動(Move)させる。これらはすべて無停止(Zero-downtime)で行われる。

—

3. 実務の設計レビュー:なぜ「ホットスポット」を踏むのか?

さて、ここからが本題だ。システム開発の現場で最も頻発する障害、それが「単一タブレットへの負荷集中(ホットスポット)」である。

❌ 悪夢のアンチパターン:連番・タイムスタンプ・UUIDv4の先頭付与

次のようなテーブル設計をしたことはないか?

— 【アンチパターン】典型的なホットスポット製造機
CREATE TABLE AccessLogs (
LogId STRING(64) NOT NULL, — UUIDv4 または 単なるオートインクリメント / 現在時刻ベースのID
UserId INT64 NOT NULL,
Action STRING(MAX),
CreatedAt TIMESTAMP NOT NULL,
) PRIMARY KEY(LogId);

何が起きるか?
1. UUIDv4やタイムスタンプは、生成されるたびに「値が常に増大(Append-only)」する。
2. 主キー順にソートされるSpannerにおいて、新しく挿入されるデータは常に「最後のタブレット」の末尾に集中する。
3. Spannerはこのタブレットをスプリットしようと試みるが、「常に最後のキーにしか書き込みが来ない」ため、スプリットしても結局新しいタブレットの末尾に書き込みが集中し続ける。
4. 結果、たった1台(あるいは数台)のタブレットサーバーのCPUが100%に張り付き、レイテンシが爆発、全体がスロットリングされる。

⭕ 堅牢な設計パターン:プレフィックス・シャーディング(分散キー)

このアーキテクチャの制約を逆手に取り、タブレットサーバー群に負荷を強制的に分散させるのがプロの技術だ。

— 【推奨パターン】ハッシュプレフィックスによる分散
CREATE TABLE AccessLogs (
ShardId INT64 NOT NULL, — 0からNのハッシュ値(例: 0〜15)
LogId STRING(64) NOT NULL,
UserId INT64 NOT NULL,
Action STRING(MAX),
CreatedAt TIMESTAMP NOT NULL,
) PRIMARY KEY(ShardId, LogId);

設計の解説:

  • 主キーの先頭に `ShardId`(例えば `UserId` のハッシュ値を16で割った余りなど)を置く。
  • これにより、新しい書き込みデータが16個の異なるキー範囲(すなわち異なるタブレット)に綺麗に分散される。
  • Spannerはすべてのタブレットサーバーのコンピュートパワーを並列に使い潰すことができる。

> Architect’s Note:
> 「でも、それだと `ShardId` が分からないと検索できないのでは?」という懸念を持つだろう。その通りだ。もし `LogId` 単体でのランダムアクセスが必要なら、インデックス(Secondary Index)を張るか、アプリケーション層でクエリを工夫する必要がある。しかし、「書き込みスループットの最大化」が要件であるバッチ処理やログ基盤において、先頭キーの分散は絶対に外せない鉄則だ。

—

4. パフォーマンス上の注意点:読み取りとコミットのメカニズム

タブレットサーバーの挙動を知ると、クエリパフォーマンスの解像度が劇的に変わる。

読み取り専用トランザクション(Read-Only Transactions)の優位性

Spannerの強みの一つが、過去のタイムスタンプを指定した非同期読み取り(Snapshot Read)だ。
これが行われる時、データはリーダーだけでなく、ローカルのフォロワー(タブレットサーバー)のメモリ/ストレージから直接読み出されることが多い。
つまり、リーダーへのロック競合が完全にゼロになり、線形スケーラビリティを発揮する。高負荷な参照系クエリは、可能な限りRead-OnlyかつStale(古いタイムスタンプの許容)を活用すべきだ。

書き込み(Read-Write Transactions)のコスト

一方、書き込みを伴うトランザクションは、対象タブレットのPaxosリーダーとの間でラウンドトリップが発生し、さらに2相コミット(2PC)のオーバーヘッドが乗る。
「1つのトランザクションで異なるタブレットにまたがるデータを大量に更新する」設計は、調整コスト(コーディネーションコスト)を激増させ、スループットを著しく低下させる。

—

5. チーフアーキテクトからの総括

Cloud Spannerのタブレットサーバーアーキテクチャの本質は、「データの物理的な局所性と分散のトレードオフを、自動スプリットとPaxosで極限まで自動化したもの」である。

しかし、自動化されているからといって、アプリケーション層のエンジニアが物理的なキー構造(タブレットの存在)を無視してよい免罪符にはならない。

  • 書き込みのホットスポットを避けるため、キーの先頭には常に分散要素(ハッシュやテナントIDなど)を持たせろ。
  • Append-onlyな単一キーへの高頻度書き込みという「タブレットキラー」な設計をコードレビューで見つけたら、容赦なく差し戻せ。

このアーキテクチャを脳内に焼き付けた者だけが、真にスケーラブルで頑健な分散RDBシステムを構築できる。次の設計レビューで、君のその知見を示してほしい。

コメント

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