End-To-End-Tracing mit OpenTelemetry
August 31, 2022
Ob Sie Microservices implementieren oder nicht (und wahrscheinlich sollten Sie es nicht), besteht Ihr System höchstwahrscheinlich aus mehreren Komponenten. Das einfachste System besteht wahrscheinlich aus einem Reverse-Proxy, einer Anwendung und einer Datenbank. In diesem Fall ist Monitoring nicht nur eine gute Idee; es ist eine Notwendigkeit. Je höher die Anzahl der Komponenten, durch die eine Anfrage fließen kann, desto stärker ist die Anforderung.
Allerdings ist Monitoring nur der Anfang der Reise. Wenn Anfragen en masse zu scheitern beginnen, benötigen Sie eine aggregierte Ansicht über alle Komponenten. Dies wird als Tracing bezeichnet und ist eine der Säulen der Observability; die anderen beiden sind Metriken und Logs.
In diesem Beitrag werde ich mich ausschließlich auf Traces konzentrieren und beschreiben, wie Sie Ihre Reise in die Observability beginnen können.
Die W3C Trace Context-Spezifikation
Eine Tracing-Lösung sollte ein Standardformat bieten, das über heterogene Technologie-Stacks hinweg funktioniert. Ein solches Format muss einer Spezifikation entsprechen, entweder einer formellen oder einer de facto-Spezifikation.
Man muss verstehen, dass eine Spezifikation selten aus dem Nichts entsteht. In der Regel gibt es bereits einige unterschiedliche Implementierungen auf dem Markt. Meistens führt eine neue Spezifikation zu einer zusätzlichen Implementierung, wie der berühmte XKCD-Comic beschreibt:
Manchmal geschieht jedoch ein Wunder: Der Markt hält sich an die neue Spezifikation. Hier ist Trace Context eine W3C-Spezifikation, und es scheint, dass sie den Trick gemacht hat:
Diese Spezifikation definiert standardisierte HTTP-Header und ein Wertformat, um Kontextinformationen zu propagieren, die verteilte Tracing-Szenarien ermöglichen. Die Spezifikation standardisiert, wie Kontextinformationen zwischen Diensten gesendet und modifiziert werden. Kontextinformationen identifizieren eindeutig einzelne Anfragen in einem verteilten System und definieren auch ein Mittel, um anbieter-spezifische Kontextinformationen hinzuzufügen und zu propagieren. -- https://www.w3.org/TR/trace-context/
Zwei kritische Konzepte ergeben sich aus dem Dokument:
- Ein Trace verfolgt den Pfad einer Anfrage, die mehrere Komponenten umfasst
- Ein Span ist an eine einzelne Komponente gebunden und durch eine Eltern-Kind-Beziehung mit einem anderen Span verknüpft

