在分布式系统中(不仅限于微服务架构),相关多个事件和日志是必要的。OpenTelemetry跟踪为此提供了一个标准化的解决方案。
OpenTelemetry支持多种可观测性解决方案,本文仅关注跟踪。OpenTelemetry不仅是一个规范,还是许多编程语言的SDK(OTel SDK)。本文使用Golang库来展示一个真实的示例。Golang库具有许多功能,因此如何激活多跳跟踪并不明显。此外,可能有不同的解决方案来实现目标。
OpenTelemetry高级架构
OpenTelemetry假设有一个集中式收集器。没有收集器,只能使用有限的OT功能。本文使用Jaeger作为集中式收集器。有关日志项的相关性将在我的下一篇文章中描述:使用Jaeger和Grafana Loki进行Istio跟踪和相关性
概念
分布式跟踪,通常称为跟踪,记录了请求(由应用程序或终端用户发出)在多服务体系结构(如微服务和无服务器应用程序)中传播时所采取的路径。
上下文传播
跟踪Context用于传播跟踪及其Span和其他状态信息之间的关系。一个跟踪有一个Span图:
跟踪及其Span
上下文在HTTP头中传播,指定在Trace Context规范中的:
traceparent
头描述了传入请求在其跟踪图中的位置。它有四个字段:version,trace-id,parent-id,trace-flags
,其中trace-id
标识整个跟踪,不应在整个跟踪中修改,parent-id
标识父Span,必须在子Span中更改。tracestate
头是供应商特定的键值集。
还有第三个HTTP头用于附加信息:
- baggage 是指在Span之间传递的上下文信息。
用于跟踪到跟踪信息的行李
跟踪状态和行李值不会自动设置为传出的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
在处理传入父Span或向ctx
中添加新Span后,必须创建一个新的子Span,例如:
ctx, span = tr.Start(ctx, "IN HTTP "+r.Method+" "+r.URL.String(),
trace.WithSpanKind(trace.SpanKindServer),
)
Start()的ctx
参数应为先前由HTTP路由器中间件创建的ctx
,返回的ctx
变量必须用于传出HTTP请求
传出请求
OTel HTTP客户端中间件可以使用上述创建的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选项
没有收集器
在转向Jaeger UI之前,让我们看看没有任何收集器的可能性。Span属性和事件无法发送到下一跳,只能使用 traceparent
,tracestate
和baggage
HTTP标头。 traceparent
的内容由规范保留,因此只能使用tracestate
和baggage
。
例如,如果我们有一个自己的客户端,它将客户端命令放在traceparent
的clientCommand
键中,可以在服务器端提取并记录到控制台。
带有traceparent
,tracestate
和行李的HTTP请求标头
代码的仪表化章节已经描述了提取和传播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(),
))
在https://pkg.go.dev/go.opentelemetry.io/otel/semconv上有很多预定义的Span属性键。
创建的tr
可用于创建一个新的子Span,例如:
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
模块提供了更多预定义函数,以从HTTP请求中填充Span属性。
在HTTP路由器中间件中,必须将ctx
设置回http.Request上。
Jaeger Span Exporter通过Thrift协议(HTTP REST)将数据发送到收集器。
带有Jaeger的OpenTelemetry
测试设置
创建了一个客户端、一个前端和两个后端。业务逻辑非常简单:客户端向前端发送URL列表,前端应调用这些列表上的后端。
客户端在Trace State和Baggage的client_command
字段中发送自己的CLI命令。服务器组件必须使用它(控制台日志,Span属性)并传播它。
下面的Tracer Provider属性由自己的源代码设置:
- service.namespace
- service.instance.id
- attrID (=PID)
下面的Span属性由自己的源代码设置:
- http.route
- http.server_name
- otel.library.name
- otel.library.version
- span.kind
semconv
模块的其他Span属性由预定义函数设置。
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请求一个
后端Trace Provider属性包含来自客户端的client_command
(使用Trace State):
后端Span属性
后端事件包含用户名:
后端事件
源代码
源代码可以在https://github.com/pgillich/opentracing-example找到
总结
OpenTelemetry是跟踪分布式系统的强大而灵活的解决方案。它具有许多功能,因此实现完整功能的学习路径很长。Istio已经支持traceparent
HTTP头(参见:https://istio.io/latest/docs/tasks/observability/distributed-tracing/overview/),因此可以在Kiali仪表板上呈现跟踪结果,可以参考我接下来的文章:使用Jaeger进行Istio跟踪
译自:https://faun.pub/multi-hop-tracing-with-opentelemetry-in-golang-792df5feb37c
评论(0)