Erreichung umfassender Microservice-Observability mit Go und OpenTelemetry
Wenhao Wang
Dev Intern · Leapcell

Einleitung
In der sich rasant entwickelnden Landschaft der modernen Softwareentwicklung sind Microservices zum De-facto-Architekturstil für den Aufbau skalierbarer, resilienter und unabhängig bereitstellbarer Anwendungen geworden. Während Microservices unbestreitbare Vorteile in Bezug auf Agilität und Wartbarkeit bieten, führen sie zu erheblicher Komplexität, insbesondere beim Verständnis des Anfrageflusses durch ein verteiltes System. Eine einzelne Benutzerinteraktion kann eine Kaskade von Aufrufen über zahlreiche Dienste auslösen, was es äußerst schwierig macht, Leistungsengpässe zu diagnostizieren, Fehler zu identifizieren oder sogar den Gesamtzustand des Systems ohne ausreichende Sichtbarkeit zu erfassen.
Hier glänzt das Konzept des verteilten Tracings. Verteiltes Tracing ermöglicht es uns, den gesamten Weg einer Anfrage über alle beteiligten Dienste zu visualisieren und unschätzbare Einblicke in Latenz, Fehler und Inter-Service-Abhängigkeiten zu geben. Angesichts der Bedeutung von Go für die Erstellung leistungsstarker Microservices ist die Integration einer robusten Tracing-Lösung von größter Bedeutung. OpenTelemetry, ein branchenüblicher Open-Source-Observability-Framework, bietet einen einheitlichen Ansatz zur Erfassung von Traces, Metriken und Logs. Dieser Artikel führt Sie durch die Integration von OpenTelemetry in Ihre Go-Microservices, ermöglicht umfassendes Full-Stack-Tracing und stattet Sie mit beispielloser Sichtbarkeit in Ihre verteilten Anwendungen aus.
Kernkonzepte des verteilten Tracings verstehen
Bevor wir uns mit der Implementierung befassen, wollen wir ein gemeinsames Verständnis der Schlüsselkonzepte entwickeln, die für verteiltes Tracing und OpenTelemetry zentral sind.
- Trace: Ein Trace stellt den gesamten Ausführungspfad einer einzelnen Anfrage oder Transaktion dar, während sie sich durch ein verteiltes System ausbreitet. Es ist eine Sammlung geordneter Spans.
- Span: Ein Span ist eine benannte, zeitgesteuerte Operation, die eine logische Arbeitseinheit innerhalb eines Traces darstellt. Jeder Span hat eine Start- und Endzeit, einen Namen und Attribute. Spans können verschachtelt sein und eine Eltern-Kind-Beziehung bilden. Beispielsweise kann eine API-Anfrage einen übergeordneten Span generieren, der dann untergeordnete Spans für Datenbankaufrufe, Aufrufe externer Dienste oder die Ausführung interner Geschäftslogik aufweist.
- Kontextweitergabe (Context Propagation): Der Mechanismus, durch den Tracing-Informationen (wie
trace_id
undspan_id
) zwischen Diensten weitergegeben werden, wenn Anfragen durch das System laufen. Dies ist entscheidend für die Verknüpfung von Spans zur Bildung eines vollständigen Traces. OpenTelemetry verwendet ein vereinbartes Kontextformat (z. B. W3C Trace Context), um die Interoperabilität sicherzustellen. - Tracer Provider: Der Einstiegspunkt für die Erstellung von
Tracer
-Instanzen. Er konfiguriert Exporter, Sampler und Ressourcenattribute. - Tracer: Eine Schnittstelle zur Erstellung von
Span
-Objekten. - Exporter: Verantwortlich für das Senden abgeschlossener Spans an ein Backend-System (z. B. Jaeger, Zipkin, OTLP-Collector) zur Speicherung und Analyse.
- Sampler: Bestimmt, welche Traces aufgezeichnet und exportiert werden sollen. Sampling kann verwendet werden, um das Volumen der Tracing-Daten zu steuern, insbesondere bei Systemen mit hohem Durchsatz.
Implementierung von Full-Stack-Tracing mit Go und OpenTelemetry
Lassen Sie uns die Integration von OpenTelemetry in eine einfache Go-Microservice-Architektur veranschaulichen. Wir betrachten zwei Dienste: einen Order Service
, der die Bestellungserstellung bearbeitet, und einen Product Service
, der Produktdetails abruft. Der Order Service
wird den Product Service
aufrufen.
Zuerst müssen wir OpenTelemetry in beiden Diensten einrichten.
1. Initialisierung von OpenTelemetry in Go
Wir erstellen eine Hilfsfunktion zur Initialisierung von OpenTelemetry mit einem Jaeger-Exporter, einem beliebten Open-Source-System für verteiltes Tracing.
// common/otel.go package common import ( "context" "fmt" "log" "os" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/sdk/resource" tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" ) // InitTracerProvider initializes an OpenTelemetry TracerProvider func InitTracerProvider(serviceName string) (func(context.Context) error, error) { // Erstelle Jaeger-Exporter url := "http://localhost:14268/api/traces" // Standard Jaeger Collector Endpoint exporter, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url))) if err != nil { return nil, fmt.Errorf("fehlgeschlagen, Jaeger-Exporter zu erstellen: %w", err) } // Erstelle einen neuen Tracer Provider mit dem Jaeger-Exporter // und einem BatchSpanProcessor, um Spans effizient zu senden. tp := tracesdk.NewTracerProvider( tracesdk.WithBatchProcessor(tracesdk.NewBatchSpanProcessor(exporter)), // Resource identifiziert den Dienst und seine Attribute. tracesdk.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(serviceName), attribute.String("environment", "development"), )), ) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(otel.NewCompositeTextMapPropagator( // Standard-Kontext-Propagatoren für W3C Trace Context // und B3 (für Abwärtskompatibilität bei Bedarf). // Wir verwenden hauptsächlich W3C Trace Context. otel.GetTextMapPropagator(), // Standardmäßig wird W3C Trace Context enthalten sein )) log.Printf("OpenTelemetry für Dienst initialisiert: %s", serviceName) return tp.Shutdown, nil }
Diese Funktion InitTracerProvider
tut Folgendes:
- Konfiguriert einen Jaeger-Exporter: Sie weist OpenTelemetry an, Traces an einen lokal laufenden Jaeger-Collector zu senden.
- Erstellt einen
TracerProvider
: Dieser Provider verwaltetTracer
-Instanzen und konfiguriert, wie Spans verarbeitet werden (z. B. Verwendung einesBatchSpanProcessor
zur Effizienz). - Setzt
Resource
-Attribute: Diese Attribute liefern Metadaten über den'`'Dienst selbst (z. B. Dienstname, Umgebung). - Setzt
TextMapPropagator
: Dies ist entscheidend für die Kontextweitergabe. Es konfiguriert, wie der Trace-Kontext in Header von Anfragen injiziert und extrahiert wird.otel.GetTextMapPropagator()
enthält standardmäßigW3C Trace Context
, den empfohlenen Standard.
2. Implementierung des Product Service
Der Product Service
gibt einfach eine Liste von Produkten zurück. Wir instrumentieren ihn so, dass für eingehende HTTP-Anfragen automatisch Spans erstellt werden.
// product-service/main.go package main import ( "context" "fmt" "log" "net/http" "os" "time" "github.com/yourusername/app/common" // Angenommen, common/otel.go ist hier "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) func productsHandler(w http.ResponseWriter, r *http.Request) { // Holt den Span aus dem Anfragekontext, der automatisch von otelhttp erstellt wurde. // Wir können diesem Span benutzerdefinierte Attribute hinzufügen oder untergeordnete Spans erstellen. span := trace.SpanFromContext(r.Context()) span.SetAttributes(attribute.String("product.category", "electronics")) // Simuliert einige Arbeit time.Sleep(50 * time.Millisecond) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"products": [{"id": "prod1", "name": "Laptop"}, {"id": "prod2", "name": "Monitor"}]}`)) log.Println("Antwort auf /products-Anfrage gesendet") } func main() { // Initialisiere OpenTelemetry shutdown, err := common.InitTracerProvider("product-service") if err != nil { log.Fatalf("Fehler bei der Initialisierung von OpenTelemetry: %v", err) } defer func() { if err := shutdown(context.Background()); err != nil { log.Fatalf("Fehler bei der Beendigung von TracerProvider: %v", err) } }() // Verwende otelhttp.NewHandler, um den HTTP-Server zu instrumentieren http.Handle("/products", otelhttp.NewHandler(http.HandlerFunc(productsHandler), "/products")) port := ":8081" log.Printf("Product Service lauscht auf %s", port) if err := http.ListenAndServe(port, nil); err != nil { log.Fatalf("Product Service konnte nicht gestartet werden: %v", err) } }
Wichtige Punkte im Product Service
:
common.InitTracerProvider
: Initialisiert OpenTelemetry.otelhttp.NewHandler
: Dies ist ein praktischer Wrapper ausgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
. Er fängt automatisch eingehende HTTP-Anfragen ab, erstellt für jede Anfrage einen Span (extrahiert bei Bedarf den übergeordneten Kontext aus Headern) und konfiguriert den HTTP-Handler des Servers zur Verwendung des instrumentierten Kontexts.trace.SpanFromContext(r.Context())
: Ermöglicht uns, den aktuellen Span aus dem Anfragekontext abzurufen und benutzerdefinierte Attribute hinzuzufügen, um detailliertere Informationen über den Vorgang zu erhalten.
3. Implementierung des Order Service
Der Order Service
stellt einen Endpunkt für die Auftragserstellung bereit. Dieser Endpunkt ruft dann einen HTTP-Aufruf an den Product Service
auf, um Produktdetails abzurufen.
// order-service/main.go package main import ( "context" "fmt" "io" "log" "net/http" "os" "time" "github.com/yourusername/app/common" // Angenommen, common/otel.go ist hier "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) var tracer = otel.Tracer("order-service") func createOrderHandler(w http.ResponseWriter, r *http.Request) { // Erstellt einen neuen Span für den gesamten Auftragserstellungsprozess. // Der übergeordnete Kontext wird implizit aus der eingehenden HTTP-Anfrage (über otelhttp.NewHandler) übernommen. ctx, span := tracer.Start(r.Context(), "createOrder") defer span.End() span.SetAttributes(attribute.String("order.id", "order123")) log.Println("Order Service: Anfrage zur Auftragserstellung erhalten") // Simuliert eine anfängliche Verarbeitung time.Sleep(10 * time.Millisecond) // Führt einen HTTP-Aufruf an den Product Service durch productSvcURL := "http://localhost:8081/products" req, err := http.NewRequestWithContext(ctx, "GET", productSvcURL, nil) if err != nil { span.RecordError(err) span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("Fehler beim Erstellen der Anfrage: %v", err), http.StatusInternalServerError) return } // Instrumentiert den HTTP-Client-Aufruf // otelhttp.Client ist entscheidend für die Weitergabe des Trace-Kontexts an den nachgelagerten Dienst. client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} log.Printf("Order Service: Rufe Product Service unter %s auf", productSvcURL) resp, err := client.Do(req) if err != nil { span.RecordError(err) span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("Fehler beim Aufrufen des Product Service: %v", err), http.StatusInternalServerError) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("Product Service gab einen Status ungleich 200 zurück: %d", resp.StatusCode), http.StatusInternalServerError) return } body, err := io.ReadAll(resp.Body) if err != nil { span.RecordError(err) span.SetAttributes(attribute.Bool("error", true)) http.Error(w, fmt.Sprintf("Fehler beim Lesen der Product Service-Antwort: %v", err), http.StatusInternalServerError) return } log.Printf("Order Service: Produkte empfangen: %s", string(body)) // Simuliert das abschließende Speichern der Bestellung time.Sleep(20 * time.Millisecond) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) w.Write([]byte(fmt.Sprintf(`{"message": "Order created successfully with products: %s"}`, string(body)))) log.Println("Order Service: Bestellung erfolgreich erstellt") } func main() { // Initialisiere OpenTelemetry shutdown, err := common.InitTracerProvider("order-service") if err != nil { log.Fatalf("Fehler bei der Initialisierung von OpenTelemetry: %v", err) } defer func() { if err := shutdown(context.Background()); err != nil { log.Fatalf("Fehler bei der Beendigung von TracerProvider: %v", err) } }() // Instrumentiert den eingehenden HTTP-Server für den Order Service http.Handle("/order", otelhttp.NewHandler(http.HandlerFunc(createOrderHandler), "/order")) port := ":8080" log.Printf("Order Service lauscht auf %s", port) if err := http.ListenAndServe(port, nil); err != nil { log.Fatalf("Order Service konnte nicht gestartet werden: %v", err) } }
Wichtige Punkte im Order Service
:
tracer.Start(r.Context(), "createOrder")
: Erstellt manuell einen neuen Span für diecreateOrder
-Operation. Entscheidend ist, dassr.Context()
übergeben wird, das den Trace-Kontext enthält, der vomotelhttp.NewHandler
für die eingehende Anfrage weitergegeben wurde. Dadurch wirdcreateOrder
zu einem untergeordneten Span des Spans der eingehenden Anfrage.http.NewRequestWithContext(ctx, "GET", productSvcURL, nil)
: Beim Ausführen einer ausgehenden Anfrage ist es wichtig, den aktuellen Tracing-context.Context
(ctx
vontracer.Start
) zu übergeben. Dies stellt sicher, dass die Trace-ID und die übergeordnete Span-ID im Anfragekontext enthalten sind.client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
:otelhttp.NewTransport
wird verwendet, um den Standard-HTTP-Client-Transport zu umschließen. Dieser Wrapper injiziert automatisch den Trace-Kontext ausreq.Context()
in die ausgehenden HTTP-Anfrage-Header (z. B. dentraceparent
-Header). Dies ist die Magie, die die Kontextweitergabe zwischen Diensten ermöglicht.span.RecordError(err)
undspan.SetAttributes(attribute.Bool("error", true))
: Best Practice, Fehler zu protokollieren und den Span bei einem Fehler als fehlerhaft zu markieren. Dies erleichtert das Filtern nach problematischen Traces in Ihrem Observability-Backend.
Ausführen des Beispiels
-
Jaeger starten: Sie können Jaeger mit Docker ausführen:
docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 -p 6831:6831/udp -p 16686:16686 jaegertracing/all-in-one:latest
Greifen Sie dann über
http://localhost:16686
auf die Jaeger UI zu. -
Dienste erstellen und ausführen:
# Im Verzeichnis product-service go mod init github.com/yourusername/app/product-service go mod tidy go run main.go # Im Verzeichnis order-service go mod init github.com/yourusername/app/order-service go mod tidy go run main.go
Stellen Sie sicher, dass
common/otel.go
zugänglich ist, z. B. indem Sie es in einemcommon
-Verzeichnis auf derselben Ebene wieproduct-service
undorder-service
platzieren und dieimport
-Pfade anpassen. -
Eine Anfrage stellen:
curl http://localhost:8080/order
-
In der Jaeger UI beobachten: Gehen Sie zu
http://localhost:16686
, wählen Sieorder-service
als Dienst und finden Sie Ihre Traces. Sie sollten einen Trace mit Spans für denorder-service
(die eingehende Anfrage, dencreateOrder
-Span und den ausgehenden HTTP-Client-Aufruf) und einen untergeordneten Span sehen, der die eingehende Anfrage improduct-service
darstellt.
Vorteile und Anwendungsszenarien
Die Integration von OpenTelemetry für Full-Stack-Tracing bietet zahlreiche Vorteile:
- Schnellere Fehlerbehebung: Lokalisieren Sie schnell den genauen Dienst oder die Komponente, die Latenzen oder Fehler verursacht, indem Sie den Anfragefluss visualisieren.
- Leistungsüberwachung: Identifizieren Sie Leistungsengpässe in Microservices, wie z. B. langsame Datenbankabfragen, ineffiziente API-Aufrufe oder externe Abhängigkeiten mit hoher Latenz.
- Ursachenanalyse: Verfolgen Sie den Kontext von Fehlern, einschließlich der beteiligten Dienste und ihres jeweiligen Zustands, was zur effektiven Identifizierung der Grundursache beiträgt.
- Zuordnungen von Service-Abhängigkeiten: Entdecken und visualisieren Sie automatisch die Abhängigkeiten zwischen Ihren Microservices, was für das Verständnis komplexer Architekturen unerlässlich ist.
- Verbesserte Observability: Bietet einen konsistenten und einheitlichen Ansatz zur Erfassung und zum Export von Telemetriedaten (Traces, Metriken und Logs) und bewegt sich damit in Richtung einer umfassenden Observability-Strategie.
- Anbieterneutralität: OpenTelemetry ist ein offener Standard, der es Ihnen ermöglicht, zwischen verschiedenen Observability-Backends (Jaeger, Zipkin, DataDog, New Relic usw.) zu wechseln, ohne Ihren Anwendungscode zu ändern.
Dieses Setup ist für jede Go-Microservice-Anwendung, die in der Produktion betrieben wird, von E-Commerce-Plattformen bis hin zu Finanzdienstleistungen, von entscheidender Bedeutung, wo das Verständnis des Echtzeit-Systemverhaltens kritisch ist.
Fazit
Full-Stack-Tracing ist ein unverzichtbares Werkzeug in der Welt der Microservices und bietet tiefe Einblicke in den komplexen Tanz verteilter Anwendungen. Durch die Nutzung von OpenTelemetry mit Go können Entwickler ihre Dienste mit einem standardisierten, anbieterunabhängigen Framework instrumentieren. Diese Integration verwandelt undurchsichtige verteilte Systeme in transparente, beobachtbare Einheiten, vereinfacht die Fehlerbehebung, Leistungsoptimierung und das allgemeine Systemverständnis drastisch. Die Einführung von OpenTelemetry ebnet den Weg für wahrhaft robuste und wartbare Microservice-Architekturen.