OpenTelemetry in der Praxis

In jüngerer Zeit sind zwei Standards - OpenTracing und OpenCensus - endlich zu einem verschmolzen. Ein neuer Standard für die verteilte Verfolgung und Überwachung ist erschienen - OpenTelemetry. Trotz der Tatsache, dass die Entwicklung der Bibliotheken in vollem Gange ist, gibt es nicht allzu viel echte Erfahrung mit der Verwendung.



Ilya Kaznacheev, der seit acht Jahren entwickelt und als Backend-Entwickler bei MTS arbeitet, ist bereit, die Verwendung von OpenTelemetry in Golang-Projekten zu erläutern. Auf der Golang Live 2020-Konferenz sprach er darüber, wie die Verwendung eines neuen Standards für die Rückverfolgung und Überwachung eingerichtet und mit der bereits im Projekt vorhandenen Infrastruktur befreundet werden kann.





OpenTelemetry ist Ende letzten Jahres ein relativ neuer Standard. Gleichzeitig wurde es von vielen Anbietern von Software für die Rückverfolgung und Überwachung weit verbreitet und unterstützt.



Beobachtbarkeit oder Beobachtbarkeit ist ein Begriff aus der Kontrolltheorie, der bestimmt, wie sehr man den inneren Zustand eines Systems anhand seiner äußeren Erscheinungsformen beurteilen kann. In der Systemarchitektur bedeutet dies eine Reihe von Ansätzen zur Überwachung des Systemzustands zur Laufzeit. Diese Ansätze umfassen Protokollierung, Ablaufverfolgung und Überwachung.







Es gibt viele Anbieterlösungen für die Rückverfolgung und Überwachung. Bis vor kurzem gab es zwei offene Standards: OpenTracing von CNCF, das 2016 erschien, und Open Census von Google, das 2018 erschien.



Dies sind zwei ziemlich gute Standards, die eine Weile miteinander konkurrierten, bis sie 2019 beschlossen, sich zu einem neuen Standard namens OpenTelemetry zusammenzuschließen.







Dieser Standard umfasst die verteilte Verfolgung und Überwachung. Es ist kompatibel mit den ersten beiden. Darüber hinaus haben OpenTracing und Open Census die Unterstützung innerhalb von zwei Jahren eingestellt, was uns unweigerlich dem Übergang zu OpenTelemetry näher bringt.



Anwendungsfälle



Der Standard bietet zahlreiche Möglichkeiten, alles mit allem zu kombinieren, und ist in der Tat eine aktive Schicht zwischen den Quellen von Metriken und Traces und ihren Verbrauchern.

Werfen wir einen Blick auf die Hauptszenarien.



Für die verteilte Ablaufverfolgung können Sie direkt eine Verbindung zu Jaeger oder einem von Ihnen verwendeten Dienst herstellen.







Wenn die Ablaufverfolgung direkt übertragen wird, können Sie config verwenden und einfach die Bibliothek ersetzen.



Wenn Ihre Anwendung bereits OpenTracing verwendet, können Sie die OpenTracing Bridge verwenden, einen Wrapper, der Anforderungen an die OpenTracing-API in die OpenTelemetry-API auf oberster Ebene konvertiert.







Zum Sammeln von Metriken können Sie Prometheus auch so konfigurieren, dass direkt auf den Metrikport für Ihre Anwendung zugegriffen wird.







Dies ist nützlich, wenn Sie über eine einfache Infrastruktur verfügen und Metriken direkt erfassen. Der Standard bietet aber auch mehr Flexibilität.



Das Hauptszenario für die Verwendung des Standards ist das Sammeln von Metriken und Traces über einen Collector, der auch von einer separaten Anwendung oder einem separaten Container in Ihrer Infrastruktur gestartet wird. Außerdem können Sie einen vorgefertigten Behälter nehmen und zu Hause installieren.



Dazu reicht es aus, den Exporter in der Anwendung im OTLP-Format zu konfigurieren. Dies ist ein grpc-Schema für die Datenübertragung im OpenTracing-Format. Auf der Kollektorseite können Sie das Format und die Parameter für den Export von Metriken und Traces an Endbenutzer oder in andere Formate konfigurieren. Zum Beispiel in OpenCensus.







Mit dem Collector können Sie eine große Anzahl von Arten von Datenquellen und viele Datensenken am Ausgang verbinden.







Daher bietet der OpenTelemetry-Standard Kompatibilität mit vielen Open Source- und Herstellerstandards.



Der Standardverteiler ist erweiterbar. Daher haben die meisten Anbieter Exporteure bereits für ihre eigenen Lösungen bereit, falls vorhanden. Sie können OpenTelemetry auch dann verwenden, wenn Sie Metriken und Traces von einem proprietären Anbieter erfassen. Dies löst das Problem mit der Lieferantenbindung. Auch wenn etwas noch nicht direkt für OpenTelemetry angezeigt wurde, kann es über OpenCensus weitergeleitet werden.



Der Kollektor selbst ist sehr einfach über die banale YAML-Konfiguration zu konfigurieren: Hier werden







Empfänger angegeben. Ihre Anwendung hat möglicherweise eine andere Quelle (Kafka usw.):







Exporteure - Datenempfänger.

Prozessoren - Methoden zur Verarbeitung von Daten im Kollektor:







Und Pipelines, die direkt definieren, wie jeder Datenstrom, der innerhalb eines Kollektors fließt, behandelt wird:







Schauen wir uns ein anschauliches Beispiel an.







Angenommen, Sie haben einen Microservice, an den Sie OpenTelemetry bereits angeschraubt und konfiguriert haben. Und noch ein Dienst mit ähnlicher Fragmentierung.



Bisher ist alles einfach. Aber da sind:



  • Legacy-Services, die über OpenCensus ausgeführt werden;
  • eine Datenbank, die Daten in ihrem eigenen Format sendet (z. B. direkt an Prometheus, wie dies PostgreSQL tut);
  • Ein anderer Dienst, der in einem Container arbeitet und Metriken in seinem eigenen Format bereitstellt. Sie möchten diesen Container nicht neu erstellen und die Seitenwagen vermasseln, damit sie die Metriken neu formatieren. Sie möchten sie nur abholen und senden.
  • Hardware, von der Sie auch Metriken sammeln und diese irgendwie verwenden möchten.


Alle diese Metriken können in einem Kollektor kombiniert werden.







Es werden bereits viele Quellen für Metriken und Traces unterstützt, die in vorhandenen Anwendungen verwendet werden. Und falls Sie etwas Exotisches verwenden, können Sie Ihr eigenes Plugin implementieren. In der Praxis ist dies jedoch wahrscheinlich nicht erforderlich. Weil Anwendungen, die Metriken oder Traces auf die eine oder andere Weise exportieren, entweder einige gängige Standards oder offene Standards wie OpenCensus verwenden.



Jetzt wollen wir diese Informationen verwenden. Sie können Jaeger als Exporteur von Traces angeben und Metriken an Prometheus oder etwas Kompatibles senden. Nehmen wir an, die beliebtesten VictoriaMetrics aller.



Aber was ist, wenn wir uns plötzlich entschlossen, zu AWS zu wechseln und den lokalen Röntgen-Tracer zu verwenden? Kein Problem. Dies kann über OpenCensus weitergeleitet werden, das einen Exporteur für X-Ray hat.



Auf diese Weise können Sie Ihre gesamte Infrastruktur für Metriken und Traces zusammenstellen.



Die Theorie ist vorbei. Lassen Sie uns darüber sprechen, wie Tracing in der Praxis eingesetzt wird.



Instrumentierung der Golang-Anwendung: Ablaufverfolgung



Zunächst müssen Sie eine Stammspanne erstellen, aus der der Aufrufbaum wächst.



ctx := context.Background()
tr := global.Tracer("github.com/me/otel-demo")
ctx, span := tr.Start(ctx, "root")
span.AddEvent(ctx, "I am a root span!")
doSomeAction(ctx, "12345")
span.End()
      
      





Dies ist der Name Ihres Dienstes oder Ihrer Bibliothek. Auf diese Weise können Sie Bereiche in der Ablaufverfolgung definieren, die im Rahmen Ihrer Anwendung liegen, und diejenigen, die in die importierten Bibliotheken verschoben wurden.



Als Nächstes wird eine Stammspanne mit dem Namen erstellt:



ctx, span := tr.Start(ctx, "root")
      
      





Wählen Sie einen Namen, der die Ablaufverfolgungsstufe klar beschreibt. Beispielsweise kann es sich entweder um den Namen einer Methode (oder einer Klasse und einer Methode) oder um eine Architekturebene handeln. Zum Beispiel Infrastrukturschicht, Logikschicht, Datenbankschicht usw.



Die Span-Daten werden auch in einen Kontext gestellt:



ctx, span := tr.Start(ctx, "root")
span.AddEvent(ctx, "I am a root span!")
doSomeAction(ctx, "12345")

      
      





Daher müssen Sie die Methoden, die Sie verfolgen möchten, an den Kontext übergeben.



Span repräsentiert einen Prozess auf einer bestimmten Ebene im Aufrufbaum. Sie können Attribute, Protokolle und Fehlerstatus einfügen, falls dies auftritt. Die Spanne muss am Ende geschlossen sein. Im geschlossenen Zustand wird die Dauer berechnet.



ctx, span := tr.Start(ctx, "root")
span.AddEvent(ctx, "I am a root span!")
doSomeAction(ctx, "12345")
span.End()
      
      





So sieht unsere Spanne in Jaeger aus: Sie







können sie erweitern und die Protokolle und Attribute anzeigen.



Dann können Sie dieselbe Spanne aus dem Kontext abrufen, wenn Sie keine neue festlegen möchten. Sie möchten beispielsweise eine Architekturebene in einem Bereich schreiben, und Ihre Ebene ist auf mehrere Methoden und mehrere Aufrufebenen verteilt. Sie bekommen es, schreiben darauf und dann schließt es.



func doSomeAction(ctx context.Context, requestID string) {
      span := trace.SpanFromContext(ctx)
      span.AddEvent(ctx, "I am the same span!")
      ...
}
      
      





Beachten Sie, dass Sie es hier nicht schließen müssen, da es in derselben Methode geschlossen wird, in der es erstellt wurde. Wir nehmen es nur aus dem Zusammenhang.



Schreiben einer Nachricht in den Stammbereich:







Manchmal müssen Sie einen neuen untergeordneten Bereich erstellen, damit dieser separat vorhanden ist.



func doSomeAction(ctx context.Context, requestID string) {
   ctx, span := global.Tracer("github.com/me/otel-demo").
      Start(ctx, "child")
   defer span.End()
   span.AddEvent(ctx, "I am a child span!")
   ...
}
      
      





Hier erhalten wir einen globalen Tracer namens Bibliothek. Dieser Aufruf kann in eine Methode eingeschlossen werden, oder Sie können eine globale Variable verwenden, da diese für Ihren gesamten Dienst gleich ist.



Als nächstes wird eine untergeordnete Spanne aus dem Kontext erstellt und ihm ein Name zugewiesen, ähnlich wie wir es am Anfang getan haben:



   Start(ctx, "child")
      
      





Denken Sie daran, den Bereich am Ende der Methode zu schließen, in der er erstellt wurde.



  ctx, span := global.Tracer("github.com/me/otel-demo"). 
      Start(ctx, "child") 
   defer span.End()
      
      





Wir schreiben Nachrichten hinein, die in die untergeordnete Spanne fallen.







Hier können Sie sehen, dass die Nachrichten hierarchisch angezeigt werden und sich die untergeordnete Spanne unter der übergeordneten befindet. Es wird erwartet, dass es kürzer ist, da es sich um einen synchronen Anruf handelt.



Es zeigt die Attribute, die in der Spanne geschrieben werden können:



func doSomeAction(ctx context.Context, requestID string) {
      ...
      span.SetAttributes(label.String("request.id", requestID))
      span.AddEvent(ctx, "request validation ok")
   span.AddEvent(ctx, "entities loaded", label.Int64("count", 123))
      span.SetStatus(codes.Error, "insertion error")
}
      
      





Zum Beispiel kam unsere Anfrage hierher. id:







Sie können Ereignisse hinzufügen:



   span.AddEvent(ctx, "request validation ok")
      
      





Sie können hier auch eine Beschriftung hinzufügen. Dies funktioniert ähnlich wie ein strukturiertes Protokoll in Form von Logrus:



span.AddEvent(ctx, "entities loaded", label.Int64("count", 123))
      
      





Hier sehen wir unsere Nachricht im Span Log. Sie können es erweitern und Beschriftungen anzeigen. In unserem Fall wurde hier eine Etikettenanzahl hinzugefügt:







Dann ist es praktisch, sie beim Filtern in einer Suche zu verwenden.



Wenn ein Fehler auftritt, können Sie dem Bereich einen Status hinzufügen. In diesem Fall wird es als ungültig markiert.



  span.SetStatus(codes.Error, "insertion error")
      
      





Der Standard, der verwendet wurde, um Fehlercodes von OpenCensus zu verwenden, und sie stammten von grpc. Jetzt sind nur noch OK, ERROR und UNSET übrig. OK ist die Standardeinstellung, im Fehlerfall wird ERROR hinzugefügt.



Hier sehen Sie, dass die Fehlerspur mit einem roten Symbol markiert ist. Es gibt einen Fehlercode und eine Meldung dazu:







Wir dürfen nicht vergessen, dass die Ablaufverfolgung kein Ersatz für Protokolle ist. Der Hauptpunkt besteht darin, den Informationsfluss durch ein verteiltes System zu verfolgen. Dazu müssen Sie Spuren in Netzwerkanforderungen einfügen und diese von dort aus lesen können.



Trace Microservices



OpenTelemetry verfügt bereits über zahlreiche Set-Party-Implementierungen von Interceptors und Middleware für verschiedene Frameworks und Bibliotheken. Sie finden sie im Repository: github.com/open-telemetry/opentelemetry-go-contrib



Liste der Frameworks, für die es Interceptors und Middleware gibt:



  • Beego
  • erhol dich
  • Gin
  • gocql
  • mux
  • Echo
  • http
  • grpc
  • sarama
  • Memcache
  • Mongo
  • Macaron


Lassen Sie uns anhand eines Standard-HTTP-Clients und -Servers als Beispiel sehen, wie dies verwendet wird.



Middleware-Client



Im Client fügen wir einfach einen Interceptor als Transport hinzu. Anschließend werden unsere Anforderungen mit trace.id und den Informationen, die zum Fortsetzen des Trace erforderlich sind, angereichert.



client := http.Client{
      Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
      
      





Middleware-Server



Auf dem Server wird eine kleine Middleware mit dem Namen der Bibliothek hinzugefügt:



http.Handle("/", otelhttp.NewHandler(
      http.HandlerFunc(get), "root"))
err := http.ListenAndServe(addr, nil)
      
      





Dann, wie üblich: Holen Sie sich eine Spanne aus dem Kontext, arbeiten Sie damit, schreiben Sie etwas hinein, erstellen Sie Kinderspannen, schließen Sie sie usw.



So sieht eine einfache Anfrage aus, die drei Dienste durchläuft:







Der Screenshot zeigt die Hierarchie der Anrufe, die Aufteilung in Dienste, ihre Dauer und Reihenfolge. Sie können auf jeden von ihnen klicken und detailliertere Informationen sehen.



Und so sieht der Fehler aus:







Es ist einfach zu verfolgen, wo er passiert ist, wann und wie viel Zeit vergangen ist.

In span sehen Sie detaillierte Informationen zu dem Kontext, in dem der Fehler aufgetreten ist:







Darüber hinaus können Felder, die sich auf den gesamten Bereich beziehen (verschiedene Anforderungs-IDs, Schlüsselfelder in der Tabelle in der Anforderung, einige andere Metadaten, die Sie einfügen möchten), beim Erstellen in den Bereich verschachtelt werden. Grob gesagt müssen Sie nicht alle diese Felder kopieren und an jeder Stelle einfügen, an der Sie einen Fehler behandeln. Sie können Daten darüber schreiben, um sie zu überspannen.



Middleware-Funktion



Hier ist ein kleiner Bonus: Wie erstelle ich Middleware, damit Sie sie als globale Middleware für Dinge wie Gorilla und Gin verwenden können:



middleware := func(h http.Handler) http.Handler {
      return otelhttp.NewHandler(h, "root")
}
      
      





Golang Application Instrumentation: Überwachung



Es ist Zeit, über die Überwachung zu sprechen.



Die Verbindung zum Überwachungssystem wird auf die gleiche Weise wie für die Ablaufverfolgung konfiguriert.



Messungen werden in zwei Typen unterteilt:



1. Synchron, wenn der Benutzer zum Zeitpunkt des Aufrufs explizit Werte übergibt:



  • Zähler
  • UpDownCounter
  • ValueRecorder


int64, float64



2. Asynchron, das SDK zum Zeitpunkt der Datenerfassung aus der Anwendung liest:



  • SumObserver
  • UpDownSumObserver
  • ValueObserver


int64, float64



Die Metriken selbst sind:



  • Additiv und monoton (Zähler, SumObserver), die positive Zahlen summieren und nicht abnehmen.
  • Additiv, aber nicht monoton (UpDownCounter, UpDownSumObserver), das positive und negative Zahlen summieren kann.
  • Nicht additiv (ValueRecorder, ValueObserver), die einfach eine Folge von Werten aufzeichnen. Zum Beispiel eine Art Verteilung.


Zu Beginn des Programms wird ein globaler Zähler erstellt, auf dem der Name der Bibliothek oder des Dienstes angegeben ist.



meter := global.Meter("github.com/ilyakaznacheev/otel-demo")
floatCounter := metric.Must(meter).NewFloat64Counter(
         "float_counter",
         metric.WithDescription("Cumulative float counter"),
   ).Bind(label.String("label_a", "some label"))
defer floatCounter.Unbind()
      
      





Als nächstes wird eine Metrik erstellt:



floatCounter := metric.Must(meter).NewFloat64Counter(
         "float_counter",
         metric.WithDescription("Cumulative float counter"),
   ).Bind(label.String("label_a", "some label"))
      
      





Sie erhält einen Namen:



   "float_counter",
      
      





Beschreibung:




         metric.WithDescription("Cumulative float counter"),
      
      





Eine Reihe von Beschriftungen, nach denen Sie Anforderungen filtern können. Zum Beispiel beim Erstellen von Dashboards in Grafana:




    ).Bind(label.String("label_a", "some label"))

      
      





Am Ende des Programms müssen Sie außerdem für jede Metrik Unbind aufrufen, wodurch Ressourcen freigesetzt und korrekt geschlossen werden:




defer floatCounter.Unbind()

      
      





Das Aufzeichnen von Änderungen ist einfach:



var (
counter metric.BoundFloat64Counter
udCounter metric.BoundFloat64UpDownCounter
valueRecorder metric.BoundFloat64ValueRecorder
)
...
counter.Add(ctx, 1.5)
udCounter.Add(ctx, -2.5)
valueRecorder.Record(ctx, 3.5)

      
      





Dies sind positive Zahlen für Counter, beliebige Zahlen für den UpDownCounter, die summiert werden, sowie beliebige Zahlen für den ValueRecorder. Für alle Arten von Instrumenten unterstützt Go int64 und float64.



Das bekommen wir am Ausgang:



# HELP float_counter Cumulative float counter
# TYPE float_counter counter
float_counter{label_a="some label"} 20
      
      





Dies ist unsere Metrik mit einem Kommentar und einer bestimmten Bezeichnung. Dann können Sie es entweder direkt über Prometheus nehmen oder über den OpenTelemetry-Kollektor exportieren und dann verwenden, wo immer wir es brauchen.



Golang Application Instrumentation: Libraries



Das Letzte, was ich sagen möchte, ist die Fähigkeit, die der Standard für die Instrumentierung von Bibliotheken bietet.



Bisher konnten Sie bei Verwendung von OpenCensus und OpenTracing Ihre einzelnen Bibliotheken, insbesondere Open Source-Bibliotheken, nicht instrumentieren. Denn in diesem Fall haben Sie eine Lieferantenbindung erhalten. Jeder, der eng mit der Ablaufverfolgung zusammengearbeitet hat, hat wahrscheinlich darauf geachtet, dass große Clientbibliotheken oder große APIs für Cloud-Dienste von Zeit zu Zeit mit schwer zu erklärenden Fehlern abstürzen.



Die Rückverfolgung wäre hier sehr nützlich. Besonders in Bezug auf die Produktivität, wenn Sie eine unklare Situation haben und ich wirklich gerne wissen würde, warum es passiert ist. Sie haben jedoch nur eine Fehlermeldung aus Ihrer importierten Bibliothek.



OpenTelemetry löst dieses Problem.







Da das SDK und die API im Standard getrennt sind, kann die Metrik-Tracing-API unabhängig vom SDK und bestimmten Datenexporteinstellungen verwendet werden. Darüber hinaus können Sie zuerst Ihre Methoden instrumentieren und erst dann den Export dieser Daten nach außen konfigurieren.

Auf diese Weise können Sie die importierte Bibliothek instrumentieren, ohne sich Gedanken darüber machen zu müssen, wie und wo die Daten exportiert werden. Dies funktioniert sowohl für interne als auch für Open Source-Bibliotheken.



Sie müssen sich keine Gedanken über die Lieferantenbindung machen, Sie müssen sich keine Gedanken darüber machen, wie diese Informationen verwendet werden oder ob sie überhaupt verwendet werden. Bibliotheken und Anwendungen werden im Voraus instrumentiert, und die Datenexportkonfiguration wird bei der Initialisierung der Anwendung angegeben.



So können Sie sehen, dass die Konfigurationseinstellungen in der SDK-Anwendung festgelegt sind. Als nächstes müssen Sie sich mit den Exporteuren von Ablaufverfolgung und Metriken befassen. Es kann ein Exporteur über OTLP sein, wenn Sie in den OpenTelemetry-Kollektor exportieren. Dann fallen alle erforderlichen Traces und Metriken in den Kontext und werden von einer anderen Methode über den Aufrufbaum weitergegeben.



Die Anwendung erbt den Rest der Bereiche vom Stammbereich, indem sie einfach die OpenTelemetry-API und die im Kontext enthaltenen Daten verwendet. In diesem Fall erhalten die importierten Bibliotheken die Kontextmethoden als Eingabe. Versuchen Sie, Informationen über den Stammbereich dieser Methode zu lesen. Wenn es nicht da ist, erstellen sie ihre eigenen und weisen dann die Logik an. Auf diese Weise können Sie zuerst Ihre Bibliothek instrumentieren.



Darüber hinaus können Sie alles instrumentieren, aber die Datenexporteure nicht konfigurieren und einfach bereitstellen.



Dies funktioniert möglicherweise in der Produktion für Sie. Bis die Infrastruktur eingerichtet ist, sind Tracing und Überwachung nicht konfiguriert. Dann konfigurieren Sie sie, stellen dort einen Kollektor bereit, einige Anwendungen zum Sammeln dieser Daten, und alles wird für Sie funktionieren. Sie müssen nichts direkt an den Methoden selbst ändern.



Wenn Sie also über eine Open Source-Bibliothek verfügen, können Sie diese mithilfe von OpenTelemetry instrumentieren. Dann konfigurieren die Benutzer OpenTelemetry und verwenden diese Daten.



Abschließend möchte ich sagen, dass der OpenTelemetry-Standard vielversprechend ist. Vielleicht ist dies schließlich der gleiche universelle Standard, den wir alle sehen wollten.



Unser Unternehmen nutzt den OpenCensus-Standard aktiv zur Verfolgung und Überwachung der Microservice-Landschaft des Unternehmens. Es ist geplant, OpenTelemetry nach seiner Veröffentlichung zu implementieren.



All Articles