OpenTelemetry Collector in Produktion
Pipelines, Sampling und Betrieb als Shared Service.
von PlatPrep
Der OpenTelemetry Collector ist konzeptionell einfach: Receiver nehmen Daten entgegen, Processors transformieren sie, Exporters schicken sie weiter. In der Praxis ist der Collector einer der Dienste im Observability-Stack, der am häufigsten falsch konfiguriert wird.
Ein reales Szenario: ein Team deployed den Collector als DaemonSet auf jedem Node. Konfiguration minimal — OTLP-Receiver, Exporter zu Jaeger. Läuft. Drei Monate später: Memory-Spikes, Collector-Pods crashen, Traces gehen verloren.
Das Problem: kein Batching konfiguriert, kein Memory-Limiter, kein Queuing. Der Collector hat Traffic-Spikes ungefiltert ins Backend weitergeleitet, bis er OOMed ist.
Die Pipeline, die in Produktion funktioniert
Eine produktionstaugliche Trace-Pipeline sieht im Kern so aus:
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 25
batch:
send_batch_size: 8192
timeout: 5s
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp]
Die Reihenfolge ist nicht kosmetisch. Der memory_limiter gehört an den Anfang der Processor-Kette: er prüft den Heap-Verbrauch und lehnt bei Überschreitung neue Daten ab, statt dass der Prozess crasht. Das ist der Unterschied zwischen kalkuliertem Datenverlust mit Metrik-Spur und unerklärlichen Pod-Restarts. Der batch-Processor aggregiert Spans vor dem Export — weniger, größere Requests ans Backend, deutlich besserer Throughput und weniger Backend-Last.
Dazu gehört der dritte Baustein, der gern vergessen wird: die sending_queue im Exporter. Wenn das Backend kurz nicht erreichbar ist, puffert die Queue und retried mit Backoff. Ohne Queue bedeutet ein zweiminütiger Backend-Neustart zwei Minuten verlorene Telemetrie. Für harte Anforderungen gibt es zusätzlich persistente Queues auf Disk — dann überlebt der Puffer auch einen Collector-Restart.
Ein vierter Processor, der sich auf Plattform-Ebene lohnt: resource bzw. k8sattributes reichert alle Daten zentral mit Attributen wie k8s.cluster.name, k8s.namespace.name oder deployment.environment an. Das erspart jedem App-Team die eigene Konfiguration — und sorgt dafür, dass die Attribute überall gleich heißen.
Bei der Auswahl des Binaries lohnt ein kurzer Blick: es gibt das schlanke Core-Release und die Contrib-Distribution mit der vollen Komponentensammlung — viele in Kubernetes übliche Bausteine wie k8sattributes oder der tail_sampling-Processor stecken in Contrib. Wer es sauber will, baut sich mit dem OpenTelemetry Collector Builder ein eigenes Binary, das nur die tatsächlich genutzten Komponenten enthält — kleinere Angriffsfläche, kleineres Image.
Agent und Gateway: das Zwei-Schichten-Pattern
DaemonSet oder Deployment? Die Antwort für die meisten Plattformen: beides, mit unterschiedlichen Rollen.
Der Agent läuft als DaemonSet auf jedem Node — bewusst schlank konfiguriert. Er nimmt OTLP von lokalen Pods entgegen, sammelt optional Host-Metriken und Kubelet-Stats, und leitet alles an das Gateway weiter. Apps senden an den Node-lokalen Agent, was Netzwerk-Hops spart und Backpressure früh abfängt.
Das Gateway läuft als Deployment mit HPA, typischerweise 2–5 Replicas. Hier sitzt die gesamte Verarbeitungslogik: Attribut-Anreicherung, Filterung, Sampling, Routing zu den Backends (Tempo, Prometheus, Loki, oder ein Vendor). Wenn sich ein Backend-Endpoint ändert oder ein neuer Vendor dazukommt, änderst du eine zentrale Konfiguration — nicht hunderte App-Deployments.
Tail-based Sampling: mächtig, aber mit Haken
Head-based Sampling (“behalte jeden zehnten Trace”) ist einfach, wirft aber blind weg — auch die Fehler-Traces, die du am dringendsten brauchst. Tail-based Sampling entscheidet erst, nachdem alle Spans eines Traces eingetroffen sind: Traces mit Fehlern oder hoher Latenz zu 100 % behalten, vom Rest nur eine Stichprobe. Die Signal-zu-Rauschen-Ratio im Tracing-Backend verbessert sich drastisch, und die Storage-Kosten sinken oft um 80–90 %.
Der Haken: der tail_sampling-Processor muss alle Spans eines Traces im selben Collector-Pod sehen. Bei mehreren Gateway-Replicas brauchst du davor eine Schicht mit dem loadbalancing-Exporter, der nach Trace-ID routet. Außerdem hält der Processor alle offenen Traces für die Entscheidungsdauer (typisch 10–30 Sekunden) im Speicher — der Memory-Bedarf des Gateways skaliert direkt mit dem Span-Durchsatz. Das ist kein Grund, es nicht zu tun. Es ist ein Grund, vorher zu rechnen.
Den Collector selbst überwachen
Der Collector exportiert eigene Metriken, und mindestens drei davon gehören in ein Dashboard mit Alerts: otelcol_exporter_send_failed_spans (Backend-Probleme), otelcol_processor_refused_spans (Memory-Limiter greift, Apps werden gedrosselt) und otelcol_exporter_queue_size relativ zur Kapazität (Queue läuft voll, Datenverlust droht). Ein Collector, der still Daten verwirft, ist schlimmer als gar keiner — du glaubst, du siehst alles, und siehst nur einen Ausschnitt.
Collector-Konfiguration gehört unter Versionskontrolle, deployed als ConfigMap oder über den OpenTelemetry Operator, getestet in Staging. Der Collector ist kritische Observability-Infrastruktur und verdient dieselbe Sorgfalt wie Prometheus oder Loki. Wie OTel und Prometheus im Gesamtbild zusammenspielen, liest du in OpenTelemetry und Prometheus; die Grundlagen vertieft der OpenTelemetry-Lernpfad.