【実務・中級編】 トランザクション再試行ロジック – Cloud Spanner

Cloud Spannerの「アボート」を飼いならせ:トランザクション再試行の極意

Cloud Spannerを使い始めたエンジニアが必ずぶつかる壁、それが「`Aborted`エラー」だ。

「なぜリクエストが成功しないのか?」「ネットワークが不安定なのか?」と悩むのはもう終わりにしてほしい。Spannerにおけるアボートは、バグや障害ではない。Spannerが究極の整合性を守るために行っている、極めて健全かつ計算された「戦略的撤退」なのだ。

今日は、この「避けられないアボート」とどう向き合い、堅牢なシステムを構築するか。その設計哲学と実装の勘所を伝授する。

—

1. なぜSpannerは「アボート」するのか

Spannerは外部整合性(External Consistency)を保証する。これを実現するために、内部ではTrueTimeという高精度な時刻同期技術と、楽観的並行制御(OCC)の亜種が動いている。

読み取りと書き込みの競合が発生した際、Spannerは待機してデッドロックを作るのではなく、「そのトランザクションはやり直したほうが速いし安全だ」と判断してアボートを投げる。

つまり、アボートはSpannerのアーキテクチャが生んだ「副作用」ではなく、「機能」だ。この前提を理解していないと、アプリケーションはいつまで経っても不安定なままだ。

2. 再試行ロジックの「禁じ手」と「王道」

多くのエンジニアが陥る罠が、「単純なループで即座に再試行する」ことだ。これは、競合している相手をさらに追い込み、最悪の場合はシステム全体で「再試行の嵐(Retry Storm)」を巻き起こす。

堅牢な再試行の鉄則:指数バックオフとジッター

単なる遅延ではなく、以下の要素を組み合わせた再試行エンジンが必要だ。

1. 指数バックオフ: 失敗するたびに待機時間を倍増させる(例: 10ms → 20ms → 40ms…)。
2. ジッター(揺らぎ): 待機時間にランダムな値を加える。これにより、複数のトランザクションが同時に再試行を開始して衝突する「雷鳴効果(Thundering Herd Problem)」を防ぐ。
3. 上限回数とタイムアウト: 無限ループは死を意味する。ビジネスロジックに応じて、妥当な上限を設ける。

3. 実装のベストプラクティス(Go言語での例)

Googleが提供する公式クライアントライブラリには、すでに高度な再試行ロジックが組み込まれている。自前で車輪の再発明をする前に、以下のコードのように「ライブラリの恩恵をフルに受ける書き方」を徹底してほしい。

// 誤った設計: 自前で複雑な再試行ループを実装しようとしない
// 正しい設計: ライブラリのトランザクション関数を信頼する

func updateBalance(ctx context.Context, client spanner.Client, accountID string, amount int64) error {
// ReadWriteTransactionは、内部でアボート時の再試行を自動的にハンドルする
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn spanner.ReadWriteTransaction) error {
// 1. 読み取り
row, err := txn.ReadRow(ctx, “Accounts”, spanner.Key{accountID}, []string{“Balance”})
if err != nil {
return err
}
var balance int64
row.Column(0, &balance)

// 2. 更新
newBalance := balance + amount
stmt := spanner.Statement{
SQL: `UPDATE Accounts SET Balance = @balance WHERE AccountID = @id`,
Params: map[string]interface{}{“balance”: newBalance, “id”: accountID},
}

// 更新処理の実行
return txn.Update(ctx, stmt)
})

// ライブラリ側で解決できないエラー(コンテキストキャンセル等)や、
// 最大再試行回数を超えたエラーがここで戻ってくる
return err
}

なぜこの実装が最強なのか?

  • コンテキスト管理: `ReadWriteTransaction` は関数内で発生した `Aborted` ステータスを検知し、内部で再試行をトリガーする。
  • 分離の原則: トランザクション内での副作用(外部APIコールなど)は絶対禁止だ。再試行されるたびに外部APIが叩かれることになる。「トランザクション関数は冪等であるべき」という原則をコードレビューで必ずチェックすること。

4. パフォーマンスを劇的に改善するチューニング

アボートの頻度が高すぎる場合、コードの修正だけでなく、設計の見直しが必要になる。

  • ホットスポットの回避: 頻繁に更新される行(例:カウンターや集計テーブル)が一箇所に集中していないか? シャーディングを検討せよ。
  • 読み取りの最適化: 書き込みが必要ない場合は、`Single-Use Read-Only Transaction` や `Staleness` を活用して、トランザクションの競合を徹底的に排除する。
  • 読み取りと書き込みの分離: 複雑な読み取り処理をトランザクションの中に含めていないか? 「読んでから書く」までの時間を最小化するのがSpanner設計の鉄則だ。

最後に:エンジニアへのメッセージ

Spannerのトランザクション再試行ロジックに向き合うことは、分散システムの深淵を覗くことと同義だ。

アボートを「消し去るべき悪」と考えるのではなく、「システムが整合性を守るための高潔な決断」と捉えてほしい。その上で、あなたのコードがその再試行をいかにエレガントに、いかに静かに処理できるか。そこに、シニアエンジニアとしての腕の見せ所がある。

さあ、次はあなたの設計したシステムで、その堅牢なトランザクションを実装してくれ。レビューを楽しみにしている。

コメント

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