Tail-based Sampling im OpenTelemetry Collector: Alle Fehler behalten, 80 % der Kosten sparen
Wie wir Sampling-Pipelines im Petabyte-Maßstab betreiben, welche Fehler wir dabei gemacht haben und was wir zurück in das OpenTelemetry-Projekt gegeben haben.
Zusammenfassung
- Head-based Sampling entscheidet, bevor es weiß, ob eine Trace interessant ist. Tail-based Sampling entscheidet danach und behält garantiert alle fehlerhaften und langsamen Traces.
- Der Preis: Der Collector muss Spans puffern und alle Spans einer Trace auf derselben Instanz sehen. Wir zeigen eine zweistufige Collector-Topologie, die das zuverlässig löst.
- Mit fünf Policies reduzieren unsere Kunden das gespeicherte Trace-Volumen im Schnitt um 80 %, ohne einen einzigen Fehler-Trace zu verlieren.
Jedes Team, das Distributed Tracing in Produktion einführt, stößt nach wenigen Wochen auf dieselbe Frage: Müssen wir wirklich jede Trace speichern? Die ehrliche Antwort ist nein. Die meisten Traces sind erfolgreiche Anfragen mit normaler Latenz, und davon braucht niemand Millionen pro Stunde. Aber die interessanten Traces, die mit Fehlern, Timeouts und seltsamen Latenzen, will man vollständig haben. Genau die verliert klassisches Head-based Sampling.
In diesem Beitrag beschreiben wir, wie wir Tail-based Sampling mit dem OpenTelemetry Collector betreiben, welche Policies sich bewährt haben und welche Fehler wir unterwegs gemacht haben. Alles hier Beschriebene funktioniert mit dem Open-Source-Collector; Obseria ist nur das Backend, in das die Traces am Ende fließen.
Head-based vs. Tail-based: Wann wird entschieden?
Head-based Sampling entscheidet am Anfang einer Trace, im SDK des ersten Services: „Diese Anfrage behalten wir mit 10 % Wahrscheinlichkeit.“ Die Entscheidung wird über den Trace-Context propagiert. Das ist billig und einfach, aber das SDK weiß zu diesem Zeitpunkt nicht, ob die Anfrage 40 Millisekunden später mit einem 502 enden wird. 90 % aller Fehler-Traces sind weg, bevor sie entstehen.
Tail-based Sampling verschiebt die Entscheidung an das Ende: Der Collector sammelt alle Spans einer Trace, wartet, bis sie vollständig ist, und wendet dann Policies an. Fehlerhaft? Behalten. Langsamer als p99? Behalten. Seltener Endpunkt? Behalten. Alles andere mit 5 % Wahrscheinlichkeit. Der Preis dafür sind Speicher und eine Anforderung an die Architektur, auf die wir gleich kommen.
Fünf Policies, die sich bewährt haben
Nach zwei Jahren Betrieb bei Kunden mit Volumen von 10.000 bis 2 Millionen Spans pro Sekunde haben sich fünf Policies als Standard etabliert. Hier die Konfiguration des tail_sampling-Prozessors:
processors:
tail_sampling:
decision_wait: 10s
num_traces: 200000
expected_new_traces_per_sec: 5000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 1500 }
- name: rare-endpoints
type: string_attribute
string_attribute:
key: http.route
values: [ "/api/v1/refund", "/api/v1/export/.*" ]
enabled_regex_matching: true
- name: debug-header
type: string_attribute
string_attribute: { key: debug.trace, values: ["true"] }
- name: baseline
type: probabilistic
probabilistic: { sampling_percentage: 5 }
Drei Dinge dazu: decision_wait muss länger sein als Ihre längste Trace, sonst werden späte Spans gegen eine bereits getroffene Entscheidung geprüft und fallen heraus. num_traces bestimmt den Speicherbedarf; rechnen Sie mit etwa 1 bis 2 KB pro gepufferter Trace. Und die Policies sind mit ODER verknüpft: Eine Trace wird behalten, sobald eine Policy zutrifft.
Die Collector-Topologie: Alle Spans einer Trace an einem Ort
Hier liegt die eigentliche Schwierigkeit. Tail-based Sampling funktioniert nur, wenn alle Spans einer Trace auf derselben Collector-Instanz landen. Mit einem Load Balancer davor, der Round-Robin verteilt, ist das nicht der Fall: Die Hälfte der Spans einer Trace ist auf Instanz A, die andere auf B, und beide treffen Entscheidungen mit unvollständigen Daten.
Die Lösung ist eine zweistufige Topologie mit dem loadbalancing-Exporter:
exporters:
loadbalancing:
routing_key: traceID
protocol:
otlp: { tls: { insecure: true } }
resolver:
k8s:
service: otel-sampler.observability
ports: [4317]
Der Exporter hasht die Trace-ID und schickt alle Spans derselben Trace an dieselbe Sampler-Instanz. Der Kubernetes-Resolver beobachtet die Endpoints des Sampler-Services; beim Skalieren werden Traces, die gerade in der Schwebe sind, kurzzeitig falsch zugeordnet. Deshalb skalieren wir Stufe 2 nie unter Last, sondern in ruhigen Zeitfenstern.
Drei Fehler, die wir gemacht haben
- Zu kurzes
decision_wait. Unser erster Wert waren 2 Sekunden. Batch-Jobs mit 30-Sekunden-Traces wurden systematisch zerschnitten. Heute sind 10 bis 30 Sekunden Standard, abhängig vom längsten erwarteten Trace. - Speicher unterschätzt. Bei 50.000 neuen Traces pro Sekunde und 10 Sekunden Wartezeit puffern Sie 500.000 Traces. Mit 2 KB pro Trace ist das 1 GB, bei Lastspitzen mehr. Planen Sie Limits und Alerts auf
otelcol_processor_tail_sampling_sampling_trace_dropped_too_early. - Metriken aus Spans nach dem Sampling berechnet. Wer RED-Metriken (Rate, Errors, Duration) aus gesampelten Spans ableitet, bekommt eine Fehlerrate von 60 %, weil Fehler überrepräsentiert sind. Der
spanmetrics-Connector gehört vor den Sampler.
Das Ergebnis in Zahlen
Über alle Kunden, die diese Topologie einsetzen, sehen wir im Median ein gespeichertes Trace-Volumen von 18 bis 22 % des Ausgangswerts, bei 100 % Abdeckung aller Traces mit Fehlerstatus oder Latenz über dem Schwellwert. Da Obseria nach gespeicherten Spans abrechnet und nicht nach empfangenen, schlägt sich das eins zu eins in der Rechnung nieder.
Faustregel: Starten Sie mit errors + latency + probabilistic 10 %, messen Sie eine Woche lang das Verhältnis von gespeicherten zu empfangenen Spans, und senken Sie dann die Baseline schrittweise. Die Fehler-Traces bleiben ohnehin vollständig.
Was wir zurückgegeben haben
Zwei Dinge aus dieser Arbeit sind inzwischen Teil des OpenTelemetry-Projekts: eine Korrektur im loadbalancing-Exporter, die beim Wechsel von Endpoints Spans nicht mehr verwirft, sowie zusätzliche Metriken im tail_sampling-Prozessor, die zeigen, welche Policy eine Entscheidung getroffen hat. Letztere sind die Grundlage für das Sampling-Dashboard, das Obseria automatisch für jede Collector-Pipeline anlegt.
Wenn Sie Ihre eigene Collector-Konfiguration prüfen möchten: otelbin visualisiert Pipelines und warnt vor den hier beschriebenen Fallstricken, etwa einem spanmetrics-Connector hinter dem Sampler.