Zum Zeitpunkt dieses Schreibens ist die Spezifikation eine W3C-Empfehlung, was das finale Stadium ist.
Trace Context hat bereits viele Implementierungen. Eine davon ist OpenTelemetry.
OpenTelemetry als der Goldstandard
Je näher Sie dem operativen Teil der IT sind, desto höher ist die Wahrscheinlichkeit, dass Sie von OpenTelemetry gehört haben:
OpenTelemetry ist eine Sammlung von Tools, APIs und SDKs. Verwenden Sie es, um Telemetriedaten (Metriken, Logs und Traces) zu instrumentieren, zu generieren, zu sammeln und zu exportieren, um die Leistung und das Verhalten Ihrer Software zu analysieren. OpenTelemetry ist in mehreren Sprachen allgemein verfügbar und zur Verwendung geeignet. -- https://opentelemetry.io/
OpenTelemetry ist ein Projekt, das von der CNCF verwaltet wird. Vor OpenTelemetry gab es zwei Projekte:
- OpenTracing, das sich, wie der Name schon sagt, auf Traces konzentrierte
- OpenCensus, dessen Ziel es war, Metriken und Traces zu verwalten
Beide Projekte fusionierten und fügten Logs hinzu. OpenTelemetry bietet nun eine Reihe von "Schichten", die sich auf Observability konzentrieren:
- Instrumentierungs-APIs in verschiedenen Sprachen
- Kanonische Implementierungen, wiederum in verschiedenen Sprachen
- Infrastrukturkomponenten wie Collector
- Interoperabilitätsformate, wie z.B. das W3C's Trace Context
Beachten Sie, dass OpenTelemetry zwar eine Trace Context-Implementierung ist, aber mehr bietet. Trace Context beschränkt sich auf HTTP, während OpenTelemetry es ermöglicht, dass Spans nicht-web-basierte Komponenten wie Kafka durchqueren. Dies liegt jedoch außerhalb des Rahmens dieses Blogbeitrags.
Der Anwendungsfall
Mein Lieblingsanwendungsfall ist ein E-Commerce-Shop, also lassen wir ihn unverändert. In diesem Fall ist der Shop um Microservices herum entworfen, die jeweils über eine REST-API zugänglich sind und hinter einem API-Gateway geschützt sind. Um die Architektur für den Blogbeitrag zu vereinfachen, werde ich nur zwei Microservices verwenden: catalog
verwaltet Produkte, und pricing
kümmert sich um den Preis der Produkte.
Wenn ein Benutzer auf der App ankommt, ruft die Startseite alle Produkte ab, holt ihre jeweiligen Preise und zeigt sie an.
Um die Sache interessanter zu machen, ist catalog
eine Spring Boot-Anwendung, die in Kotlin programmiert ist, während pricing
eine Python Flask-Anwendung ist.
Tracing sollte es uns ermöglichen, den Pfad einer Anfrage über das Gateway, beide Microservices und, wenn möglich, die Datenbanken zu verfolgen.
Traces am Gateway
Der Einstiegspunkt ist der spannendste Teil des Tracings, da er die Trace-ID generieren sollte: In diesem Fall ist der Einstiegspunkt das Gateway. Ich werde Apache APISIX verwenden, um das Demo zu implementieren:
Apache APISIX bietet umfangreiche Funktionen zur Verkehrssteuerung wie Lastenausgleich, dynamisches Upstream, Canary Release, Circuit Breaking, Authentifizierung, Observability usw. -- https://apisix.apache.org/
Apache APISIX basiert auf einer Plugin-Architektur und bietet ein OpenTelemetry-Plugin:
Das
opentelemetry
-Plugin kann verwendet werden, um Tracing-Daten gemäß der OpenTelemetry-Spezifikation zu melden. Das Plugin unterstützt nur binärkodiertes OLTP über HTTP. -- https://apisix.apache.org/docs/apisix/plugins/opentelemetry/
Lassen Sie uns das opentelemetry
-Plugin konfigurieren:
apisix:
enable_admin: false #1
config_center: yaml #1
plugins:
- opentelemetry #2
plugin_attr:
opentelemetry:
resource:
service.name: APISIX #3
collector:
address: jaeger:4318 #4
- Führen Sie Apache APISIX im Standalone-Modus aus, um das Demo einfacher zu verfolgen. Es ist ohnehin eine gute Praxis in der Produktion
- Konfigurieren Sie
opentelemetry
als globales Plugin - Setzen Sie den Namen des Dienstes. Es ist der Name, der in der Trace-Anzeigekomponente erscheinen wird
- Senden Sie die Traces an den
jaeger
-Dienst. Der folgende Abschnitt wird ihn beschreiben.
Wir möchten jede Route verfolgen, daher sollten wir das Plugin als globales Plugin einrichten, anstatt es zu jeder Route hinzuzufügen:
global_rules:
- id: 1
plugins:
opentelemetry:
sampler:
name: always_on #1
- Tracing hat Auswirkungen auf die Leistung. Je mehr wir verfolgen, desto stärker ist die Auswirkung. Daher sollten wir sorgfältig abwägen, ob der Leistungsverlust die Vorteile der Observability rechtfertigt. Für das Demo möchten wir jedoch jede Anfrage verfolgen.
Sammeln, Speichern und Anzeigen von Traces
Während Trace Context eine W3C-Spezifikation und OpenTelemetry ein de facto-Standard ist, gibt es auf dem Markt viele Lösungen zum Sammeln, Speichern und Anzeigen von Traces. Jede Lösung kann alle drei Fähigkeiten bieten oder nur einen Teil davon. Zum Beispiel kümmert sich der Elastic-Stack um Speicherung und Anzeige, aber Sie müssen sich für die Sammlung auf etwas anderes verlassen. Andererseits bieten Jaeger und Zipkin eine komplette Suite, um alle drei Fähigkeiten zu erfüllen.
Jaeger und Zipkin sind älter als OpenTelemetry, daher hat jeder sein eigenes Trace-Transportformat. Sie bieten jedoch Integration mit dem OpenTelemetry-Format.
Im Rahmen dieses Blogbeitrags ist die genaue Lösung nicht relevant, da wir nur die Fähigkeiten benötigen. Ich habe Jaeger gewählt, weil es ein All-in-One-Docker-Image bietet: Jede Fähigkeit hat ihre eigene Komponente, aber sie sind alle in demselben Image eingebettet, was die Konfiguration viel einfacher macht.
Die relevanten Ports des Images sind die folgenden:
Port | Protokoll | Komponente | Funktion |
---|---|---|---|
16686 | HTTP | query | dient dem Frontend |
4317 | HTTP | collector | akzeptiert OpenTelemetry Protocol (OTLP) über gRPC, wenn aktiviert |
4318 | HTTP | collector | akzeptiert OpenTelemetry Protocol (OTLP) über HTTP, wenn aktiviert |
Der Docker Compose-Teil sieht so aus:
services:
jaeger:
image: jaegertracing/all-in-one:1.37 #1
environment:
- COLLECTOR_OTLP_ENABLED=true #2
ports:
- "16686:16686" #3
- Verwenden Sie das
all-in-one
-Image - Sehr wichtig: Aktivieren Sie den Collector im OpenTelemetry-Format
- Exponieren Sie den UI-Port
Nachdem wir die Infrastruktur eingerichtet haben, können wir uns darauf konzentrieren, Traces in unseren Anwendungen zu aktivieren.
Traces in Flask-Apps
Der pricing
-Dienst ist eine einfache Flask-Anwendung. Er bietet einen einzigen Endpunkt, um den Preis eines einzelnen Produkts aus der Datenbank abzurufen.
@app.route('/price/<product_str>') #1-2
def price(product_str: str) -> Dict[str, object]:
product_id = int(product_str)
price: Price = Price.query.get(product_id) #3
if price is None:
return jsonify({'error': 'Product not found'}), 404
else:
low: float = price.value - price.jitter #4
high: float = price.value + price.jitter #4
return {
'product_id': product_id,
'price': round(uniform(low, high), 2) #4
}
- Endpunkt
- Die Route erfordert die ID des Produkts
- Daten aus der Datenbank mit SQLAlchemy abrufen
- Echte Preis-Engines geben nie denselben Preis über die Zeit zurück; lassen Sie uns den Preis ein wenig randomisieren, um Spaß zu haben
Warnung: Das Abrufen eines einzelnen Preises pro Aufruf ist höchst ineffizient. Es erfordert so viele Aufrufe wie Produkte, aber es macht den Trace interessanter. Im wirklichen Leben sollte die Route in der Lage sein, mehrere Produkt-IDs zu akzeptieren und alle zugehörigen Preise in einer Anfrage-Antwort abzurufen.
Jetzt ist es an der Zeit, die Anwendung zu instrumentieren. Es gibt zwei Optionen: automatische Instrumentierung und manuelle Instrumentierung. Automatisch ist wenig Aufwand und ein schneller Gewinn; manuell erfordert gezielte Entwicklungszeit. Ich würde empfehlen, mit der automatischen Instrumentierung zu beginnen und nur bei Bedarf manuelle hinzuzufügen.
Wir müssen ein paar Python-Pakete hinzufügen:
opentelemetry-distro[otlp]==0.33b0
opentelemetry-instrumentation
opentelemetry-instrumentation-flask
Wir müssen ein paar Parameter konfigurieren:
pricing:
build: ./pricing
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1
OTEL_RESOURCE_ATTRIBUTES: service.name=pricing #2
OTEL_METRICS_EXPORTER: none #3
OTEL_LOGS_EXPORTER: none #3
- Senden Sie die Traces an Jaeger
- Setzen Sie den Namen des Dienstes. Es ist der Name, der in der Trace-Anzeigekomponente erscheinen wird
- Wir sind weder an Logs noch an Metriken interessiert
Jetzt verwenden wir nicht den Standardbefehl flask run
, sondern umschließen ihn:
opentelemetry-instrument flask run
Allein damit sammeln wir bereits Spans von Methodenaufrufen und Flask-Routen.
Wir können bei Bedarf manuell zusätzliche Spans hinzufügen, z.B.:
from opentelemetry import trace
@app.route('/price/<product_str>')
def price(product_str: str) -> Dict[str, object]:
product_id = int(product_str)
with tracer.start_as_current_span("SELECT * FROM PRICE WHERE ID=:id", attributes={":id": product_id}) as span: #1
price: Price = Price.query.get(product_id)
# ...
- Fügen Sie einen zusätzlichen Span mit dem konfigurierten Label und Attribut hinzu
Traces in Spring Boot-Apps
Der catalog
-Dienst ist eine reaktive Spring Boot-Anwendung, die in Kotlin entwickelt wurde. Er bietet zwei Endpunkte:
- Einen, um ein einzelnes Produkt abzurufen
- Einen, um alle Produkte abzurufen
Beide suchen zuerst in der Produktdatenbank und fragen dann den oben genannten pricing
-Dienst nach dem Preis ab.
Wie bei Python können wir automatische und manuelle Instrumentierung nutzen. Beginnen wir mit der einfachen Lösung, der automatischen Instrumentierung. Auf der JVM erreichen wir dies über einen Agenten:
java -javaagent:opentelemetry-javaagent.jar -jar catalog.jar
Wie bei Python erzeugt dies Spans für jeden Methodenaufruf und HTTP-Einstiegspunkt. Es instrumentiert auch JDBC-Aufrufe, aber wir haben einen reaktiven Stack und verwenden daher R2DBC. Für den Rekord gibt es ein GitHub-Issue, um Unterstützung hinzuzufügen.
Wir müssen das Standardverhalten konfigurieren:
catalog:
build: ./catalog
environment:
APP_PRICING_ENDPOINT: http://pricing:5000/price
OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317 #1
OTEL_RESOURCE_ATTRIBUTES: service.name=orders #2
OTEL_METRICS_EXPORTER: none #3
OTEL_LOGS_EXPORTER: none #3
- Senden Sie die Traces an Jaeger
- Setzen Sie den Namen des Dienstes. Es ist der Name, der in der Trace-Anzeigekomponente erscheinen wird
- Wir sind weder an Logs noch an Metriken interessiert
Wie bei Python können wir das Spiel aufwerten, indem wir manuelle Instrumentierung hinzufügen. Es gibt zwei Optionen, programmatisch und anmerkungsbasiert. Ersteres ist etwas aufwendig, es sei denn, wir führen Spring Cloud Sleuth ein. Fügen wir Anmerkungen hinzu.
Wir benötigen eine zusätzliche Abhängigkeit:
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.17.0-alpha</version>
</dependency>
Seien Sie vorsichtig, das Artefakt wurde kürzlich von io.opentelemetry:opentelemetry-extension-annotations
verschoben.
Wir können jetzt unseren Code mit Anmerkungen versehen:
@WithSpan("ProductHandler.fetch") //1
suspend fun fetch(@SpanAttribute("id") id: Long): Result<Product> { //2
val product = repository.findById(id)
return if (product == null) Result.failure(IllegalArgumentException("Product $id not found"))
else Result.success(product)
}
- Fügen Sie einen zusätzlichen Span mit dem konfigurierten Label hinzu
- Verwenden Sie den Parameter als Attribut, mit dem Schlüssel
id
und dem Wert des Parameterwerts zur Laufzeit
Das Ergebnis
Wir können jetzt mit unserem einfachen Demo spielen, um das Ergebnis zu sehen:
curl localhost:9080/products
curl localhost:9080/products/1
Die Antworten sind nicht interessant, aber schauen wir uns die Jaeger-UI an. Wir finden beide Traces, einen pro Aufruf:
Wir können in die Spans eines einzelnen Traces eintauchen:
Beachten Sie, dass wir den Sequenzfluss ohne das obige UML-Diagramm ableiten können. Noch besser, die Sequenz zeigt die internen Aufrufe einer Komponente an.
Jeder Span enthält Attribute, die die automatische Instrumentierung hinzugefügt hat und die wir manuell hinzugefügt haben:
Fazit
In diesem Beitrag habe ich Tracing gezeigt, indem ich eine Anfrage über ein API-Gateway, zwei Apps basierend auf verschiedenen Tech-Stacks und ihre jeweiligen Datenbanken verfolgt habe. Ich habe nur die Oberfläche des Tracings gestreift: In der realen Welt würde Tracing wahrscheinlich Komponenten umfassen, die nicht mit HTTP verwandt sind, wie Kafka und Message Queues.
Dennoch verlassen sich die meisten Systeme in irgendeiner Weise auf HTTP. Obwohl die Einrichtung nicht trivial ist, ist sie auch nicht zu schwierig. Das Tracing von HTTP-Anfragen über Komponenten hinweg ist ein guter Start in Ihre Reise zur Observability Ihres Systems.
Der vollständige Quellcode für diesen Beitrag kann auf GitHub gefunden werden.
Um weiterzugehen: