paint-brush
Java-Backend-Beobachtbarkeit mit OpenTelemetry Traces und minimalem Codevon@apanasevich
292 Lesungen

Java-Backend-Beobachtbarkeit mit OpenTelemetry Traces und minimalem Code

von Dmitriy Apanasevich10m2024/11/15
Read on Terminal Reader
Read this story w/o Javascript

Zu lang; Lesen

So haben wir das OpenTelemetry-Framework in unser Java-Backend integriert und Tracing mit minimalem Coding erreicht.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Java-Backend-Beobachtbarkeit mit OpenTelemetry Traces und minimalem Code
Dmitriy Apanasevich HackerNoon profile picture

Hallo zusammen! Ich bin Dmitriy Apanasevich, Java-Entwickler bei MY.GAMES und arbeite am Spiel Rush Royale. Ich möchte unsere Erfahrungen mit der Integration des OpenTelemetry-Frameworks in unser Java-Backend mit euch teilen. Es gibt hier einiges zu besprechen: Wir werden die notwendigen Codeänderungen zur Implementierung sowie die neuen Komponenten besprechen, die wir installieren und konfigurieren mussten – und natürlich werden wir einige unserer Ergebnisse mit euch teilen.

Unser Ziel: Systembeobachtung erreichen

Lassen Sie uns unseren Fall etwas näher erläutern. Als Entwickler möchten wir Software erstellen, die einfach zu überwachen, auszuwerten und zu verstehen ist (und genau das ist der Zweck der Implementierung von OpenTelemetry – die Systemleistung zu maximieren. Beobachtbarkeit ).


Herkömmliche Methoden zum Sammeln von Einblicken in die Anwendungsleistung beinhalten häufig das manuelle Protokollieren von Ereignissen, Metriken und Fehlern:



Natürlich gibt es viele Frameworks, die uns die Arbeit mit Protokollen ermöglichen, und ich bin sicher, dass jeder, der diesen Artikel liest, über ein konfiguriertes System zum Sammeln, Speichern und Analysieren von Protokollen verfügt.


Da die Protokollierung für uns ebenfalls vollständig konfiguriert war, nutzten wir die von OpenTelemetry bereitgestellten Funktionen zum Arbeiten mit Protokollen nicht.


Eine weitere gängige Methode zur Überwachung des Systems ist die Nutzung von Metriken:


Wir hatten außerdem ein vollständig konfiguriertes System zum Erfassen und Visualisieren von Metriken und ignorierten daher auch hier die Möglichkeiten von OpenTelemetry im Hinblick auf die Arbeit mit Metriken.


Ein weniger verbreitetes Werkzeug zum Erhalten und Analysieren dieser Art von Systemdaten sind jedoch Spuren .


Eine Spur stellt den Weg dar, den eine Anfrage während ihrer Lebensdauer durch unser System nimmt. Sie beginnt normalerweise, wenn das System eine Anfrage empfängt, und endet mit der Antwort. Spuren bestehen aus mehreren Spannweiten , die jeweils eine bestimmte Arbeitseinheit darstellen, die vom Entwickler oder der von ihm gewählten Bibliothek bestimmt wird. Diese Bereiche bilden eine hierarchische Struktur, die dabei hilft, zu visualisieren, wie das System die Anfrage verarbeitet.


In dieser Diskussion konzentrieren wir uns auf den Tracing-Aspekt von OpenTelemetry.

Weitere Hintergrundinformationen zu OpenTelemetry

Werfen wir auch einen Blick auf das OpenTelemetry-Projekt, das durch die Zusammenführung der OpenTracing Und OpenCensus Projekte.


OpenTelemetry bietet jetzt eine umfassende Palette von Komponenten basierend auf einem Standard, der eine Reihe von APIs, SDKs und Tools für verschiedene Programmiersprachen definiert. Das Hauptziel des Projekts besteht darin, Daten zu generieren, zu sammeln, zu verwalten und zu exportieren.


Allerdings bietet OpenTelemetry kein Backend für Datenspeicherung oder Visualisierungstools.


Da wir uns nur für das Tracing interessierten, untersuchten wir die beliebtesten Open-Source-Lösungen zum Speichern und Visualisieren von Traces:

  • Jaeger
  • Zipkin
  • Grafana Tempo


