Grafana Tempo betreiben, ohne dass Spans still verschwinden

Warum Grafana Tempo Spans ohne Fehlermeldung verwirft, welche Limits dahinterstehen und wie Retention, Metrics-Generator und ein Alarm den Betrieb absichern.
Inhalt

Die Instrumentierung steht, der Collector liefert, und trotzdem fehlt der Trace zur Störung von letzter Nacht. Im Log der Anwendung steht kein Fehler, im Collector auch nicht, und Grafana Tempo meldet nichts. Genau so sieht der häufigste Betriebsfehler nach einem Tracing-Rollout aus: Tempo verwirft Spans an seinen Limits, und zwar leise. Dieser Artikel zeigt, an welchen Stellen das passiert, welche Konfiguration dahintersteht und mit welcher Abfrage Sie es sehen, bevor jemand einen Trace vermisst.

Ausgangslage: Rollout fertig, Traces trotzdem lückenhaft

Das Muster wiederholt sich in den meisten Tracing-Rollouts, die wir übernehmen. OpenTelemetry-Instrumentierung auf einigen Dutzend Services, eine Collector-Topologie mit Tail-Sampling, Grafana Tempo im Kubernetes-Cluster mit S3-kompatiblem Objektspeicher. Nach dem Rollout ist das Tracing für ein paar Wochen ein Erfolg, die Traces sind lesbar, die Teams nutzen sie.

Dann kommen die ersten Meldungen aus dem Betrieb, und sie klingen immer ähnlich. Ein nächtlicher Batch-Job löst eine Störung aus, aber sein Trace ist nicht auffindbar. Ein Team sieht in Grafana nur einen Teil der Spans einer langen Anfrage. Und die Service-Graph-Metriken aus dem Metrics-Generator, dem Baustein von Tempo, der aus Spans Metriken berechnet, reißen an manchen Tagen ab.

Die drei Symptome haben dieselbe Ursache: Grenzen, die Tempo mit Standardwerten mitbringt. Sie passen für einen Testcluster, und in Produktion greifen sie ohne Ankündigung, weil kein Client davon erfährt.

Wie Grafana Tempo Spans verwirft, ohne es zu sagen

Tempo ist auf Durchsatz gebaut. Der Distributor nimmt Spans per OTLP an, verteilt sie an die Ingester, die daraus Blöcke bilden, und der Compactor legt diese Blöcke im Objektspeicher ab. An mehreren Stellen dieser Kette gibt es Grenzen je Mandant, und wer sie überschreitet, bekommt keinen Fehler im Anwendungslog, sondern einen Zähler.

Drei Grenzen sind in der Praxis relevant. max_bytes_per_trace begrenzt die Größe eines einzelnen Traces, der Standardwert liegt bei fünf Megabyte. Ein Batch-Job, der über Stunden tausende Spans an dieselbe Trace-ID hängt, überschreitet das; ab diesem Punkt werden alle weiteren Spans dieses Traces verworfen. max_traces_per_user begrenzt die Zahl der gleichzeitig offenen Traces im Ingester, Standard sind zehntausend; bei hoher Parallelität ist das schnell erreicht.

Die dritte Grenze ist die Ingestion-Rate: rate_limit_bytes und burst_size_bytes deckeln die Bytes pro Sekunde je Mandant. Mandant ist dabei, was im Header X-Scope-OrgID ankommt. Ohne Mandantenfähigkeit teilen sich alle Teams einen einzigen Satz Grenzen, und ein Team mit einem lauten Batch-Job verdrängt die Spans der anderen.

Jeder verworfene Span landet in tempo_discarded_spans_total, einer Prometheus-Metrik mit dem Label reason. Es nimmt unter anderem die Werte trace_too_large, live_traces_exceeded und rate_limited an; die Tempo-Dokumentation zu abgewiesenen Spans beschreibt die Fälle. Wer diese Metrik nicht beobachtet, erfährt vom Verlust erst, wenn jemand einen Trace sucht.

Die vierte Grenze sitzt im Metrics-Generator

Der Metrics-Generator berechnet aus Spans Service-Graph- und Span-Metriken und schreibt sie per Remote Write nach Mimir oder Prometheus. Jede Kombination aus Service, Span-Name, Status und den konfigurierten Dimensionen ist eine eigene Zeitreihe. Die Kardinalität, also die Zahl unterscheidbarer Label-Kombinationen, wächst deshalb mit jedem Attribut, das als Dimension konfiguriert ist.

