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.
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.
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
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
In dieser Diskussion konzentrieren wir uns auf den Tracing-Aspekt von OpenTelemetry.
Werfen wir auch einen Blick auf das OpenTelemetry-Projekt, das durch die Zusammenführung der
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:
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.
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
Die Java-API, das SDK:
Eine wichtige Komponente ist der OpenTelemetry Collector , ein Proxy, der Daten empfängt, verarbeitet und weiterleitet – schauen wir uns das genauer an.
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:
Um zu bestimmen, welche Spuren gespeichert und welche verworfen werden sollen, werden die folgenden beiden wichtigsten Stichprobenverfahren verwendet:
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.
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:
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
OpenTelemetry unterstützt eine Vielzahl von
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
@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.
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
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.
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]
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
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.
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
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!