【実務・中級編】 ロック待機タイムアウト – Cloud Spanner

Cloud Spannerの「ロック待機タイムアウト」を制する者は、分散トランザクションを制する

Cloud Spannerを使いこなせていると自負しているエンジニアほど、一度は「なぜこのトランザクションがハングしたように見えるのか?」という壁にぶつかる。その正体は、間違いなくロック待機だ。

Spannerは強整合性(Strong Consistency)を実現するために、内部で厳密なロック管理を行っている。しかし、実務においてデフォルトの挙動を鵜呑みにするのは危険だ。今日は、Spannerのロック待機タイムアウトをどう設計し、どう制御すべきか、その「極限の知見」を伝授する。

—

1. ロックの正体:なぜ「待機」が発生するのか

Spannerのトランザクションは、読み取りと書き込みの整合性を担保するために、各行(またはレンジ)に対してロックを確保する。
競合が発生すると、後続のトランザクションはロックが解放されるまで待機する。

ここで重要なのは、「Spannerはデフォルトで無限に近い待機を許容する」という設計思想だ。アプリケーション側でタイムアウトを制御しなければ、コネクションプールを枯渇させ、システム全体を雪崩式にダウンさせる「連鎖的な遅延」を引き起こす。

2. タイムアウト設定の実装:`request_tag` と `priority` の併用

Spannerには「グローバルなタイムアウト設定」という魔法のスイッチは存在しない。代わりに、クライアントライブラリ側で制御を行うのが定石だ。

具体的な実装パターン(Goの例)

Goのクライアントライブラリを使用する場合、`context.WithTimeout` を使うのが基本だが、ただタイムアウト時間を設けるだけでは不十分だ。「どのトランザクションが詰まっているか」を可視化するために `request_tag` を付与し、優先度を管理する設計を強く推奨する。

// コンテキストにタイムアウトを設定し、リクエストをタグ付けする
ctx, cancel := context.WithTimeout(context.Background(), 5time.Second)
defer cancel()

// トランザクションの重要度に応じて優先度とタグを設定
txOptions := spanner.TransactionOptions{
CommitOptions: spanner.CommitOptions{
RequestTag: “tx_update_user_balance_v1”, // 調査時に必須
},
Priority: spanner.PriorityHigh, // クリティカルな処理ならHighに引き上げる
}

_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn spanner.ReadWriteTransaction) error {
// 処理ロジック
return nil
}, txOptions)

if status.Code(err) == codes.DeadlineExceeded {
// タイムアウト発生時のハンドリング
// ここでリトライすべきか、エラーを返すか、即座に判断する
return fmt.Errorf(“lock wait timeout: %w”, err)
}

3. 「ロック待機」を回避するための堅牢な設計パターン

タイムアウトを短くするだけでは「対症療法」に過ぎない。ロック待機を根本から減らすためのアーキテクチャ設計が必要だ。

① 「ホットスポット」を物理的に排除する

特定の行(例えば、カウンター値や単一の口座レコード)に更新が集中すると、ロックは必ず競合する。

  • 解決策: レコードを分散させる(Sharding)。カウンターなら10個のレコードに分散させ、合計を計算する設計に切り替える。

② 読み取りと書き込みの分離

読み取りのためにロックを保持し続けるのはNGだ。

  • 解決策: 読み取りのみが必要な場合は `ReadOnlyTransaction` を使用し、ロックを取得しない `Snapshot` 読み取りを活用する。これだけで競合は劇的に減る。

③ トランザクションの「短縮」は正義

トランザクション内部で外部API呼び出しや重い計算を行ってはならない。

  • アンチパターン: `外部API呼び出し -> DB更新`
  • 正解: `DB読み込み -> 外部API呼び出し -> 計算 -> DB更新`(更新範囲を極限まで絞る)

4. パフォーマンス上の注意点:監視の勘所

Cloud Spannerコンソールの「インサイト」や「モニタリング」を眺めているだけでは、真のボトルネックは見えてこない。以下のメトリクスを常に監視対象に含めること。

1. `spanner.googleapis.com/api/request_latencies`: `50th`, `95th`, `99th` パーセンタイルを注視せよ。
2. `Lock Wait Time`: 特定のテーブルに偏っていないか?
3. `Transaction Aborts`: タイムアウトと並行して「アボート率」も急増していないか?(アボートが多い場合、ロックの粒度が粗すぎる可能性がある)

アーキテクトからの提言

ロック待機タイムアウトを「エラーが発生するまで放置する」のは、エンジニアとして最もやってはいけないことだ。
私がレビューを行う際、必ず確認するのは「このトランザクションは、最悪のシナリオで何秒ロックを保持し続けるか?」という点だ。

分散データベースにおいて、ロックは「悪」ではない。しかし、ロックを「無自覚に握り続けること」はシステムに対する背信行為だ。トランザクションのスコープを最小化し、万が一のタイムアウトを予期した設計こそが、Spannerを最強のバックエンドにする唯一の道である。

さあ、コードを開いて、あなたのトランザクション設計を見直してほしい。そこに改善の余地はないか?

コメント

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