テールサンプリングの統計と希少異常の捕捉
エラーや高遅延の希少トレースを取りこぼさず保存料を抑えたい人へ。トレース完了後に重要度で残すテールサンプリングのバッファリングコストと完全性、ヘッドサンプリングとの違い、バイアス付き抽出の統計を原理から解説します。
- 1.ヘッドサンプリングはトレース開始時に確率pで残すか決めるため軽量だが、その時点ではまだエラーや高遅延が起きておらず、希少な異常トレースをほぼ確実に取りこぼす。判断材料がないのが本質的限界。
- 2.テールサンプリングはトレースが完了するまでスパンをバッファに溜め、エラー・高遅延・特定属性といった完了後にしか分からない重要度で残す。代償はバッファのメモリと、同一トレースのスパンを1台に集める集約コスト。
- 3.全数保存できないなら、希少な異常を高確率で残し正常を間引く『バイアス付き抽出』が最適。残存確率の逆数で重み付け(Horvitz-Thompson推定)すれば、偏った標本からでも母集団のレートを不偏推定できる。
なぜサンプリングが要るのか、そしてどこで決めるのか
分散トレーシングは1リクエストの経路を多数のスパンとして記録します(/devops/distributed-tracing/)。だが高トラフィックなサービスで全トレースを保存すると、ストレージとインジェスト帯域が爆発します。秒間数十万リクエストのトレースを丸ごと貯めれば、観測のコストが本番のコストを上回りかねない。そこで一定割合だけ残すサンプリングが要ります。
問題は「いつ・何を根拠に残すか」です。ここで二つの方式が分かれます。ヘッドサンプリングはトレースの開始時に「このトレースを残すか」を決めます。テールサンプリングはトレースが完了してから、収集した全スパンを見て決めます。この一見小さな違いが、希少な異常を捕まえられるかどうかを決定づけます。
スパン単位ではなくトレース単位で残すのが原則です。あるスパンを残し別のスパンを捨てると、トレースに穴が空き因果が途切れて無価値になる。だから残す/捨てるの判断はトレース内の全スパンで一貫していなければならない。ヘッドサンプリングはこの一貫性をトレースID由来の確率判定で、テールサンプリングは全スパン集約後の一括判定で担保します。
ヘッドサンプリングの仕組みと本質的限界
ヘッドサンプリングは、トレースのルートスパン生成時にサンプリング判定を下し、その決定を sampled フラグとしてコンテキストに載せて下流へ伝播します(/devops/tracing-context-propagation/)。典型は確率的サンプリングで、トレースIDのハッシュを [0,1) に正規化し、それがサンプリングレート p 未満なら残す。
ヘッドサンプリング(確率的)
p = 0.01 # 1%を残す
r = hash(traceID) / 2^k # traceIDを[0,1)へ写像(決定的)
sampled = (r < p) # rがp未満なら残す
# この判定を全サービスがtraceIDから再計算 → トレース全体で一貫
ハッシュを使う巧妙さは、どのサービスも同じ traceID から同じ判定を導ける点にあります。中央調整なしにトレース全体で残す/捨てるが一致し、決定はゼロに近いコストで下せて、スパンをバッファに溜める必要もない。これがヘッドサンプリングの最大の長所です。
ところが致命的な弱点があります。判定の時点では、そのトレースがエラーになるか高遅延になるかをまだ知らない。ルートスパンが始まった瞬間に、500ms後のタイムアウトや末端サービスの例外は予見できません。つまりヘッドサンプリングは結果と無関係なランダム抽出であり、エラーや高遅延が全体の0.1%しかない希少現象なら、その異常トレースも確率 p でしか残らない。
エラー率が0.1%、ヘッドサンプリングのレートが1%だとします。1万リクエスト中エラーは10件。そのうち残るのは期待値で 10 × 0.01 = 0.1件 ——つまり大半のデバッグで「該当トレースが1本も無い」状態に陥ります。レートを上げれば捕捉率は上がりますが、正常トレースまで巻き添えで増え、保存料が線形に膨らむ。残すべき希少な異常と、間引きたい大量の正常が、同じ確率で扱われてしまうのが本質的限界です。
テールサンプリングの仕組みとコスト
テールサンプリングは判断をトレース完了後まで遅らせます。コレクタは到着するスパンを traceID ごとにバッファへ蓄積し、一定の待機時間(decision wait)が過ぎてトレースが出揃ったとみなしたら、全スパンを材料に残すかを決めます。判定ルールは結果を見て書けます——「いずれかのスパンが error なら残す」「ルートの所要時間が閾値を超えたら残す」「特定の customer.tier=premium 属性を含むなら残す」。
テールサンプリング(ポリシー評価)
buffer[traceID].append(span) # 完了まで溜める
on decision_timer(traceID): # decision wait経過
t = buffer[traceID]
if any(s.status == ERROR for s in t): decision = KEEP # 異常は全部残す
elif root_duration(t) > 1s: decision = KEEP # 高遅延も残す
else: decision = (rand() < p_normal) # 正常は間引く
emit_or_drop(t, decision); free(buffer[traceID])
これで希少異常は確率1で残り、正常だけを p_normal で間引けます。ただし代償は二つあります。
第一にメモリ。完了までスパンを保持するので、バッファ常駐量 ≒ トレース到着率 × decision wait × 1トレースの平均スパン量。decision wait を長く取るほど遅いトレースも取りこぼさないが、メモリ常駐が増えます。待機中にメモリ上限を超えれば、判定前のトレースを溢れさせる(=取りこぼし)しかなく、完全性とメモリのトレードオフが生じます。
第二に集約(アフィニティ)コスト。判定には1トレースの全スパンが1か所に揃う必要があります。複数のコレクタにスパンが分散すると判定できないので、traceID で同一コレクタへ振り分ける必要がある(consistent hashing による trace-aware なルーティング)。このスパンの寄せ集めが、ステートレスに流せるヘッドサンプリングには無い運用負荷です。
| 観点 | ヘッドサンプリング | テールサンプリング |
|---|---|---|
| 判定時点 | トレース開始時 | トレース完了後 |
| 判定材料 | traceIDのみ(結果は不明) | 全スパン(error/遅延/属性) |
| 希少異常の捕捉 | 確率pでしか残らず取りこぼす | 条件一致なら確率1で残せる |
| メモリ | バッファ不要・ほぼゼロ | decision wait分の常駐が必要 |
| 集約 | 不要(ステートレス) | traceIDで同一コレクタへ集約必須 |
| 完全性のリスク | なし(即決) | decision wait切れ・遅延スパンで欠落 |
テールサンプリングの完全性は decision wait の長さ に依存します。短すぎると、まだ完了していないトレースを「出揃った」と誤判定し、後から来たスパンを取りこぼす。長すぎるとメモリが膨らむ。さらにルートスパンが一番最後に届く実装では「いつ完了したか」の判定自体が難しい。実務では root を観測したら待機を打ち切る、最大待機を上限に切る、といったヒューリスティックで 完全性とメモリの綱引き を調停します。判定漏れは静かなデータ欠落として残るため、バッファ溢れ率を監視するのが定石です。
バイアス付き抽出の統計:偏った標本から正しいレートを出す
テールサンプリングは「異常を高確率、正常を低確率で残す」バイアス付き抽出(biased / stratified sampling)です。便利ですが、素朴に保存済みデータを数えると統計が壊れます。エラーを100%、正常を1%で残した標本でエラー率を計算すると、エラーが過剰代表され、実際よりはるかに高いエラー率に見えてしまう。
正しく扱う鍵は、各トレースに残存確率の逆数の重みを与えることです。これは標本調査の Horvitz–Thompson 推定 に当たります。トレース i の残存確率を p_i とすると、その重みは w_i = 1 / p_i。母集団の総数や総量は、残った標本の重み付き和で不偏推定できます。
Horvitz-Thompson(残存確率の逆数で重み付け)
各トレースに残存確率 p_i を記録(例 error=1.0, normal=0.01)
重み w_i = 1 / p_i
母集団のエラー総数の推定:
Σ_{残ったerrorトレース} w_i # error は p=1 なので w=1 → そのまま件数
母集団の総リクエスト数の推定:
Σ_{残った全トレース} w_i # normal は w=100 で薄めた分を埋め戻す
エラー率の推定 = (errorの重み付き和) / (全体の重み付き和)
直観はこうです。1%だけ残した正常トレース1本は「自分の背後に捨てられた99本がいる」とみなし、重み100で数える。100%残したエラーは重み1のまま。こうして間引きで歪んだ標本を、母集団スケールへ復元できる。重みを使えば偏った標本からでもエラー率・スループット・レイテンシ分布を不偏に再構成できます。逆に重みを記録し損ねると後から復元不能になるため、サンプリング側は各トレースに残存確率(または重み)を必ず付与して保存しなければなりません。
件数やレートは重み付き和で素直に復元できますが、テイルのパーセンタイル(p99 など)は注意が要ります。高遅延を優先的に残す方式だと、遅いトレースが過剰代表され、重みで補正しないと p99 が実際より悪く見える。分位点の推定には重み付き分位点を使うか、そもそも分位点はサンプリング前のメトリクスやヒストグラム側で計測し、トレースは原因究明に使う、と役割を分けるのが堅実です(/devops/percentile-latency-statistics/)。トレース標本で全体のレイテンシ統計を語るのは事故のもとです。
OpenTelemetry での実装位置と組み合わせ
OpenTelemetry では、ヘッドサンプリングは SDK 内の Sampler(TraceIdRatioBased や ParentBased)で生成側に、テールサンプリングは Collector の tailsamplingprocessor で収集側に置かれます(/devops/opentelemetry-internals/)。両者は排他ではなく併用が現実的です。SDK 側で明らかに不要な高頻度トレース(ヘルスチェック等)を軽くヘッドで落として帯域を削り、残りを Collector のテールで「異常を必ず残す」精緻な判定にかける、という二段構え。
ただし併用には落とし穴があります。ヘッドで捨てたトレースはテールに届かないので、ヘッド側でうっかり異常になり得るトレースまで間引くと、テールが幾ら賢くても復元できない。だから上流のヘッドは「結果に無関係で安全に捨てられるもの」に限定し、結果依存の重要度判定はすべてテールに委ねるのが原則です。
ヘッドは開始時に traceID だけで決めるため軽量・ステートレスだが希少異常を取りこぼす。テールは完了後に全スパンを見るため異常を確率1で残せるがバッファのメモリと traceID 集約のコストを払う ——この対比を一文で言えると強いです。さらに バイアス付き抽出のデータは残存確率の逆数(Horvitz-Thompson 重み)で補正して初めて母集団レートを不偏推定できる こと、decision wait が完全性とメモリのトレードオフを決める ことを挙げられると上級者向けの加点になります。
まとめ
- サンプリングはトレース単位で一貫して行う。スパンを部分的に捨てると因果が切れて無価値になる。
- ヘッドサンプリングは開始時に traceID から確率判定するため軽量・ステートレスだが、その時点で結果が未確定なので希少な異常(エラー・高遅延)を取りこぼす。
- テールサンプリングは完了まで全スパンを溜めてから重要度で残すので異常を確率1で捕捉できる。代償は decision wait 分のメモリ常駐と、traceID を同一コレクタへ寄せる集約コスト。
- decision wait は完全性とメモリのトレードオフを決める。短いと遅れて来るスパンを取りこぼし、長いとメモリが膨らむ。
- 異常優先の保存はバイアス付き抽出。各トレースに残存確率を記録し、その**逆数で重み付け(Horvitz-Thompson)**すれば、偏った標本から母集団のレートを不偏推定できる。重みを記録し損ねると復元不能。
- 実装は SDK のヘッドと Collector のテールを併用するのが現実的。ただしヘッドで結果依存の異常を間引かないこと——テールでは取り戻せない。
DevOps/インフラ Article
テールサンプリングの統計と希少異常の捕捉を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
分散トレーシング
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
テールサンプリングはトレースが完了するまでスパンをバッファに溜め、エラー・高遅延・特定属性といった完了後にしか分からない重要度で残す。代償はバッファのメモリと、同一トレースのスパンを1台に集める集約コスト。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「分散トレーシング / サンプリング」に近いか確認する。
- 強みである「ヘッドサンプリングはトレース開始時に確率pで残すか決めるため軽量だが、その時点ではまだエラーや高遅延が起きておらず、希少な異常トレースをほぼ確実に取りこぼす。判断材料がないのが本質的限界。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。