概要
トレーシングで重要なのがSamplingです。
単純に全データを計測するとコストが増えたりパフォーマンスに影響が出たりします。
OpenTelemetryではSamplingに対する仕様が決まっているので、それに基づいて具体的な実装を紹介します。
OpenTelemetry Specification - Sampling
環境
- Go v1.20.2
- go.opentelemetry.io/otel/trace v1.14.0
前提知識
サンプリング手法
サンプリングには大きく2つのタイプがあります。
Head basedとTail basedです。
ref: https://newrelic.com/jp/blog/best-practices/distributed-tracing-and-sampling
Head based sampling
シンプルでよく使われるのがHead based samplingです。
分散トレースする場合にアプリケーションレイヤでサンプリングする際はこちらになります。
Pros
- シンプル
- パフォーマンス影響が少ない
- 統計的なサンプリングにより十分な透明性を与える
Cons
- ランダムなので欲しいトレース(エラーだったり遅かったり)がサンプリングされない事がある
Tail based sampling
エラーとレイテンシの高いトレースを漏れなく観測したい場合はこちらになります。
OpenTelemetry Collectorのように、一度トレースを集約させてからサンプリングする場合はこのような方法が可能です。
Pros
- 100%のトレースを計測した後でサンプリングするので欲しいデータを選択できる
Cons
- データ量が増えるのでパフォーマンスに影響を与える(CPU、メモリ、転送量)
- ↑によりコストも増える可能性がある
- OpenTelemetry CollectorのようなProxyを用意する必要がある
OpenTelemetryのSampler
Samplerはどの程度標本抽出するかをカスタマイズできるインタフェースです。
Build-in sampler
あらかじめ仕様で用意されているSamplerについて紹介します。
Sampler | 説明 |
---|---|
AlwaysOn | 全て計測 |
AlwaysOff | 全て破棄 |
TraceIdRatioBased | 指定されたサンプリングレート分だけ計測 |
ParentBased | 親スパンの設定に基づいて計測 |
ParentBasedはリモートかローカルか、親がサンプリングしているかどうかで条件が変わるので、以下の様になっています。
親 | 親がリモートか | 親はサンプリングされているか | 呼び出されるSampler | デフォルト値 |
---|---|---|---|---|
なし (ルートSpan) |
- | - | root() |
- |
あり | true | true | remoteParentSampled() |
AlwaysOn |
あり | true | false | remoteParentNotSampled() |
AlwaysOff |
あり | false | true | localParentSampled() |
AlwaysOn |
あり | false | false | localParentNotSampled() |
AlwaysOff |
Head based samplingの場合、本番ではParentBased
にTraceIdRatioBased
をルートSpanとして扱うパターンが良いです。
実装
具体的な実装方法について説明します。
リモートスパンを扱えるよう OpenTelemetryで分散トレーシング - Carpe Diem で行った分散トレーシング環境を想定します。
Goでの実装
TraceProviderでsdktrace.WithSampler()を用いて設定します。
func NewTracerProvider(serviceName string, ratio float64) (*sdktrace.TracerProvider, func(), error) { exporter, err := NewJaegerExporter() if err != nil { return nil, nil, err } r := NewResource(serviceName, "1.0.0", "local") tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(r), sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(ratio))), // ここ ) otel.SetTracerProvider(tp)
sdktrace.ParentBased()はルートSpanの設定にサンプリングレート指定のsdktrace.TraceIDRatioBased()を入れ、それ以外はデフォルト設定とします。
sdktrace.ParentBased(sdktrace.TraceIDRatioBased(ratio))
は
sdktrace.ParentBased( sdktrace.TraceIDRatioBased(ratio), sdktrace.WithLocalParentSampled(sdktrace.AlwaysSample()), sdktrace.WithLocalParentNotSampled(sdktrace.NeverSample()), sdktrace.WithRemoteParentSampled(sdktrace.AlwaysSample()), sdktrace.WithRemoteParentNotSampled(sdktrace.NeverSample()), ))
と同値です。
動作確認
Gateway: ratio 0.1, Backend: ratio 0.1
ratioを0.1
にして検証してみます。
$ ab -n10 -c 10 http://localhost:8000/hello
設定通り1/10だけサンプリングされました。
Gateway: ratio 0.1, Backend: ratio 0.5
ParentBasedがきちんと機能しているかの確認のため、Backend側のratioを0.5
に変更してみます。
backend-grpc: build: context: . dockerfile: Dockerfile args: - TARGET=backend-grpc environment: - EXPORTER_ENDPOINT=http://jaeger:14268/api/traces - SAMPLING_RATIO=0.5
期待通り1/10だけサンプリングされました。
Gateway: ratio 0.5, Backend: ratio 0.1
Gateway側のratio(親のルートSpan)を0.5
に変更してみます。
4/10だけサンプリングされました。
もう一度実行すると
9/20になったので、若干ズレは発生するようです。
その他
サンプルコード
今回のサンプルコードはこちらです。
まとめ
OpenTelemetryでのSamplingの仕様とGoでの実装方法について説明しました。