分散トレーシングの内部:コンテキスト伝播とサンプリング
trace_idがサービス境界をどう越えるか、なぜ1%抽出でも稀な異常を取り逃さないのか。分散トレーシングの内部動作を原理から理解し、サンプリング設計の判断軸が手に入ります。
- 1.W3C Trace Contextはtraceparent(trace-id/parent-id/trace-flags)とtracestateの2ヘッダで構成され、サンプリング判断をsampled bitで下流へ伝える。
- 2.head-basedは入口で確率pを掛けるだけで軽いが異常の事後選別ができず、tail-basedは全スパンをバッファしてから判断するため遅延・エラーを確実に残せる。
- 3.稀な異常を低サンプル率でも捉えられるのは、一様抽出ではなく属性に応じて採択確率を上げる重要度サンプリングを使うため。
なぜ「内部」を知る必要があるのか
分散トレーシング(/devops/distributed-tracing/)は概念としては「trace_id を伝播させてスパンをつなぐ」で済みますが、実務で「途中からトレースが切れる」「異常だけ残すサンプリングがうまく効かない」といった問題に当たると、ヘッダの形式・伝播の境界・サンプリングの判断タイミング という内部の三点を正確に押さえる必要が出てきます。本稿はその原理を、なぜそうなるかまで掘り下げます。
trace ID / span ID と W3C Trace Context
トレースの同一性は2つの識別子で表現されます。
- trace-id:リクエスト全体で共通の128ビット識別子。これが一致するスパンは同じトレースに属する。
- span-id / parent-id:個々のスパンを表す64ビット識別子。子スパンは親の span-id を
parent-idとして記録し、これでツリー構造が復元される。
サービス境界を越えるとき、これらは W3C Trace Context の HTTP ヘッダとして運ばれます。標準は traceparent と tracestate の2本です。
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
# └┬┘ └──────────────┬──────────────┘ └──────┬───────┘ └┬┘
# version trace-id (16 byte) parent-id (8 byte) trace-flags
tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
trace-flags の最下位ビットが sampled bit で、01 なら「このトレースは記録対象」を意味します。重要なのは、サンプリングの判断結果がこのビットに乗って下流へ伝わる 点です。各サービスが独立に確率を引くと一部スパンだけ採られた歯抜けのトレースになりますが、入口の判断を sampled bit で引き継げばトレース全体が「全採択」か「全不採択」のどちらかに揃います。
tracestate はベンダー固有の状態(独自のサンプリング優先度など)を key=value のリストで運ぶ補助ヘッダで、W3C 標準外の情報を traceparent を壊さずに添える役割です。
途中でトレースが分断されるのは、ほぼ常に サービス境界でヘッダを引き継ぎ損ねている からです。非同期キューやバッチでは HTTP ヘッダという運搬経路自体が無いため、メッセージ属性に traceparent を明示的に詰めて取り出す実装が要ります。自前 HTTP クライアントの自動計測漏れも典型例です。
head-based と tail-based の統計的トレードオフ
サンプリングは「いつ採否を決めるか」で二分されます。
| 観点 | head-based(ヘッド) | tail-based(テール) |
|---|---|---|
| 判断タイミング | リクエスト入口(最初のスパン生成時) | トレースの全スパンが出揃った後 |
| 必要なバッファ | 不要(判断したら即流す) | trace_idごとに完了まで全スパンを保持 |
| 異常の事後選別 | 不可能(結果を見る前に決める) | 可能(遅延・エラーを見て残せる) |
| コスト・複雑性 | 低い・分散実行できる | 高い・collectorに集約と状態が要る |
head-based は入口で確率 p(例 0.01)を一度だけ引き、その判断を sampled bit で全サービスに伝播します。実装が軽く、各サービスがローカルで完結します。欠点は 判断時点では結果が分からない こと。採択しなかったトレースで後からエラーが起きても、もう記録されていません。保存されるトレースの中でのエラー率は母集団のエラー率と等しく、稀な異常は確率 p でしか残らない のが統計的な限界です。
tail-based は逆に、trace_id ごとに全スパンを collector 側で一定時間バッファし、揃ってから 「最大レイテンシが閾値超」「いずれかの span がエラー」などの条件で採否を決めます。これにより異常トレースを選択的に残せますが、代償として、(1) 完了を待つ滞留メモリ、(2) どの collector インスタンスに同一 trace_id を集約するかのルーティング、(3) 遅れて到着するスパンを待つ打ち切り時間、という分散システム特有の難しさを抱えます。
なぜ低サンプル率でも稀少異常を捉えられるのか
「1% しか採らないなら、1万回に1回のバグは100万リクエスト見ても1回しか入らないのでは」という直感は、一様サンプリングを前提にした場合のみ正しい です。鍵は 重要度サンプリング(importance sampling) の考え方にあります。
一様抽出は全リクエストを同じ確率 p で採ります。対して重要度サンプリングは、「見たい事象」の採択確率を意図的に引き上げ、その分を後で重み付けで補正します。tail-based はこれを「事象ごとに採択確率を変える」形で実現します。
# 採択確率を事象の重要度に応じて変える例(tail-based ポリシー)
if span.has_error or trace.duration > p99_threshold:
keep_probability = 1.0 # 異常は全部残す
else:
keep_probability = 0.01 # 正常系は1%だけ
正常系を 1% に絞っても 異常系は採択確率 1.0 なので、エラーは取りこぼしません。全体の保存量は正常系が支配するため平均サンプル率は 1% 近くに収まる一方、稀少だが重要な事象だけは100%入る。これが「低い平均サンプル率と、異常の網羅性が両立する」原理です。
統計的に正確を期すなら、採択確率を変えた分は 逆数の重みで補正 します。確率 q で採ったトレースは集計時に 1/q 倍して数えると、母集団の本当の頻度を不偏推定できます。正常系を 1/0.01 = 100 倍、異常系を 1/1.0 = 1 倍するわけです。この補正を忘れると「エラー率が異常に高い」ように見える歪んだ集計になります。
OpenTelemetry では head-based は SDK の Sampler(例 ParentBased + TraceIdRatioBased)が担い、sampled bit を子へ伝播します。tail-based は Collector の tail_sampling プロセッサが担当し、latency や status_code などのポリシーを組み合わせます。head と tail は排他ではなく、入口で粗く絞り、collector で異常を拾い直す 二段構え が定石です。
設計上の含意
伝播とサンプリングは独立した話に見えて、sampled bit という1ビットでつながって います。head-based ではこのビットが最終判断そのもの。tail-based を併用するなら、入口で落としたトレースは collector に届かないため、tail で拾い直したい異常まで取りこぼさないよう 入口は採択寄り(高い p か全採択)に倒し、絞り込みは collector に任せる 設計が要ります。マイクロサービス(/devops/microservices/)が増えるほど伝播の抜け1か所がトレース全体を無価値にするため、境界ごとの伝播検証は SLO 監視(/devops/sre-slo/)と同じく継続的な点検対象です。
まとめ
- trace-id(128bit)と span-id/parent-id(64bit) がトレースの同一性とツリー構造を表し、
traceparentヘッダの sampled bit がサンプリング判断を下流へ伝播する。 - head-based は軽量だが異常の事後選別ができず稀少異常を確率
pでしか残せない。tail-based は全スパンをバッファして遅延・エラーを確実に残せる代わりに状態とルーティングのコストを負う。 - 低サンプル率でも稀少異常を捉えられるのは 重要度サンプリング——異常の採択確率だけ上げ、集計時に逆数の重みで補正することで、平均率の低さと網羅性を両立させるため。
DevOps/インフラ Article
分散トレーシングの内部:コンテキスト伝播とサンプリングを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
分散トレース
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 5
導入後に効く点
head-basedは入口で確率pを掛けるだけで軽いが異常の事後選別ができず、tail-basedは全スパンをバッファしてから判断するため遅延・エラーを確実に残せる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「分散トレース / W3C Trace Context」に近いか確認する。
- 強みである「W3C Trace Contextはtraceparent(trace-id/parent-id/trace-flags)とtracestateの2ヘッダで構成され、サンプリング判断をsampled bitで下流へ伝える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。