Cloud Spannerのロックテーブル管理:分散トランザクションの深淵と「ホットスポット」をねじ伏せる設計作法
こんにちは。テックリードの私だ。
今日のコードレビューで、「なぜこのクエリでトランザクションがアボートするのか」「なぜこの主キー設計だとスループットがスケールしないのか」について、感覚的な議論をしているチームに出くわした。
Spannerは「魔法のデータベース」ではない。リレーショナルデータベースの強靭な保証(ACID)と、NoSQLの水平スケーリングを両立させた怪物だが、その裏側で駆動している分散ロックとコンカレンシー制御の物理法則を理解していなければ、いざ本番負荷をかけた瞬間にシステムは沈黙する。
今回は、Cloud Spannerのコアアーキテクチャの心臓部である「スプリット単位のロックテーブル管理」と、「デッドロック検出・回避メカニズム」について、実務の現場で即座に使える知見を総動員して解説する。
—
1. アーキテクチャの核心:ロックテーブルはどこに存在し、どう管理されているか?
多くのエンジニアは「Spannerはテーブルごとにロックを管理している」という誤解をしている。だが、それはRDB脳の悪しき残滓だ。
Spannerのストレージ層において、データは主キーの範囲(Lexicographical order)に基づいて「スプリット(Split)」という物理単位に分割され、世界中のPaxosグループ(複数ノードの合意形成グループ)に分散配置されている。
これに伴い、ロックテーブルもまた、テーブル単位ではなく「スプリット単位」で完全に分散・独立して存在している。
[Spanner Node]
├── Paxos Group A (Split A: User ID 0000 – 4999)
│ ├── Data Storage
│ └── ★ Split-level Lock Table (A専用のロック管理メモリ領域)
└── Paxos Group B (Split B: User ID 5000 – 9999)
├── Data Storage
└── ★ Split-level Lock Table (B専用のロック管理メモリ領域)
この構造が意味する実務上のインプリケーション
1. ロックの局所性(Locality of Locks):
あるスプリット内の行に対する排他制御は、そのスプリットを管理するリーダーレプリカ上のメモリ内ロックテーブルで完結する。グローバルな単一のロックマネージャは存在しない。
2. 真のスケールアウト:
アクセスする主キーが綺麗に分散していれば、ロック競合は発生しようがない。スプリットが自動分割(Auto-splitting)されることで、ロック管理のオーバヘッドも物理的に分散される。
3. 「ホットスポット」の致命性:
逆に、連番やタイムスタンプを主キーの先頭に据えると、すべてのトランザクションが「現在最も右側にある単一のスプリット」に殺到する。結果、そのスプリットのロックテーブルとCPUが飽和し、スループットが完全にゼロに張り付く。これがSpannerにおけるホットスポットの正体だ。
—
2. 読込ロックと排他ロック、そして「意図ロック」の解剖
Spannerのトランザクションは、デフォルトで2段階ロック(2PL)と多版同時実行制御(MVCC)を組み合わせている。
- 読込(Read): 基本的にMVCCのタイムスタンプ読込(Read-only transaction)であればロックレスで走る。しかし、Read-write transaction内の読み取り(Pessimistic read)や、`SELECT … FOR UPDATE`に相当する操作では、共有ロック(Shared Lock)または排他ロック(Exclusive Lock)がスプリットのロックテーブルに獲得される。
- 書き込み(Write): データの変更はすべて排他ロックを要求する。
ここで重要なのが、Spannerのロック管理が階層構造(Hierarchical Locking)をサポートしている点だ。親テーブル(例:`Customers`)と子テーブル(例:`Orders`、`INTERLEAVE IN PARENT`で定義)の関係において、子行をロックする場合、システムは親行やテーブル構造に対して意図ロック(Intent Lock)を伝播させ、効率的な競合検知を行う。
—
3. デッドロック検出と回避メカニズム:Spannerは世界をどう見ているか?
分散環境において、複数ノードにまたがるトランザクションが絡むと、古典的なデッドロック(AがXを持ちながらYを待ち、BがYを持ちながらXを待つ)が発生する。
悲観的ロックと待機グラフ(Wait-for Graph)
SpannerのRead-write transactionにおいて、ロックが競合した場合、トランザクションはロックが解放されるまで待機(Block)する。
各Paxosグループのリーダーは、内部で「待機グラフ(Wait-for Graph)」を維持し、循環参照(サイクル)が発生していないかを常時監視している。
もしデッドロック(サイクル)が検出された場合、Spannerはどちらか一方のトランザクションを強制的にアボート(Abort)させ、エラーコード `ABORTED` を返す。
アボートに直面したときの正しい設計作法
アプリケーションコードは、`ABORTED` エラーを想定して設計されていなければプロとして失格だ。Spannerの公式クライアントライブラリは、通常、自動リトライ(Retry)を内蔵しているが、「リトライ時の爆風(Thundering Herd)」を防ぐための心得がある。
// 良い設計例:カスタムリトライとジッター(Jitter)を考慮したトランザクション実行
public void executeWithRobustRetry(DatabaseClient dbClient, Mutation mutation) {
int maxRetries = 5;
long baseBackoffMs = 10;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
dbClient.readWriteTransaction().run(transaction -> {
// ビジネスロジックと書き込み
transaction.buffer(mutation);
return null;
});
return; // 成功したら抜け出す
} catch (SpannerException e) {
if (e.getErrorCode() == ErrorCode.ABORTED) {
// 指数バックオフ + 乱数(ジッター)の付与で競合の再発を防ぐ
long jitteredBackoff = (long) (baseBackoffMs Math.pow(2, attempt) (0.5 + Math.random() 0.5));
try {
Thread.sleep(jitteredBackoff);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw SpannerExceptionFactory.newSpannerException(ie);
}
continue;
}
throw e; // ABORTED以外のエラーは即座にスロー
}
}
throw new RuntimeException(“Max retries exceeded due to transaction aborts.”);
}
—
4. 堅牢な設計パターン:ロック競合を「デザイン」で回避する
コード側のリトライに頼るだけでは、高負荷時にスループットが頭打ちになる。アーキテクトとして、最初からロック競合を発生させないデータモデリングとトランザクション設計を施すべきだ。
アンチパターン:単一カウンター行の更新
ユーザーの残高や、在庫数を1つの行で管理し、それを高頻度でインクリメント・デクリメントする設計。
これは典型的なロック競合の温床となり、スプリットのロックテーブルが完全にロックされる。
推奨パターン:シャーディング・カウンター(Sharded Counters)
カウンターを複数の物理的な行に分散させ、トランザクション内でランダムなシャードを選択して更新する。読み出し時はそれらの総和を計算する。
— テーブル定義:シャードIDを持たせることで、異なるスプリットに分散させる
CREATE TABLE InventoryShards (
ProductId STRING(64) NOT NULL,
ShardId INT64 NOT NULL,
Quantity INT64 NOT NULL,
) PRIMARY KEY(ProductId, ShardId);
このように、主キーの設計段階で「ロックが一点に集中しない構造」を強制することこそが、Spannerを極限まで使いこなすエンジニアの技量である。
—
チーフアーキテクトからの総括
Cloud Spannerのロックテーブル管理は、分散システムの限界に挑んだエンジニアリングの結晶だ。
- ロックはスプリット単位でメモリ上に分散している。
- 主キー設計のミスは、そのまま特定のロックテーブルの窒息に直結する。
- デッドロックは避けられないため、アボートを前提とした堅牢なリトライ戦略と、競合を生まないモデリングの両輪でシステムを構築せよ。
ドキュメントをなぞるだけの開発は今日で終わりにしよう。物理層の挙動まで見通した上で、美しくスケールするシステムを設計し抜け。健闘を祈る。
コメント