トランザクションタグ付けと分散トレーシング:Spannerの「ブラックボックス」を剥ぎ取る極限のオブザーバビリティ
テックリードの私だ。コードレビューや設計レビューの場において、こんな不満を聞いたことはないか?
> 「なんか特定のバッチ処理の時にSpannerのCPU使用率が跳ね上がってるんだけど、どのクエリが悪さしてるのかわからない」
> 「レイテンシのP99が悪化している。どのトランザクションのどのステップがボトルネックなんだ?」
君たちが向き合っているCloud Spannerは、世界規模のトランザクションをリニアスケーラビリティで捌く怪物だ。しかし、デフォルトのままでは、数千・数万と並行して流れるクエリやトランザクションの群れは、単なる「重たいSQLの塊」としてしか見えない。
このブラックボックスを完全に見通すための鍵が 「トランザクションタグ(Transaction Tagging)」 と 「Cloud Trace連携による分散トレーシング」 だ。
今回は、単なるドキュメントの解説ではない。実務の現場で数百ノードのSpannerクラスタを限界までチューニングしてきた私が、「どう設計し、どうトラブルシュートすべきか」 その実践知を叩き込む。
—
1. なぜ「タグ」がないSpanner運用は航海計器のない深夜飛行なのか
まず前提を合わせよう。Spannerにおいて、通常のクエリやトランザクションは、実行された瞬間、サーバー側から見れば「ただ流れてきたSQL文」に過ぎない。
例えば、次のような障害が発生したとする。
- 夜間バッチの時間帯に、特定リージョンのレイテンシがスパイクした。
- Cloud Monitoringを見ると、CPU使用率が85%を超えている。
- アクティブセッションやクエリ統計を見ても、「どのアプリケーションの、どの機能が、どのビジネス文脈で発行したSQLか」がコードレベルで紐づかない。
ここでトランザクションタグとリクエストタグの出番だ。
- リクエストタグ(Request Tag): 単一の読み取り、または単一のSQL実行単位に付与するラベル。
- トランザクションタグ(Transaction Tag): 読み取り・書き込みトランザクション全体のライフサイクル(Read-Write Transaction)に付与するラベル。
これらを付与して発行すると、Spanner内部の監査ログ(`CLOUD_AUDIT_LOGS`)やクエリ統計(`spanner.googleapis.com/query_stats`)、そしてCloud Traceにタグ名が完全に伝播する。つまり、「この数千回のロック競合を起こしている原因は、`batch-job:nightly-sync` というタグのトランザクションだ」と、一発で特定できるようになるのだ。
—
2. 実装パターン:コードレビューで合格点を出せるタグ付け設計
では、実際のアプリケーションコードでどう実装すべきか。Goの公式クライアントライブラリを例に、私たちがレビューで「合格」を出す設計パターンを示そう。
悪い例(アンチパターン)
タグを一切つけず、クエリ文字列だけで判別しようとするスタイル。
// 何の文脈で実行されたトランザクションか、サーバー側からは分からない
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn spanner.ReadWriteTransaction) error {
// 複雑なビジネスロジックとSQL群…
return nil
})
良い例(プロダクションクオリティ)
コンテキストにタグを仕込み、トレーシングのスパンスパンと完全に同期させる実装だ。
package main
import (
“context”
“fmt”
“log”
“cloud.google.com/go/spanner”
“go.opencensus.io/trace”
)
// ExecuteUserOrderProcessing ユーザーの注文処理を模したトランザクション
func ExecuteUserOrderProcessing(ctx context.Context, client spanner.Client, userID string) error {
// 1. 分散トレーシング用のスパンを切る(Cloud Trace連携の基盤)
ctx, span := trace.StartSpan(ctx, “ExecuteUserOrderProcessing”)
defer span.End()
// 2. トランザクションタグの設定
// 命名規則: [コンポーネント]:[操作名] の形式をプロジェクト全体で強制すること
txnOpts := spanner.TransactionOptions{
TransactionTag: “app:order-checkout”,
}
_, err := client.ReadWriteTransactionWithOptions(ctx, func(ctx context.Context, txn spanner.ReadWriteTransaction) error {
// 必要であれば、個別のクエリにリクエストタグを打つことも可能
// stmt := spanner.Statement{SQL: “SELECT …”, Tag: “query:fetch-user”}
// ここでビジネスロジックとDMLを実行
_, err := txn.Update(ctx, spanner.Statement{
SQL: `UPDATE Accounts SET Balance = Balance – 100 WHERE UserId = @userId`,
Params: map[string]interface{}{
“userId”: userID,
},
})
if err != nil {
return err
}
return nil
}, txnOpts)
if err != nil {
span.SetStatus(trace.Status{Code: trace.StatusCodeInternal, Message: err.Error()})
return fmt.Errorf(“order processing failed: %w”, err)
}
return nil
}
タグ命名規則の鉄則(Design Rule)
プロジェクトがスケールすると、タグがカオスになる。次のルールを設計ガイドラインとしてチームに強制してほしい。
1. 階層構造を持たせる: `[domain]:[action]`(例: `billing:invoice-generator`, `auth:token-refresh`)
2. 高カーディナリティな値を直接含めない:
- ❌ `app:checkout-user-12345` (ユーザーIDごとにタグが生成され、統計が爆発する)
- ⭕ `app:checkout` (タグは一定に保ち、詳細なIDはCloud Traceの属性やログに逃がす)
—
3. Cloud Traceによる分散トレーシング:ボトルネックの「透視」
タグを仕込んだら、次は Cloud Trace との連携だ。
Cloud Spannerのクライアントライブラリは、OpenCensus(またはOpenTelemetry)を通じて、SpannerのRPC呼び出し(`Commit`, `Read`, `ExecuteSql` など)を自動的にトレースツリーに組み込んでくれる。
現場で見るべきトレースのポイント
Cloud Traceのコンソールを開いたとき、プロフェッショナルはどこを見るか?
1. RPCのレイテンシ内訳
- `Commit` フェーズに異常な時間がかかっていないか?
- → もし `Commit` が遅いなら、ホットスポット(単一の分割キーへの書き込み集中) または ロック競合(Lock Contention) が発生している証拠だ。
2. リトライ(Aborted Transactions)の検知
- 楽観的同時実行制御(OCC)により、Spannerは競合が発生したトランザクションを `ABORTED` エラーとしてアボートし、自動リトライする。
- Cloud Trace上で、1つのリクエストツリーの中に同じクエリやコミットが複数回(Retry 1, Retry 2…)現れていないか確認しろ。ここに隠れたレイテンシの犯人がいる。
—
4. パフォーマンス上の注意点とアーキテクトからの忠告
最後に、タグ付けとトレーシングを導入するにあたって、実務上絶対に知っておかなければならない注意点を伝授する。
① タグのカーディナリティ制限に注意せよ
Spannerの内部統計やクエリ統計では、タグごとにメトリクスを集計する。前述した通り、タグの種類が数万・数百万レベルで動的に生成されるような設計にすると、Spannerの内部メタデータ領域を圧迫し、パフォーマンス劣化の原因となる。タグの総数は、システム全体で高々数十〜数百程度に抑えるべきだ。
② トレーシングのサンプリングレートの設計
高スループットなシステムで100%のトレースをCloud Traceに送り続けると、ネットワーク帯域の消費やコスト(課金額)がバカにならない。
プロダクション環境では、サンプリングレート(例: 1%〜5%)を適切に設定しつつ、エラーが発生したリクエストやレイテンシがP99を超えたリクエストは確実にキャプチャできるようにクライアント側のサンプリングポリシーをチューニングすること。
—
5. 結びにかえて
Cloud Spannerは魔法のデータベースではない。しかし、「正しく観測可能な状態(Observable)」 に設計されたSpannerは、これほど信頼性が高く、予測可能なスケーラビリティを持つデータベース他にはない。
「なぜ遅いのか分からない」という不毛な議論を、今日で終わりにしよう。
コードにタグを刻み、トレースの波形を読み解き、Spannerの鼓動を完全に手の内に収めるのだ。
それができるのは、画面の前の君たちプロフェッショナルエンジニアに他ならない。設計レビューでの健闘を祈る。
コメント