Landen aufgelöste URLs mit IDs oder Kundennummern als Span-Namen oder Dimension, entstehen Zeitreihen ohne Ende. Tempo schützt sich mit max_active_series je Mandant: Ist die Grenze erreicht, erzeugt der Generator für neue Kombinationen keine Metriken mehr, und der Service Graph bekommt Lücken, ohne dass ein Fehler erscheint. Die Metrik tempo_metrics_generator_registry_active_series zeigt, wie nah ein Mandant an der Grenze ist.

Die Wahl der Dimensionen entscheidet deshalb über den Nutzen des Generators. Service, Span-Name, Status und das Route-Template reichen für Service Graph und RED-Metriken aus. Alles, was je Nutzer oder je Auftrag verschieden ist, gehört in den Trace, nicht in eine Metrik. Wer eine Dimension ergänzt, schätzt vorher die Zahl ihrer Werte ab und multipliziert sie mit den bestehenden Zeitreihen, denn genau das tut der Generator.

Die Konfiguration, die in Produktion trägt

Die Lösung besteht aus drei Teilen: Limits bewusst setzen statt Standardwerte erben, Retention und Speicher festlegen und die Verluste alarmieren. Die folgende Konfiguration gilt für Grafana Tempo 2.8 im monolithischen Modus mit S3-kompatiblem Objektspeicher. Die Zahlenwerte sind Beispiele in der Größenordnung eines mittleren Clusters und müssen aus der eigenen Messung im Pilot kommen. Der Aufbau der Blöcke overrides und metrics_generator folgt der Konfigurationsreferenz von Tempo.

# Grafana Tempo 2.8, monolithisch, vereinfacht
storage:
  trace:
    backend: s3
    s3:
      bucket: tempo-traces
      endpoint: s3.eu-central-1.amazonaws.com
      region: eu-central-1
    wal:
      path: /var/tempo/wal

compactor:
  compaction:
    block_retention: 336h

metrics_generator:
  registry:
    external_labels:
      source: tempo
  storage:
    path: /var/tempo/generator/wal
    remote_write:
      - url: http://mimir-distributor.metrics.svc:8080/api/v1/push
        send_exemplars: true
  processor:
    service_graphs:
      dimensions: [deployment.environment]
    span_metrics:
      dimensions: [http.route]

overrides:
  defaults:
    metrics_generator:
      processors: [service-graphs, span-metrics]
      max_active_series: 200000
    ingestion:
      max_traces_per_user: 50000
      rate_limit_bytes: 30000000
      burst_size_bytes: 40000000
    global:
      max_bytes_per_trace: 20000000

block_retention von 336 Stunden hält Traces zwei Wochen; danach löscht der Compactor die Blöcke aus dem Objektspeicher, und die Retention, also die Aufbewahrungsdauer, wird zur reinen Speicherfrage. max_bytes_per_trace ist hier vervierfacht, weil lange Batch-Traces fachlich gewollt sind; die Alternative, den Batch-Job in Teil-Traces zu schneiden, steht im Abschnitt zu den Grenzen. Die dimensions im Metrics-Generator sind bewusst kurz gehalten, und http.route ist das Route-Template, nie die aufgelöste URL.

send_exemplars: true sorgt dafür, dass die Span-Metriken Exemplars tragen, also Messpunkte, die direkt auf einen Trace verweisen. Damit führt in Grafana der Klick auf einen Ausreißer im Latenz-Panel zum konkreten Trace, ohne dass jemand die Trace-ID suchen muss.

Ein Alarm, der den Verlust meldet, bevor jemand ihn bemerkt

Die wichtigste Zeile im Betrieb ist eine Alarmregel auf die verworfenen Spans. Im Expression Browser von Prometheus 3.5 beantwortet eine Abfrage die Frage, ob in der letzten Minute Spans verloren gingen und warum:

sum by (tenant, reason) (increase(tempo_discarded_spans_total[1m])) > 0

Die Abfrage liefert je Mandant und Grund die Zahl der verworfenen Spans pro Minute. increase über eine Minute statt rate über fünf ergibt eine lesbare Größe, Spans pro Minute, und reagiert innerhalb einer Minute. Als Alarmregel ohne Wartezeit geht die Meldung raus, sobald der erste Span fehlt, mit dem Grund im Label. trace_too_large zeigt auf die Batch-Jobs, rate_limited auf die Ingestion-Grenze, live_traces_exceeded auf zu viele gleichzeitig offene Traces.

