Service-Tracing, OpenTracing und Jaeger

Service-Tracing, OpenTracing und Jaeger

In unseren Projekten setzen wir auf eine mikroservicebasierte Architektur. Bei der Entstehung von Engpässen in der Leistung wird viel Zeit mit Monitoring und der Analyse von Protokollen verbracht. Beim Protokollieren der Zeitmessungen einzelner Operationen in die Log-Datei ist es in der Regel schwierig nachzuvollziehen, was die Ausführung dieser Operationen ausgelöst hat, die Reihenfolge der Aktionen zu verfolgen oder die zeitlichen Abweichungen einer Operation im Vergleich zu einer anderen über verschiedene Dienste hinweg zu erkennen.

Um den manuellen Aufwand zu minimieren, haben wir uns entschieden, eines der Tracing-Tools zu nutzen. In diesem Artikel werden wir erörtern, wie und wofür man Tracing einsetzen kann und wie wir dies umgesetzt haben.

Welche Probleme lassen sich mit Tracing lösen

  1. Den Engpass in der Leistung sowohl innerhalb eines Dienstes als auch im gesamten Ausführungsbaum zwischen allen beteiligten Diensten zu finden. Zum Beispiel:
    • Eine Vielzahl von kurzen aufeinanderfolgenden Aufrufen zwischen Diensten, zum Beispiel bei der Geokodierung oder bei Datenbankabfragen.
    • Lange Wartezeiten bei Ein- und Ausgabe, zum Beispiel bei der Datenübertragung über das Netzwerk oder beim Lesen von der Festplatte.
    • Langsame Datenverarbeitung.
    • Lange CPU-intensive Operationen.
    • Codeabschnitte, die für das Endergebnis nicht erforderlich sind und entweder entfernt oder verzögert ausgeführt werden können.
  2. Anschaulich verstehen, in welcher Reihenfolge etwas aufgerufen wird und was passiert, wenn eine Operation durchgeführt wird.
    Service-Tracing, OpenTracing und Jaeger
    Es ist erkennbar, dass zum Beispiel eine Anfrage im Dienst WS einging → der Dienst WS die Daten über den Dienst R ergänzt hat → dann die Anfrage an den Dienst V gesendet hat → der Dienst V viele Daten aus dem Dienst R geladen hat → eine Anfrage an den Dienst P gemacht hat → der Dienst P erneut den Dienst R aufgerufen hat → der Dienst V das Ergebnis ignoriert und zum Dienst J gegangen ist → und erst dann die Antwort im Dienst WS zurückgegeben hat, während er im Hintergrund weiterhin etwas anderes berechnete.
    Ohne eine solche Tracing- oder ausführliche Dokumentation des gesamten Prozesses ist es sehr schwierig zu verstehen, was passiert, wenn man zum ersten Mal auf den Code schaut, da der Code über verschiedene Dienste verteilt ist und hinter vielen Beans und Schnittstellen verborgen ist.
  3. Informationssammlung über den Ausführungsbaum für eine spätere verzögerte Analyse. In jedem Ausführungsschritt können Informationen, die zu diesem Zeitpunkt verfügbar sind, zum Trace hinzugefügt werden, um herauszufinden, welche Eingangsdaten zu diesem Szenario geführt haben. Zum Beispiel:
    • Benutzer-ID
    • Rechte
    • Art der gewählten Methode
    • Log oder Ausführungsfehler
  4. Umwandlung von Traces in ein Teilmenge von Metriken und anschließende Analyse in Form von Metriken.

Was kann die Trace-Logging-Funktion aufzeichnen. Span

In einer Trace gibt es das Konzept des Spans, das einem einzelnen Log in der Konsole entspricht. Ein Span hat:

  • Eine Bezeichnung, normalerweise der Name der ausgeführten Methode
  • Den Namen des Dienstes, in dem der Span erstellt wurde
  • Eine eigene, eindeutige ID
  • Irgendeine Meta-Information in Form von Schlüssel/Wert, die darin protokolliert wurde. Zum Beispiel die Parameter der Methode oder ob die Methode mit einem Fehler beendet wurde oder nicht.
  • Die Start- und Endzeit der Ausführung dieses Spans
  • Die ID des übergeordneten Spans

Jeder Span wird an den Collector der Spans gesendet, um in der Datenbank für die spätere Ansicht gespeichert zu werden, sobald seine Ausführung abgeschlossen ist. Später kann ein Baum aller Spans erstellt werden, indem die IDs der Eltern verbunden werden. Bei der Analyse können beispielsweise alle Spans in einem bestimmten Dienst gefunden werden, die mehr als eine bestimmte Zeit in Anspruch genommen haben. Außerdem kann man, indem man zu einem bestimmten Span wechselt, den gesamten Baum über und unter diesem Span einsehen.

Service-Tracing, OpenTracing und Jaeger

Opentrace, Jagger und wie wir dies für unsere Projekte umgesetzt haben

