OpenTelemetry OpenTelemetry

Kosten mit Sampling kontrollieren: SDK- und Collector-Konfiguration

Entscheide, wann du im SDK sampelst und wann Tail Sampling in einem zweistufigen Collector nutzt.

15 min Experte

Jeden Trace zu behalten wird schnell teuer, und die meisten sind Routineanfragen, die nie jemand öffnen wird. Sampling behält eine repräsentative oder interessante Teilmenge. OpenTelemetry bietet zwei Orte für die Entscheidung: im SDK, wenn ein Trace beginnt (Head Sampling), und im Collector, nachdem der Trace abgeschlossen ist (Tail Sampling). Beides ist reine Konfiguration. Diese Anleitung zeigt jedes und wie du wählst.

Head Sampling im SDK

Die Entscheidung fällt, wenn der Root-Span erstellt wird, sie ist also billig und braucht keine zusätzliche Infrastruktur. Sie kann nicht wissen, ob die Anfrage fehlschlagen oder langsam sein wird, weil das noch nicht passiert ist.

export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

Die von der Spezifikation definierten Sampler-Namen sind unter anderem always_on, always_off, traceidratio, parentbased_always_on (der Standard), parentbased_always_off und parentbased_traceidratio. Nutze die parentbased_-Varianten: Sie folgen der Sampling-Entscheidung des Parents der eingehenden Anfrage, sodass ein Trace über Services hinweg entweder ganz behalten oder ganz verworfen wird. Ein einfaches traceidratio in jedem Service würde unabhängige Entscheidungen treffen und kaputte Traces erzeugen.

In der deklarativen Konfigurationsdatei sieht dieselbe Idee so aus:

tracer_provider:
  sampler:
    parent_based:
      root:
        trace_id_ratio_based:
          ratio: 0.1

Prüfe die Dokumentation deines SDKs auf die Sampler-Namen, die es im Dateiformat unterstützt.

Tail Sampling im Collector

Tail Sampling wartet, bis ein Trace vollständig ist, und wendet dann Policies an. Es lässt dich jeden Fehler und jeden langsamen Trace plus einen kleinen Anteil des Rests behalten, was Head Sampling nicht kann. Der Preis ist mehr Infrastruktur und Speicher.

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    expected_new_traces_per_sec: 100
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: keep-slow
        type: latency
        latency:
          threshold_ms: 2000
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

So funktioniert es:

  • decision_wait gibt an, wie lange der Prozessor nach dem ersten Span eines Traces wartet, bevor er entscheidet. Es muss länger sein als die Dauer deines langsamsten Traces, sonst entscheidest du anhand unvollständiger Traces.
  • num_traces gibt an, wie viele Traces beim Warten im Speicher gehalten werden. Dimensioniere es auf etwa deine Trace-Ankunftsrate mal decision_wait, mit Reserve.
  • policies werden für jeden Trace ausgewertet. Ein Trace wird behalten, wenn irgendeine Policy sagt, ihn zu behalten, die drei oben bedeuten also „behalte Fehler, behalte langsame und behalte zusätzlich 5 Prozent von allem“.
  • Weitere Policy-Typen sind string_attribute (einen Wert wie eine Kundenstufe matchen), numeric_attribute, always_sample, and (Policies kombinieren) und composite (Rate über Policies zuteilen).

tail_sampling ist in der Contrib-Distribution, und seine Optionsnamen entwickeln sich weiter, prüfe also seine README für deine Version.

Der Haken: ein Trace muss einen Collector erreichen

Tail Sampling funktioniert nur, wenn alle Spans eines Traces bei derselben Collector-Instanz ankommen. Bei mehreren Replikas hinter einem einfachen Load Balancer verstreuen sich die Spans eines Traces, und Entscheidungen werden auf Fragmenten getroffen. Die Standardlösung sind zwei Stufen:

  1. Stufe 1 (Load-Balancing-Schicht) nutzt den Exporter loadbalancing, um Spans nach Trace-ID konsistent an eine Instanz der Stufe 2 zu routen.
  2. Stufe 2 (Sampling-Schicht) betreibt tail_sampling und exportiert die behaltenen Traces.

Stufe 1:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

exporters:
  loadbalancing:
    routing_key: traceID
    protocol:
      otlp:
        tls:
          insecure: true
    resolver:
      dns:
        hostname: otel-sampler-headless.observability.svc.cluster.local
        port: 4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [loadbalancing]

Stufe 2:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 20
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: keep-errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: sample-the-rest
        type: probabilistic
        probabilistic:
          sampling_percentage: 10
  batch:

exporters:
  otlp/backend:
    endpoint: tempo:4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/backend]

Nutze einen Headless Service, damit DNS die Pods der Stufe 2 auflistet, und beachte: Wenn Pods hoch- oder herunterskalieren, verschiebt sich das Routing, und einige Traces im Flug können anhand unvollständiger Daten entschieden werden. Die Schichten getrennt zu halten isoliert außerdem Fehler: Ein Problem beim Sampling blockiert die Aufnahme nicht.

Head und Tail kombinieren

Ein gängiges und sinnvolles Setup:

  • Ein moderater Head-Sampler im SDK (zum Beispiel 50 Prozent), um das Volumen an der Quelle für sehr verkehrsreiche Services zu senken.
  • Tail Sampling im Collector, um zu garantieren, dass Fehler und langsame Anfragen überleben.

Sei vorsichtig: Was am Head verworfen wird, kann der Tail nicht wiederherstellen. Musst du jeden Fehler behalten, sample diese Services am Head nicht aggressiv.

Metriken überleben das Sampling, wenn du sie zuerst ableitest

Traces zu verwerfen verwirft auch die Zahlen, die du daraus berechnen könntest, etwa Anfrageraten und Latenz-Perzentile. Erzeuge Metriken vor dem Sampling-Schritt mit dem Connector spanmetrics, damit Dashboards genau bleiben:

connectors:
  spanmetrics:

service:
  pipelines:
    traces/all:
      receivers: [otlp]
      exporters: [spanmetrics, loadbalancing]
    metrics/derived:
      receivers: [spanmetrics]
      processors: [batch]
      exporters: [otlphttp/prometheus]

Hier speist jeder Span den Connector, während nur die gesampelte Menge zum Speicher weitergeht.

Wählen

ZielAnsatz
Kosten einfach senken, keine neue InfrastrukturHead Sampling mit parentbased_traceidratio
Alle Fehler und langsamen Anfragen behaltenTail Sampling in einem zweistufigen Collector
Genaue Raten und Perzentile behaltenspanmetrics vor dem Sampling
Wenige Services, geringes VolumenKein Sampling; überprüfe es, wenn Kosten zählen

Prüfen

  • Vergleiche in Stufe 2 die Span-Zahlen ein und aus mit den eigenen Metriken des Collectors (Port 8888), um zu bestätigen, dass das Verhältnis zu deinen Policies passt.
  • Suche nach einer bekannten fehlgeschlagenen Anfrage und stelle sicher, dass ihr gesamter Trace vorhanden ist.
  • Beobachte den Speicher in Stufe 2, weil sie Traces für decision_wait hält. Wächst er, senke num_traces oder füge Replikas hinzu und halte memory_limiter davor.

Beliebte Anleitungen