テールサンプリングはタダではない — 三つのコスト
一言でいうと
ヘッドサンプリングは、まだ何も起きていない時点で判断するため、まれな事象を、そのまれさの分だけ正確に捨てます。テールサンプリングは結果を見て判断しますが、3つのコストを払います。ネットワークは減らず、メモリを大きく消費し、trace IDに基づくルーティングが必要です。ヘッドで捨てたものは、テールで復活させられません。
なぜ必要なのか
全量保存は、ほとんどの組織でコストが合いません。問題は、何を捨てるかです。ヘッドサンプリング1%で、1時間あたり5件のエラーを調査するとしましょう。
보존되는 오류 트레이스 = 5 × 0.01 = 시간당 0.05건
1건을 보려면 평균 20시간 대기
このコードブロックの韓国語は、残るエラートレースが5×0.01で1時間あたり0.05件になり、1件を見るのに平均20時間待つことになる、という意味です。
これがヘッドサンプリングの実質的な結末です。調査が必要なときにそのトレースはなく、残っているトレースはすべて正常なリクエストで、見る理由がありません。
どう動くのか
テールサンプリングは、トレースが完了するまでスパンをバッファにためておき、結果を見て判断します。エラーがあれば残し、遅ければ残し、残りは少量だけ残します。ポリシーはそれぞれ「残す条件」で、複数のポリシーのうち1つでも一致すれば保持されます。そのため、ヘルスチェックを除外するには、一致を反転させるinvert_matchを使います。
| 項目 | ヘッドサンプリング | テールサンプリング |
|---|---|---|
| 判断のタイミング | ルートスパン生成時 | decision_wait経過後 |
| エラー・遅いリクエストの保持 | 確率的にのみ | ルールで100% |
| エージェント・ネットワークのコスト | 比率の分だけ減少 | 減少なし |
| バックエンドの保存コスト | 減少 | 減少 |
| コレクターのメモリ | 無視できる | 1秒あたりのトレース数×待ち時間の分のバッファ |
| 運用要件 | なし | trace IDに基づくルーティングが必須 |
コスト1: ネットワークは減りません。判定するにはすべてのスパンがコレクターに到着する必要があるため、節約できるのはバックエンドの保存とインデックス作成だけです。コレクター自体のCPUとネットワークは、むしろ増えます。
コスト2: メモリ。decision_waitの間、すべてのスパンを保持し続ける必要があります。計算式は単純です。
초당 트레이스 수 × decision_wait(초) × 트레이스당 스팬 수 × 스팬 크기 = 버퍼 메모리
예: 10,000 trace/s × 30s × 12 스팬 × 1.2KB
= 4,320,000 KB = 약 4,219 MB (여유 2배면 약 8,438 MB)
このコードブロックの韓国語は、バッファメモリが「1秒あたりのトレース数×decision_wait(秒)×トレースあたりのスパン数×スパンのサイズ」で求まることと、その計算例、および余裕を2倍とった場合の値を示しています。
num_tracesは、同時にメモリに保持するトレース数の上限で、超過すると最も古いトレースが強制的に判定されます。必要な値も同じ掛け算から出ます。1秒あたりのトレース数×decision_waitです。10,000 trace/sで30秒待機なら、300,000が必要です。
コスト3: ルーティング。同じトレースのすべてのスパンが、同じコレクターインスタンスに到着する必要があります。コレクターを複数台に増やして、前に通常のロードバランサーを置くと、1つのトレースのスパンが複数のインスタンスに散らばり、それぞれが断片を見て判定することになり、結果は、ランダムに切れたトレースです。解決策は、前段にrouting_key: traceIDを使うloadbalancingエクスポーターの層を置くことです。
同じ理由で、DaemonSetではテールサンプリングは不可能です。ノードごとにコレクターが起動する構造では、1つのトレースのスパンが複数のノードに散らばって到着するため、各エージェントがトレースの一部しか見られません。テールサンプリングは、Deployment(ゲートウェイ)の層でのみ使います。
現場での姿
最も高い授業料を払った事故は、num_tracesを50,000にしたままトラフィックが増えたケースでした。ピークが10,000 trace/sでdecision_waitが30秒だったので、必要な値は300,000でした。上限が6分の1だったため、古いトレースが強制判定され続け、メモリ圧迫が重なって、コレクターがOOMで再起動を繰り返し、5分間分のデータを失いました。メトリクスは、ほとんど異常を報告しませんでした。データは「正常に」送信されていて、バックエンドで特定のトレースだけがない形だったからです。
教訓は2つあります。1つ目は、tail_samplingを有効にする前に、先に掛け算をすることです。2つ目は、データが消えているのにメトリクスが静かなら、パイプラインからtail_samplingを一時的に外して、再現するかを見ることです。これが最も早い切り分け方です。
実務での組み合わせは、おおむね次のとおりです。トラフィックが非常に多いサービスは、ヘッドで10–50%に事前に減らし、その上にテールを重ねて、エラーと遅いリクエストを拾います。ヘッドですでに捨てたものはテールで復活させられないため、ヘッドの比率は、耐えられる範囲でできるだけ高く設定するのが原則です。
次のラボですること
/root/otca-sampling/の下に、テールサンプリングポリシーを5つ書きます。エラーと遅延を100%残す結果ベースのポリシー、invert_matchでヘルスチェックを除外するポリシー、VIPテナントと遅延をandで組み合わせた複合ポリシー、そして残りのための確率ポリシーです。そのあと、前段のloadbalancingエクスポーターを設定し、バッファメモリを自分で計算してファイルに残し、最後にその値をそのまま入れたOpenTelemetryCollector CRを書きます。