Letztendlich haben wir uns aufgrund der beeindruckenden Visualisierungsmöglichkeiten, des schnellen Entwicklungstempos und der Integration in unser vorhandenes Grafana-Setup zur Metrikvisualisierung für Grafana Tempo entschieden. Ein einziges, einheitliches Tool zu haben, war ebenfalls ein erheblicher Vorteil.

OpenTelemetry-Komponenten

Lassen Sie uns auch die Komponenten von OpenTelemetry ein wenig analysieren.


Die Spezifikation:

  • API – Datentypen, Operationen, Enumerationen

  • SDK – Spezifikationsimplementierung, APIs in verschiedenen Programmiersprachen. Eine andere Sprache bedeutet einen anderen SDK-Status, von Alpha bis stabil.

  • Datenprotokoll (OTLP) und semantische Konventionen


Die Java-API, das SDK:

  • Code-Instrumentierungsbibliotheken
  • Exporter – Tools zum Exportieren generierter Traces in das Backend
  • Cross Service Propagators – ein Tool zum Übertragen des Ausführungskontexts außerhalb des Prozesses (JVM)


Eine wichtige Komponente ist der OpenTelemetry Collector , ein Proxy, der Daten empfängt, verarbeitet und weiterleitet – schauen wir uns das genauer an.

OpenTelemetry-Sammler

Für Systeme mit hoher Auslastung, die Tausende von Anfragen pro Sekunde verarbeiten, ist die Verwaltung des Datenvolumens von entscheidender Bedeutung. Das Volumen der Trace-Daten übersteigt häufig das der Geschäftsdaten. Daher ist es wichtig, Prioritäten für die zu erfassenden und zu speichernden Daten festzulegen. Hier kommt unser Tool zur Datenverarbeitung und -filterung ins Spiel, mit dem Sie bestimmen können, welche Daten es wert sind, gespeichert zu werden. Normalerweise möchten Teams Traces speichern, die bestimmte Kriterien erfüllen, wie zum Beispiel:


  • Spuren mit Antwortzeiten, die einen bestimmten Schwellenwert überschreiten.
  • Spuren, bei deren Verarbeitung Fehler aufgetreten sind.
  • Spuren, die bestimmte Attribute enthalten, z. B. solche, die durch einen bestimmten Mikrodienst gelaufen sind oder im Code als verdächtig gekennzeichnet wurden.
  • Eine zufällige Auswahl regelmäßiger Ablaufverfolgungen, die eine statistische Momentaufnahme der normalen Vorgänge des Systems liefern und Ihnen dabei helfen, typisches Verhalten zu verstehen und Trends zu erkennen.

Um zu bestimmen, welche Spuren gespeichert und welche verworfen werden sollen, werden die folgenden beiden wichtigsten Stichprobenverfahren verwendet:

  • Head Sampling – entscheidet zu Beginn einer Spur, ob sie behalten wird oder nicht
  • Tail Sampling – entscheidet erst, wenn die komplette Spur verfügbar ist. Dies ist notwendig, wenn die Entscheidung von Daten abhängt, die später in der Spur erscheinen. Zum Beispiel Daten, die Fehlerspannen enthalten. Diese Fälle können nicht durch Head Sampling behandelt werden, da sie zuerst eine Analyse der gesamten Spur erfordern.


Der OpenTelemetry Collector hilft dabei, das Datenerfassungssystem so zu konfigurieren, dass nur die erforderlichen Daten gespeichert werden. Wir werden die Konfiguration später besprechen, aber jetzt wollen wir uns der Frage zuwenden, was im Code geändert werden muss, damit Traces generiert werden.

Zero-Code-Instrumentierung

Für die Generierung von Traces war nur minimaler Code erforderlich. Wir mussten unsere Anwendungen lediglich mit einem Java-Agenten starten und dabei Folgendes angeben: Konfiguration :


-javaagent:/opentelemetry-javaagent-1.29.0.jar

-Dotel.javaagent.configuration-file=/otel-config.properties


OpenTelemetry unterstützt eine Vielzahl von Bibliotheken und Frameworks , sodass wir nach dem Starten der Anwendung mit dem Agenten sofort Traces mit Daten zu den Phasen der Anforderungsverarbeitung zwischen Diensten, im DBMS usw. erhielten.