Es gibt einen gemeinsamen Standard Opentrace, der beschreibt, wie und was gesammelt werden soll, ohne sich durch die Traces an eine spezifische Implementierung in einer bestimmten Sprache zu binden. Zum Beispiel erfolgt in Java die gesamte Arbeit mit Traces über die gemeinsame API Opentrace, hinter der beispielsweise Jaeger oder eine leere Standardimplementierung, die nichts tut, verborgen sein kann.
Wir verwenden Jaeger als Implementierung von Opentrace. Es besteht aus mehreren Komponenten:

Service-Tracing, OpenTracing und Jaeger

  • Jaeger-Agent — ein lokaler Agent, der normalerweise auf jeder Maschine läuft und auf den lokalen Standardport loggen die Dienste. Wenn der Agent nicht vorhanden ist, sind die Traces aller Dienste auf dieser Maschine normalerweise deaktiviert.
  • Jaeger-Collector — dieser empfängt die gesammelten Traces von allen Agenten und speichert sie in der ausgewählten Datenbank.
  • Datenbank — bevorzugt verwenden sie Cassandra, aber wir verwenden Elasticsearch; es gibt Implementierungen für einige andere Datenbanken und eine In-Memory-Implementierung, die nichts auf die Festplatte speichert.
  • Jaeger-Query — dies ist ein Dienst, der in die Datenbank geht und die bereits gesammelten Traces zur Analyse zurückgibt.
  • Jaeger-UI — dies ist die Weboberfläche zur Suche und zum Anzeigen von Traces, die auf Jaeger-Query zugreift.

Service-Tracing, OpenTracing und Jaeger

Ein wichtiger Bestandteil ist die Implementierung von OpenTracing Jaeger für spezifische Programmiersprachen, über die Spans an den Jaeger-Agent gesendet werden.
Integration von Jaeger in Java besteht darin, das Interface io.opentracing.Tracer zu implementieren, wodurch alle Traces über dieses Interface an den echten Agenten gesendet werden.

Service-Tracing, OpenTracing und Jaeger

Für Spring-Komponenten kann ebenfalls opentracing-spring-cloud-starter und die Implementierung von Jaeger opentracing-spring-jaeger-cloud-starter eingebunden werden, die automatisch das Tracing für alles konfiguriert, was durch diese Komponenten läuft, wie z.B. HTTP-Anfragen in Controller, Datenbankabfragen über JDBC usw.

Logging von Traces in Java

An oberster Stelle sollte der erste Span erstellt werden, was automatisch durch den Spring-Controller bei Empfang einer Anfrage geschehen kann, oder manuell, falls dies nicht der Fall ist. Dieser wird dann durch den Scope weitergegeben. Wenn eine Methode weiter unten einen neuen Span hinzufügen möchte, nimmt sie den aktuellen activeSpan aus dem Scope, erstellt einen neuen Span, erklärt, dass der erhaltene activeSpan der übergeordnete ist, und macht den neuen Span aktiv. Bei Aufrufen externer Dienste wird der aktuelle aktive Span übergeben, und diese Dienste erstellen neue Spans in Verbindung mit diesem Span.
Die gesamte Arbeit erfolgt über das Tracer-Instanz. Sie kann über den DI-Mechanismus oder GlobalTracer.get() als globale Variable abgerufen werden, falls der DI-Mechanismus nicht funktioniert. Standardmäßig wird, wenn der Tracer nicht initialisiert wurde, ein NoopTracer zurückgegeben, der keine Funktion hat.
Von dem Tracer wird dann über den ScopeManager der aktuelle Scope abgerufen. Ein neuer Scope wird basierend auf dem aktuellen erstellt, der an einen neuen Span gebunden ist. Anschließend wird der erstellte Scope geschlossen, wodurch der span beendet wird und der vorherige Scope wieder aktiv wird. Da der Scope an den Thread gebunden ist, muss beim Multithreading berücksichtigt werden, dass der aktive Span in einen anderen Thread übergeben wird, um den Scope des anderen Threads mit diesem Span zu aktivieren.

io.opentracing.Tracer tracer = ...; // GlobalTracer.get()

void DoSmth () {
   try (Scope scope = tracer.buildSpan("DoSmth").startActive(true)) {
      ...
   }
}
void DoOther () {
    Span span = tracer.buildSpan("someWork").start();
    try (Scope scope = tracer.scopeManager().activate(span, false)) {
        // Dinge erledigen.
    } catch(Exception ex) {
        Tags.ERROR.set(span, true);
        span.log(Map.of(Fields.EVENT, "error", Fields.ERROR_OBJECT, ex, Fields.MESSAGE, ex.getMessage()));
    } finally {
        span.finish();
    }
}

void DoAsync () {
    try (Scope scope = tracer.buildSpan("ServiceHandlerSpan").startActive(false)) {
        ...
        final Span span = scope.span();
        doAsyncWork(() -> {
            // SCHRITT 2 OBEN: reaktiviere den Span im Callback, beginne aktiv zu sein, wenn der Span beendet werden muss.
            try (Scope scope = tracer.scopeManager().activate(span, false)) {
                ...
            }
        });
    }
}

