OpenTelemetry Collector in Produktion: Die Konfiguration, über die niemand schreibt
Der Betrieb des OpenTelemetry Collectors in Produktion ist der Punkt, an dem die meisten Observability-Projekte still ins Stocken geraten. Die Dokumentation ist großzügig darin, was der Collector ist, das Pipeline-Modell, Receiver, Processor und Exporter, und fast stumm bei der Konfiguration, die ihn stehen lässt, wenn echter Traffic ankommt. Teams deployen einen Collector aus einem Quickstart-YAML, richten ihre SDKs darauf und verbringen dann die nächsten drei Wochen damit, Out-of-Memory-Kills, verlorene Spans und ein Backend zu debuggen, das umfällt, weil der Collector nie gelernt hat, langsamer zu machen. Dieser Beitrag ist die Produktions-Konfigurationsreferenz, die wir bei Wolf-Tech für einen Collector vor Prometheus, Loki und Tempo ausliefern. Er behandelt genau die Blöcke, die zählen, die Fehlermeldungen, die du wirklich sehen wirst, und die Sizing-Hinweise, die niemand in ein README schreibt.
Die Kurzfassung: Ein produktiver OpenTelemetry Collector braucht vier korrekt konfigurierte Processor, bevor alles andere eine Rolle spielt. Der Memory Limiter verhindert, dass er gekillt wird. Der Batch-Processor verhindert, dass er deine Backends bombardiert. Die Retry- und Queue-Einstellungen der Exporter verhindern Datenverlust, wenn ein Backend kurz wegkippt. Tail Sampling verhindert, dass deine Storage-Rechnung linear deinem Traffic folgt. Bekommst du diese vier richtig hin, wird der Collector langweilig, und genau das willst du von Telemetrie-Infrastruktur.
Beginne mit dem Memory Limiter, nicht mit den Receivern
Der mit Abstand häufigste Produktionsfehler ist ein Collector, der in seinem Container OOM-gekillt wird. Die Standardkonfiguration hat keine Speicherobergrenze, also puffert der Collector bei einem Traffic-Spike immer mehr Daten, bis der Kernel ihn beendet, du die Telemetrie im Flug verlierst und der Neustart-Sturm den Vorfall, den du beobachten wolltest, unsichtbar macht. Die Lösung ist der Processor memory_limiter, und er muss der erste Processor in jeder Pipeline sein.
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 25
check_interval steuert, wie oft der Collector seinen eigenen Heap misst. Eine Sekunde ist der richtige Default; bei längeren Intervallen kann ein Spike dem Limiter davonlaufen. limit_percentage ist die weiche Obergrenze als Prozentsatz des Speicherlimits des Containers, und spike_limit_percentage ist der Puffer, den der Limiter für plötzliche Ausschläge in Reserve hält. Überschreitet die Nutzung das weiche Limit, beginnt der Collector, neue Daten abzulehnen, und liefert Fehler an die SDKs zurück, die dann ihren eigenen Backpressure anwenden. Genau dieses Verhalten willst du: Last bewusst abwerfen statt zu sterben.
Zwei Regeln machen das wirklich funktionsfähig. Erstens: Setze ein explizites Speicherlimit auf den Container (Kubernetes resources.limits.memory oder das Äquivalent deines Orchestrators), damit die Prozentwerte etwas haben, an dem sie messen können. Zweitens: Setze memory_limiter an die erste Stelle der processors-Liste jeder Pipeline. Ein Memory Limiter, der nach deinem Batch-Processor läuft, schützt das Falsche.
Der Batch-Processor ist nicht optional
Ohne Batching leitet der Collector jeden Span und jeden Metrikpunkt einzeln ans Backend weiter, sobald er ankommt. Das bedeutet tausende winzige Writes pro Sekunde an Tempo oder deinen Metrik-Store, und so machst du aus einem gesunden Backend den Flaschenhals. Der Processor batch gruppiert Telemetrie vor dem Export in sinnvoll große Payloads.
processors:
batch:
send_batch_size: 8192
send_batch_max_size: 16384
timeout: 5s
send_batch_size ist die Zielanzahl an Items, bevor ein Batch gesendet wird, timeout erzwingt einen Flush, auch wenn der Batch nicht voll ist, damit Phasen mit wenig Traffic nicht auf Daten sitzen, und send_batch_max_size deckelt den Batch, damit ein Burst nicht einen einzigen überdimensionierten Request erzeugt, den das Backend ablehnt. Die Reihenfolge in der Pipeline ist bewusst gewählt: erst memory_limiter, dann batch, dann alles andere. Der Limiter soll Daten ablehnen, bevor du Speicher für das Zusammenstellen von Batches ausgegeben hast.
Siehst du Backend-Ablehnungen wegen der Request-Größe, ist send_batch_max_size der Regler. Siehst du hohen Speicherverbrauch, der mit der Export-Latenz korreliert, kommt das Backend nicht hinterher, und du musst auf die Queue-Einstellungen unten schauen statt die Batches größer zu machen.
Retry und Queue: Wo Datenverlust wirklich passiert
Backends fallen aus. Tempo startet neu, Loki wird rate-limited, das Netzwerk hat Schluckauf. Was in diesen Sekunden mit deiner Telemetrie passiert, entscheidet, ob ein Vorfall beobachtbar ist. Die Einstellungen retry_on_failure und sending_queue des Exporters sind der Unterschied zwischen kurzem Puffern und einem stillen Loch in deinen Traces.
exporters:
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
sending_queue:
enabled: true
num_consumers: 10
queue_size: 5000
retry_on_failure nutzt exponentielles Backoff zwischen initial_interval und max_interval und gibt nach max_elapsed_time auf. Die sending_queue ist der In-Memory-Puffer, der Daten hält, während Retries laufen; queue_size bestimmt, wie viele Batches sie hält, und num_consumers, wie viele Export-Worker sie parallel leeren. Ist ein Backend down, füllt sich die Queue. Läuft sie ganz voll, verwirft der Collector Daten und loggt das, was dein Signal ist, dass entweder der Ausfall länger dauert, als deine Queue überbrücken kann, oder die Queue für deinen Durchsatz zu klein ist. Für Telemetrie, die du während eines Backend-Neustarts nicht verlieren darfst, lohnt sich die persistente, plattenbasierte Queue, auch wenn sie etwas Durchsatz gegen Haltbarkeit tauscht.
Der Fehlermodus, auf den du achten musst, ist eine Queue, die im Normalbetrieb still voll ist. Das bedeutet, dein Backend ist dauerhaft langsamer als deine Ingest-Rate, und kein Retry-Tuning rettet dich. Das ist ein Kapazitätsproblem, und es ist genau die Art von Befund, die ein Code-Quality- und Performance-Review zutage fördert, bevor daraus ein Alarm um 2 Uhr nachts wird.
Resource Detection: Einmal anreichern, nicht in jedem SDK
Jeder Span und jede Metrik sollte Host-, Container- und Cloud-Metadaten tragen, damit du nach Node, Region oder Deployment filtern kannst. Das in jedem Anwendungs-SDK zu tun bedeutet duplizierte Konfiguration und Drift. Der Processor resourcedetection ergänzt es zentral im Collector.
processors:
resourcedetection:
detectors: [env, system, docker]
timeout: 5s
override: false
Die detectors-Liste läuft der Reihe nach und mergt Attribute: env liest OTEL_RESOURCE_ATTRIBUTES, system ergänzt Hostname und OS, und Cloud-Detektoren (ec2, gcp, azure) fügen Instanz- und Region-Metadaten hinzu, wenn du dort läufst. override: false bedeutet, dass der Collector Attribute nicht überschreibt, die das SDK bereits gesetzt hat, sodass ein Service, der bewusst seine eigene Identität meldet, sie behält. Das ist der Processor, mit dem du "auf welchem Node war dieser langsame Trace" beantworten kannst, ohne diese Frage in jeden Service zu instrumentieren.
Tail Sampling: Die Konfiguration, die deine Rechnung steuert
Head Sampling, also die Entscheidung am Anfang eines Traces, ihn zu behalten oder zu verwerfen, ist billig, aber dumm: Es wirft Traces weg, bevor es weiß, ob sie einen Fehler oder einen langsamen Request enthielten. Tail Sampling wartet, bis ein Trace vollständig ist, und entscheidet dann, wodurch du alles Interessante behalten und das Rauschen des gesunden Pfads verwerfen kannst. Das ist der Processor, der verhindert, dass dein Trace-Storage linear mit dem Traffic wächst.
processors:
tail_sampling:
decision_wait: 10s
num_traces: 100000
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 500 }
- name: healthy-sample
type: probabilistic
probabilistic: { sampling_percentage: 5 }
decision_wait ist die Zeit, die der Collector die Spans eines Traces hält, bevor er entscheidet; sie muss länger sein als dein langsamster realistischer Trace, damit du nicht über einen unvollständigen Trace entscheidest. num_traces ist die Anzahl der In-Flight-Traces im Speicher; dimensioniere sie als decision_wait mal deine Traces pro Sekunde, mit Puffer. Die Policies werden als ODER ausgewertet: Diese Konfiguration behält jeden fehlerhaften Trace, jeden Trace langsamer als 500 Millisekunden und eine 5-Prozent-Stichprobe von allem anderen. Diese Kombination gibt dir volle Sicht auf Probleme und reduziert den Storage auf dem gesunden Pfad um eine Größenordnung.
Der Haken, über den Teams stolpern: Tail Sampling erfordert, dass alle Spans eines Traces dieselbe Collector-Instanz erreichen. Betreibst du mehrere Collectors hinter einem naiven Load Balancer, verteilen sich die Spans eines Traces über Instanzen und der Sampler sieht unvollständige Traces. Du brauchst einen Load-Balancing-Exporter in einem zweistufigen Setup: eine erste Stufe, die nach Trace-ID routet, zu einer zweiten Stufe, die das Sampling macht. Diesen Schritt zu überspringen ist der häufigste Grund, warum Tail Sampling in Produktion "nicht funktioniert".
Eine vollständige Produktions-Pipeline
So fügen sich die Teile zu einem funktionierenden service-Block zusammen. Die Reihenfolge innerhalb jeder Pipeline ist nicht kosmetisch; sie ist die Verarbeitungssequenz.
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resourcedetection, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, resourcedetection, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, resourcedetection, batch]
exporters: [loki]
telemetry:
metrics:
level: detailed
address: 0.0.0.0:8888
Beachte, dass memory_limiter jede Pipeline anführt und batch als Letztes vor dem Export kommt, mit tail_sampling vor batch in der Traces-Pipeline, damit das Batching auf bereits gesampelten Daten arbeitet. Der telemetry-Block stellt die eigenen Metriken des Collectors auf Port 8888 bereit, die du scrapen solltest. Ein Collector, der sich selbst nicht beobachten kann, ist ein blinder Fleck mitten in deinem Observability-Stack.
Die Fehlermeldungen lesen
Ein paar Meldungen tauchen immer wieder auf, und ihre wahre Bedeutung ist aus dem Text nicht offensichtlich.
"data refused due to high memory usage" bedeutet, dass der Memory Limiter seinen Job macht und Last abwirft. Gelegentliches Auftreten während Spikes ist in Ordnung; ein Dauerstrom bedeutet, dass der Container zu klein ist oder die Batch- und Queue-Einstellungen zu viel halten.
"sending queue is full" von einem Exporter bedeutet, dass das Backend mit dem Ingest nicht Schritt hält. Eine größere queue_size verschafft während eines kurzen Ausfalls Zeit, behebt aber kein dauerhaftes Missverhältnis, was eine Kapazitätsentscheidung ist.
"context deadline exceeded" beim Export bedeutet meist, dass die Backend-Latenz den Timeout des Exporters überschritten hat. Prüfe das Backend, bevor du den Timeout anfasst, denn ein längerer Timeout bei einem strauchelnden Backend staut nur die Queue.
Sizing-Hinweise für echten Traffic
Exakte Zahlen hängen von Span-Größe und Attribut-Kardinalität ab, aber ein brauchbarer Startpunkt: Für rund 10.000 Spans pro Sekunde stattest du den Collector mit etwa 1 CPU und 512 MB Speicher aus, send_batch_size um 8192 und eine sending_queue von einigen tausend. Bei 50.000 Spans pro Sekunde plane 2 bis 4 CPUs und 1 bis 2 GB ein und ziehe das Tail Sampling in eine eigene, dedizierte Stufe, damit der Sampling-Speicher nicht mit dem Ingest konkurriert. Darüber skalierst du horizontal mit der oben beschriebenen Trace-ID-Load-Balancing-Stufe statt einen einzelnen Collector größer zu machen. Lasttests immer mit produktionsähnlichen Daten, denn ein Collector, der mit uniformen synthetischen Spans entspannt ist, kann sich unter hochkardinalem echtem Traffic ganz anders verhalten.
Der Collector belohnt es, wie der Produktionsservice behandelt zu werden, der er ist: Gib ihm ein Speicherlimit, beobachte seine eigenen Metriken und stimme Batch- und Queue-Einstellungen gegen beobachtetes Verhalten ab statt gegen Vermutungen.
Wo das einzuordnen ist
Die OpenTelemetry-Collector-Konfiguration produktionsreif hinzubekommen ist ein kleiner Teil einer größeren Observability-Praxis und taucht meist neben Entscheidungen über Instrumentierung, Backend-Wahl und Kosten auf. Wenn dein Team Observability für ein wachsendes SaaS aufbaut oder eine Collector-Konfiguration erbt, die immer wieder umfällt, ist das die Art von Arbeit, die wir in Engagements zur individuellen Softwareentwicklung und Tech-Stack-Strategie leisten, und sie passt natürlich zu einem breiteren Code- und Performance-Review, wenn die Pipeline ein Symptom tieferer Kapazitätsprobleme ist.
Wenn du ein zweites Paar Augen auf deiner Collector-Konfiguration oder deinem Observability-Stack möchtest, melde dich unter hello@wolf-tech.io oder schau dir an, was wir tun, auf wolf-tech.io. Wir helfen dir lieber, Telemetrie langweilig zu machen, als zuzusehen, wie sie dich um 2 Uhr nachts anpiept.