In unserer Agentenkonfiguration haben wir die von uns verwendeten Bibliotheken deaktiviert, deren Spannen wir nicht in den Spuren sehen wollten, und um Daten über die Funktionsweise unseres Codes zu erhalten, haben wir ihn mit markiert Anmerkungen :


 @WithSpan("acquire locks") public CompletableFuture<Lock> acquire(SortedSet<Object> source) { var traceLocks = source.stream().map(Object::toString).collect(joining(", ")); Span.current().setAttribute("locks", traceLocks); return CompletableFuture.supplyAsync(() -> /* async job */); }


In diesem Beispiel wird für die Methode die Annotation @WithSpan verwendet, die die Notwendigkeit signalisiert, einen neuen Span mit dem Namen " acquire locks " zu erstellen, und dem erstellten Span wird im Methodenkörper das Attribut " locks " hinzugefügt.


Wenn die Methode ihre Arbeit beendet, wird der Span geschlossen. Bei asynchronem Code ist es wichtig, auf dieses Detail zu achten. Wenn Sie Daten zur Arbeit von asynchronem Code in Lambda-Funktionen abrufen müssen, die von einer kommentierten Methode aufgerufen werden, müssen Sie diese Lambdas in separate Methoden aufteilen und mit einer zusätzlichen Annotation markieren.

Unser Trace-Sammel-Setup

Lassen Sie uns nun darüber sprechen, wie das gesamte Trace-Sammelsystem konfiguriert wird. Alle unsere JVM-Anwendungen werden mit einem Java-Agenten gestartet, der Daten an den OpenTelemetry-Sammler sendet.


Ein einzelner Collector kann jedoch keinen großen Datenfluss verarbeiten und dieser Teil des Systems muss skaliert werden. Wenn Sie für jede JVM-Anwendung einen separaten Collector starten, wird das Tail-Sampling unterbrochen, da die Trace-Analyse auf einem Collector erfolgen muss und wenn die Anfrage mehrere JVMs durchläuft, landen die Spannen eines Trace auf verschiedenen Collectoren und ihre Analyse ist unmöglich.


Hier ein Collector konfiguriert als Balancer kommt zur Rettung.


Als Ergebnis erhalten wir das folgende System: Jede JVM-Anwendung sendet Daten an denselben Balancer-Sammler, dessen einzige Aufgabe darin besteht, von verschiedenen Anwendungen empfangene, aber mit einer bestimmten Ablaufverfolgung verknüpfte Daten an denselben Sammler-Prozessor zu verteilen. Anschließend sendet der Sammler-Prozessor Daten an Grafana Tempo.



Schauen wir uns die Konfiguration der Komponenten in diesem System genauer an.

Lastausgleichskollektor

In der Collector-Balancer-Konfiguration haben wir die folgenden Hauptteile konfiguriert:


 receivers:  otlp:    protocols:      grpc: exporters:  loadbalancing:    protocol:      otlp:        tls:          insecure: true    resolver:      static:        hostnames:          - collector-1.example.com:4317          - collector-2.example.com:4317          - collector-3.example.com:4317 service:  pipelines:    traces:      receivers: [otlp]      exporters: [loadbalancing]


  • Empfänger — hier werden die Methoden konfiguriert, mit denen der Collector Daten empfangen kann. Wir haben den Datenempfang ausschließlich im OTLP-Format konfiguriert. (Es ist möglich, den Datenempfang über viele andere Protokolle , zum Beispiel Zipkin, Jaeger.)
  • Exporteure — der Teil der Konfiguration, in dem der Datenausgleich konfiguriert wird. Unter den in diesem Abschnitt angegebenen Sammler-Prozessoren werden die Daten abhängig vom aus der Trace-ID berechneten Hash verteilt.
  • Der Abschnitt „Service“ gibt die Konfiguration der Funktionsweise des Dienstes an: nur mit Traces, unter Verwendung des oben konfigurierten OTLP-Empfängers und Datenübertragung als Balancer, d. h. ohne Verarbeitung.

Der Sammler mit Datenverarbeitung

Die Konfiguration von Kollektor-Prozessoren ist komplizierter, schauen wir uns das also einmal an:


 receivers:  otlp:    protocols:      grpc:        endpoint: 0.0.0.0:14317 processors:  tail_sampling:    decision_wait: 10s    num_traces: 100    expected_new_traces_per_sec: 10    policies:      [          {            name: latency500-policy,            type: latency,            latency: {threshold_ms: 500}          },          {            name: error-policy,            type: string_attribute,            string_attribute: {key: error, values: [true, True]}          },          {            name: probabilistic10-policy,            type: probabilistic,            probabilistic: {sampling_percentage: 10}          }      ]  resource/delete:    attributes:      - key: process.command_line        action: delete      - key: process.executable.path        action: delete      - key: process.pid        action: delete      - key: process.runtime.description        action: delete      - key: process.runtime.name        action: delete      - key: process.runtime.version        action: delete exporters:  otlp:    endpoint: tempo:4317    tls:      insecure: true service:  pipelines:    traces:      receivers: [otlp]      exporters: [otlp]


Ähnlich wie die Collector-Balancer-Konfiguration besteht die Verarbeitungskonfiguration aus den Abschnitten Receivers, Exporters und Service. Wir konzentrieren uns jedoch auf den Abschnitt Processors, in dem erklärt wird, wie Daten verarbeitet werden.


Zunächst demonstriert der Abschnitt tail_sampling eine Konfiguration Dadurch können die für die Speicherung und Analyse benötigten Daten gefiltert werden:


  • latency500-policy : Diese Regel wählt Spuren mit einer Latenz von mehr als 500 Millisekunden aus.

  • Fehlerrichtlinie : Diese Regel wählt Traces aus, bei deren Verarbeitung Fehler aufgetreten sind. Sie sucht in den Trace-Spans nach einem Zeichenfolgenattribut namens „error“ mit den Werten „true“ oder „True“.

  • probabilistic10-policy : Diese Regel wählt zufällig 10 % aller Spuren aus, um Einblicke in den normalen Anwendungsbetrieb, Fehler und die Verarbeitung langer Anforderungen zu bieten.


Zusätzlich zum Tail_Sampling zeigt dieses Beispiel den Abschnitt „Ressource/Delete“ , um unnötige Attribute zu löschen, die für die Datenanalyse und -speicherung nicht erforderlich sind.

Ergebnisse

Das resultierende Grafana-Trace-Suchfenster ermöglicht es Ihnen, Daten nach verschiedenen Kriterien zu filtern. In diesem Beispiel zeigen wir einfach eine Liste von Traces an, die vom Lobby-Dienst empfangen wurden, der Spielmetadaten verarbeitet. Die Konfiguration ermöglicht zukünftiges Filtern nach Attributen wie Latenz, Fehlern und Zufallsstichproben.


Im Fenster „Trace-Ansicht“ wird die Ausführungszeitleiste des Lobby-Dienstes angezeigt, einschließlich der verschiedenen Bereiche, aus denen die Anforderung besteht.


Wie Sie der Abbildung entnehmen können, läuft die Abfolge der Ereignisse wie folgt ab: Es werden Sperren erworben, dann werden Objekte aus dem Cache abgerufen, anschließend erfolgt die Ausführung einer Transaktion, die die Anforderungen verarbeitet, wonach die Objekte erneut im Cache gespeichert und die Sperren freigegeben werden.


Die Spans im Zusammenhang mit Datenbankanforderungen wurden dank der Instrumentierung von Standardbibliotheken automatisch generiert. Im Gegensatz dazu wurden die Spans im Zusammenhang mit Sperrverwaltung, Cache-Operationen und Transaktionsinitiierung mithilfe der oben genannten Anmerkungen manuell zum Geschäftscode hinzugefügt.



Beim Anzeigen eines Spans können Sie Attribute sehen, die Ihnen ein besseres Verständnis dafür ermöglichen, was während der Verarbeitung passiert ist. Beispielsweise können Sie eine Abfrage in der Datenbank anzeigen.



Eines der interessanten Features von Grafana Tempo ist die Servicediagramm , das alle Dienste, die Traces exportieren, die Verbindungen zwischen ihnen sowie die Rate und Latenz der Anfragen grafisch darstellt:


Einpacken

Wie wir gesehen haben, hat die Arbeit mit OpenTelemetry Tracing unsere Beobachtungsmöglichkeiten ganz erheblich verbessert. Mit minimalen Codeänderungen und einem gut strukturierten Collector-Setup haben wir tiefe Einblicke gewonnen – und außerdem haben wir gesehen, wie die Visualisierungsfunktionen von Grafana Tempo unser Setup zusätzlich ergänzt haben. Danke fürs Lesen!