Für die Multi-Thread-Programmierung gibt es auch TracedExecutorService und ähnliche Wrapper, die den aktuellen Span beim Start von asynchronen Aufgaben automatisch in den Thread durchreichen:

private ExecutorService executor = new TracedExecutorService(
    Executors.newFixedThreadPool(10), GlobalTracer.get()
);

Für externe HTTP-Anfragen gibt es TracingHttpClient

HttpClient httpClient = new TracingHttpClientBuilder().build();

Herausforderungen, mit denen wir konfrontiert sind

  • Beans und DI funktionieren nicht immer, wenn der Tracer nicht in einem Dienst oder einer Komponente verwendet wird. Dann Autowired kann der Tracer möglicherweise nicht funktionieren und es muss GlobalTracer.get() verwendet werden.
  • Annotations funktionieren nicht, wenn es sich nicht um eine Komponente oder einen Dienst handelt, oder wenn der Methodenaufruf aus einer benachbarten Methode derselben Klasse erfolgt. Man muss vorsichtig sein, überprüfen, was funktioniert, und die manuelle Erstellung des Traces verwenden, wenn @Traced nicht funktioniert. Zusätzlich kann ein Compiler für Java-Annotationen integriert werden, dann sollten sie überall funktionieren.
  • In älteren Spring- und Spring-Boot-Versionen funktioniert die Autokonfiguration von OpenTracing mit Spring Cloud nicht aufgrund von Bugs in der DI. Wenn man möchte, dass die Traces in den Spring-Komponenten automatisch funktionieren, kann man dies analog zu machen wie in github.com/opentracing-contrib/java-spring-jaeger/blob/master/opentracing-spring-jaeger-starter/src/main/java/io/opentracing/contrib/java/spring/jaeger/starter/JaegerAutoConfiguration.java
  • In Groovy funktioniert try mit Ressourcen nicht, man muss unbedingt try finally verwenden.
  • Jeder Dienst muss seinen eigenen spring.application.name angeben, unter dem die Traces protokolliert werden. Zudem sollte ein separater Name für die Produktion und den Test verwendet werden, um diese nicht zu vermischen.
  • Wenn GlobalTracer und Tomcat verwendet werden, haben alle im Tomcat laufenden Dienste denselben GlobalTracer. Daher wird für alle derselbe Dienstname verwendet.
  • Beim Hinzufügen von Traces zu einer Methode sollte sichergestellt werden, dass diese nicht mehrmals in einer Schleife aufgerufen wird. Es ist besser, einen gemeinsamen Trace für alle Aufrufe hinzuzufügen, der die gesamte Ausführungszeit protokolliert. Andernfalls wird eine übermäßige Last erzeugt.
  • Einmal haben wir im jaeger-ui zu große Anfragen für eine große Anzahl von Traces gemacht und, da wir auf die Antwort nicht gewartet haben, haben wir es erneut gemacht. Infolgedessen begann jaeger-query viel Speicher zu verbrauchen und verlangsamte Elasticsearch. Ein Neustart von jaeger-query hat geholfen.

Sampling, Speicherung und Ansicht von Traces

Es gibt drei Arten des Trace-Samplings:

  1. Const, das alle Traces sendet und speichert.
  2. Probabilistisch, das Traces mit einer bestimmten Wahrscheinlichkeit filtert.
  3. Rate-Limiting, das die Anzahl der Traces pro Sekunde begrenzt. Diese Parameter können entweder am Client, am jaeger-agent oder im Collector konfiguriert werden. Derzeit verwenden wir im Validator-Stack den Const-Wert 1, da die Anfragen nicht sehr zahlreich sind, aber sie viel Zeit in Anspruch nehmen. Könnte dies künftig zu einer übermäßigen Belastung des Systems führen, können wir eine Begrenzung einrichten.

Wenn Cassandra verwendet wird, speichert sie standardmäßig Traces nur für zwei Tage. Wir verwenden elasticsearch Die Traces werden zeitlich unbegrenzt gespeichert und nicht gelöscht. Für jeden Tag wird ein separater Index erstellt, zum Beispiel jaeger-service-2019-03-04. Zukünftig sollte eine automatische Bereinigung alter Traces eingerichtet werden.

Um die Traces anzuzeigen, müssen Sie:

  • Einen Service auswählen, nach dem Sie die Traces filtern möchten, zum Beispiel tomcat7-default für den Service, der in Tomcat läuft und keinen eigenen Namen haben kann.
  • Anschließend einen Vorgang, einen Zeitrahmen und eine Mindestlaufzeit wählen, zum Beispiel von 10 Sekunden, um nur die langwierigen Ausführungen zu erfassen.
    Service-Tracing, OpenTracing und Jaeger
  • In einen der Traces wechseln und überprüfen, was dort blockiert hat.
    Service-Tracing, OpenTracing und Jaeger

Wenn eine Anfrage-ID bekannt ist, kann der Trace über die Tagsuche mit dieser ID gefunden werden, vorausgesetzt, diese ID wird im Span des Traces protokolliert.

Dokumentation

Artikel

Video

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster