在分布式系统中(不仅限于微服务架构),相关多个事件和日志是必要的。OpenTelemetry跟踪为此提供了一个标准化的解决方案。

OpenTelemetry支持多种可观测性解决方案,本文仅关注跟踪。OpenTelemetry不仅是一个规范,还是许多编程语言的SDK(OTel SDK)。本文使用Golang库来展示一个真实的示例。Golang库具有许多功能,因此如何激活多跳跟踪并不明显。此外,可能有不同的解决方案来实现目标。

OpenTelemetry高级架构

OpenTelemetry假设有一个集中式收集器。没有收集器,只能使用有限的OT功能。本文使用Jaeger作为集中式收集器。有关日志项的相关性将在我的下一篇文章中描述:使用Jaeger和Grafana Loki进行Istio跟踪和相关性

概念

分布式跟踪,通常称为跟踪,记录了请求(由应用程序或终端用户发出)在多服务体系结构(如微服务和无服务器应用程序)中传播时所采取的路径。

上下文传播

跟踪Context用于传播跟踪及其Span和其他状态信息之间的关系。一个跟踪有一个Span图:

跟踪及其Span

上下文在HTTP头中传播,指定在Trace Context规范中的:

traceparentversion,trace-id,parent-id,trace-flagstrace-idparent-idtracestate

还有第三个HTTP头用于附加信息:

用于跟踪到跟踪信息的行李

跟踪状态和行李值不会自动设置为传出的HTTP请求。它必须在http.Client传输OTel仪表化的HTTP请求上下文中设置。

上述HTTP头用于在Span之间传播数据。还可以将另一种数据类型发送到Collector:

  • 跟踪器提供程序属性(在Jaeger UI上进行处理)
  • 跟踪器选项(Jaeger UI上的标签)
  • Span属性(Jaeger UI上的标签)
  • Span事件(Jaeger UI上的日志)

跟踪状态和行李的值不会自动发送到Collector,因此必须手动提取。

在Span上调用End()后,数据将发送到Collector。数据以批处理方式发送到Collector,因此应在Tracer Provider上调用ForceFlush()或Shutdown(),以确保数据将及时发送到Collector。

代码仪表化

传入请求

以下行可提取传入HTTP请求的父Span:

ctx := otel.GetTextMapPropagator().Extract(
    r.Context(), propagation.HeaderCarrier(r.Header),
)
parentSpan := trace.SpanFromContext(ctx)
parentBaggage := baggage.FromContext(ctx)

“parentSpan”变量包含跟踪状态键值对,可以将其记录到控制台。记录的信息可用于从多个日志文件中收集相同跟踪的日志项。

如果缺少OpenTelemetry标头(因为它是根Span),则以下示例将向上下文中添加一个键值对:

command := r.Method + " " + r.URL.String()                                                             traceState := trace.TraceState{}                               traceState, _ = traceState.Insert("client_command", command)
ctx = trace.ContextWithSpanContext(ctx, trace.NewSpanContext(
    trace.SpanContextConfig{TraceState: traceState}
))

请注意,跟踪状态键和值必须编码。

大多数上述代码由HTTP路由器中间件执行。

子Span

ctx
ctx, span = tr.Start(ctx, "IN HTTP "+r.Method+" "+r.URL.String(),
    trace.WithSpanKind(trace.SpanKindServer),
)
ctxctxctx

传出请求

ctx
req, _ := http.NewRequestWithContext(
    ctx, http.MethodGet, beURL, http.NoBody,
)httpClient := &http.Client{Transport: otelhttp.NewTransport(
    http.DefaultTransport,
)}resp, err := httpClient.Do(req)

上述代码将http.Client.Transport仪表化为使用OTel HTTP客户端中间件。此中间件通过trace.SpanFromContext()调用(由tr.Start()设置)从HTTP请求上下文中提取Span,并在发送到较低层(http.DefaultTransport = http.Transport)之前设置传出的HTTP请求标头。

一些库希望http.Transport在http.Client.Transport中。在这种情况下,可以使用http.Transport.RegisterProtocol()来仪表化http.Transport,但需要更多的编码。另一种解决方法是以与otelhttp.NewTransport相同的方式更改请求HTTP标头:

t.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header))

otelhttp.NewTransport()可以具有选项,例如:- Span名称格式化程序

  • 过滤器以排除特定请求
  • 附加Span选项
没有收集器
traceparenttracestatebaggagetraceparenttracestatebaggage
traceparentclientCommand
traceparenttracestate

代码的仪表化章节已经描述了提取和传播Trace State和Baggage的方法。

OTel提供了一个虚拟的stdout Exporter,但如果我们的仪表化代码或服务器应用程序代码格式化并将日志消息发送到中央日志服务器,则更加灵活。

没有收集器的OpenTelemetry

使用收集器

可以将多个数据发送到收集器(跟踪器提供程序,跟踪器选项,Span属性和Span事件),这些数据提供了有关我们的系统的更多信息。Jaeger是一个著名的收集器,具有自己的Exporter。如果收集器支持OpenTelemetry Protocol Exporter (OTLP),则可以使用供应商不可知的Exporter,请参见:https://medium.com/@magstherdev/grafana-cloud-tempo-3b95373ff9d0

可以通过以下示例注册Exporter:

traceExporter, _ := jaeger.New(jaeger.WithCollectorEndpoint(
 jaeger.WithEndpoint(jaegerURL),
))tp := sdktrace.NewTracerProvider(
   sdktrace.WithSampler(sdktrace.AlwaysSample()),
   sdktrace.WithBatcher(traceExporter),
   sdktrace.WithResource(resource.NewWithAttributes(
       semconv.SchemaURL,
       semconv.ServiceNamespaceKey.String("demo"),
       semconv.ServiceNameKey.String(service),
       semconv.ServiceInstanceIDKey.String(instance),
   )),
)tr := tp.Tracer("demotracer", trace.WithInstrumentationVersion(
    tracing.SemVersion(),
))
tr
ctx, span = tr.Start(ctx, "IN HTTP "+r.Method+" "+r.URL.String(),
 trace.WithAttributes(
     semconv.NetAttributesFromHTTPRequest("tcp", r)...),
 trace.WithAttributes(
     semconv.HTTPClientAttributesFromHTTPRequest(r)...),
 trace.WithAttributes(
     semconv.HTTPServerAttributesFromHTTPRequest(
         instance, route, r)...),
 trace.WithSpanKind(trace.SpanKindServer),
 trace.WithAttributes(attribute.String(
     tracing.StateKeyClientCommand, clientCommand,
 )),
)
semconv
ctx

Jaeger Span Exporter通过Thrift协议(HTTP REST)将数据发送到收集器。

带有Jaeger的OpenTelemetry

测试设置

创建了一个客户端、一个前端和两个后端。业务逻辑非常简单:客户端向前端发送URL列表,前端应调用这些列表上的后端。

client_command

下面的Tracer Provider属性由自己的源代码设置:

  • service.namespace
  • service.instance.id
  • attrID (=PID)

下面的Span属性由自己的源代码设置:

  • http.route
  • http.server_name
  • otel.library.name
  • otel.library.version
  • span.kind
semconv

Jaeger Deep Dependency Graph的扩展协作图:

测试设置协作图

一个Trace在Jaeger UI上看起来像:

一个Trace

Trace名称包含客户端CLI命令。如果没有设置(因为客户端不支持OpenTelemetry),则前端将请求方法+URL设置为命令:

客户端不支持OpenTelemetry

OTel HTTP客户端中间件为每个传出的HTTP请求创建一个新的客户端Span(otel.library.name = go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp):

自动客户端Span,每个传出的HTTP请求一个

client_command

后端Span属性

后端事件包含用户名:

后端事件

源代码 总结
traceparent