Für den Metrics-Generator kommt eine zweite Regel dazu, die tempo_metrics_generator_registry_active_series ins Verhältnis zum konfigurierten Limit setzt. Steigt der Wert über achtzig Prozent, ist es Zeit, die Dimensionen zu prüfen, bevor der Service Graph Lücken bekommt. Beide Regeln gehören ins Dashboard neben die Tempo-Mixin-Panels für Ingester und Compactor, damit der Betrieb den Zustand von Tempo an einer Stelle sieht.

Was sich mit dieser Konfiguration ändert

Mit dieser Konfiguration verschwinden die drei Symptome vom Anfang, weil ihre Ursache konfiguriert statt geerbt ist. Lange Batch-Traces bleiben vollständig, hohe Parallelität läuft nicht mehr in max_traces_per_user, und der Service Graph bleibt geschlossen. Wichtiger ist, was dazukommt: Jeder künftige Verlust löst innerhalb einer Minute einen Alarm mit Grund aus, statt Tage später als vermisster Trace aufzufallen.

Zahlen zum Speicherbedarf nennen wir nicht, weil sie von Span-Größe, Sampling-Rate und Retention abhängen und sich zwischen Landschaften nicht übertragen lassen. Was sich messen lässt und gemessen werden sollte, ist die Null: tempo_discarded_spans_total steigt nach der Umstellung nur noch, wenn jemand die Grenzen bewusst reißt, und dann steht der Grund im Label.

Wo diese Konfiguration an Grenzen stößt

Höhere Limits kaufen Zeit, keine Lösung. Ein max_bytes_per_trace von zwanzig Megabyte macht Ingester und Querier speicherhungriger, und ein Trace mit zehntausenden Spans ist in Grafana ohnehin nicht mehr lesbar. Die sauberere Antwort auf lange Batch-Jobs ist, je Verarbeitungsschritt einen eigenen Trace zu starten und die Schritte über ein gemeinsames Attribut wie batch.run_id zu verbinden. Das kostet Instrumentierungsarbeit im Code.

Der Alarm auf tempo_discarded_spans_total meldet Verluste in Tempo, nicht davor. Spans, die der Collector am memory_limiter abweist oder die das Tail-Sampling bewusst verwirft, tauchen dort nicht auf. Dafür braucht es die eigenen Metriken des Collectors zu abgewiesenen und verworfenen Spans, und die gehören in dasselbe Dashboard.

Und der Metrics-Generator ersetzt kein Sampling-Konzept, er sieht nur, was der Collector durchlässt. Wer Fehler-Traces im Tail-Sampling vollständig behält und den Rest anteilig, bekommt Fehlerraten im Service Graph, die gegenüber der Realität verschoben sind, weil Fehler überrepräsentiert sind. Das ist bekannt und beherrschbar. Es muss aber jedem gesagt werden, der die RED-Metriken liest, also Rate, Fehler und Dauer je Service.

Das Fazit: Limits sind Konfiguration, keine Überraschung

Grafana Tempo verliert Spans nicht durch Fehler, sondern durch Grenzen, die jemand nicht gesetzt hat. Wer max_bytes_per_trace, max_traces_per_user, die Ingestion-Rate und max_active_series aus der eigenen Messung ableitet, die Retention bewusst festlegt und tempo_discarded_spans_total alarmiert, betreibt Tempo ohne stille Verluste.

Wir setzen diese vier Grenzen in jedem Tracing-Rollout, den wir übernehmen, vor dem ersten Produktionstag, und den Alarm dazu noch am selben Tag.

Beitrag teilen

LinkedIn
XING
E-Mail

Ein Thema aus diesem Beitrag betrifft Sie gerade?

Im Erstgespräch klären wir Ihren Stand und den nächsten sinnvollen Schritt. Mit Erfolgsgarantie auf die vereinbarten Ziele.

Erster Schritt: ein kurzes, kostenloses Erstgespräch – direkt mit einem Senior Consultant, keine Vertriebskette. Unverbindlich – danach entscheiden Sie.

// WEITERLESEN

Weitere Beiträge