跳到主要内容

概览

为什么需要链路追踪

在微服务体系中,一次HTTP/RPC调用很可能涉及多个服务,每个服务内又可能调用其他的 HTTP API、MySQL、Redis等等,那如何知道是谁出了问题呢?又是在哪一个环节出了问题呢?如果使用日志来排查是可以做到,但是需要花费大量的时间,但是链路追踪可以帮助我们快速定位到问题所在。

OpenTelemetry 标准

分布式链路跟踪( Distributed Tracing )的概念最早是由Google提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在Tracing技术这块比较有影响力的是两大开源技术框架:Netflix公司开源的OpenTracing和Google开源的OpenCensus。两大框架都拥有比较高的开发者群体。为形成统一的技术标准,两大框架最终磨合成立了OpenTelemetry项目,简称otel。具体可以参考:

因此,我们的Tracing技术方案以 OpenTelemetry 为实施标准,协议标准的一些Golang实现开源项目:

https://github.com/open-telemetry/opentelemetry-go
https://github.com/open-telemetry/opentelemetry-go-contrib

其他第三方的框架和系统(如Jaeger/Prometheus/Grafana等)也会按照标准化的规范来对接 OpenTelemetry,使得系统的开发和维护成本大大降低。

核心概念

我们先看看 OpenTelemetry 的架构图,我们这里不会完整介绍,只会介绍其中大家常用的几个概念。关于 OpenTelemetry 的内部技术架构设计介绍,可以参考 OpenTelemetry 架构 ,关于语义约定请参考:https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md

opentelemetry_arch

TracerProvider

主要负责创建 Tracer,一般是需要第三方的分布式链路跟踪管理平台提供具体的实现。默认情况是一个空的 TracerProvider(NoopTracerProvider),虽然也能创建 Tracer 但是内部其实不会执行具体的数据流传输逻辑。举个例子,假如使用 jaeger,往往是这么来初始化并注入 jaegerTracerProvider

// InitTracerProvider returns an OpenTelemetry TracerProvider configured to use
// the Jaeger exporter that will send spans to the provided url. The returned
// TracerProvider will also use a Resource configured with all the information
// about the application.
func InitTracerProvider(serviceName, endpoint string, options ...Option) (*tracesdk.TracerProvider, error) {
var endpointOption jaeger.EndpointOption
if serviceName == "" {
return nil, errors.New("no service name provided")
}
if strings.HasPrefix(endpoint, "http") {
// http to collector
endpointOption = jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(endpoint))
} else {
// udp to agent
endpointOption = jaeger.WithAgentEndpoint(jaeger.WithAgentHost(endpoint))
}

// Create the Jaeger exporter
exporter, err := jaeger.New(endpointOption)
if err != nil {
return nil, err
}

opts := applyOptions(options...)
tp := tracesdk.NewTracerProvider(
// set sample
tracesdk.WithSampler(tracesdk.TraceIDRatioBased(opts.SamplingRatio)),
// Always be sure to batch in production.
tracesdk.WithBatcher(exporter),
// Record information about this application in an Resource.
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
)

// Register our TracerProvider as the global so any imported
// instrumentation in the future will default to using it.
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(jaegerprop.Jaeger{})

return tp, nil
}

数据模型

tracing-data-model

  • Trace (调用链):一个 Trace 代表一个事务或者流程在(分布式)系统中的执行过程。例如来自客户端的一个请求从接收到处理完成的过程就是一个 Trace。
  • Span(跨度):Span 是分布式追踪的最小跟踪单位,一个 Trace 由多段 Span 组成。可以被理解为一次方法调用, 一个程序块的调用, 或者一次 RPC/数据库访问。只要是一个具有完整时间周期的程序访问,都可以被认为是一个 Span。
  • SpanContext(跨度上下文):分布式追踪的上下文信息,包括 Trace id,Span id 以及其它需要传递到下游服务的内容。一个 OpenTracing 的实现需要将 SpanContext 通过某种序列化协议 (Wire Protocol) 在进程边界上进行传递,以将不同进程中的 Span 关联到同一个 Trace 上。对于 HTTP 请求来说,SpanContext 一般是采用 HTTP header 进行传递的。

多个 Span 共同组成一个有向无环图(DAG)形成了 Trace ,SpanContext 则用于将一个 Span 的上下文传递到其下游的 Span 中,以将这些 Span 关联起来。

Tracer

Tracer 表示一次完整的追踪链路,tracer 由一个或多个 span 组成。下图示例表示了一个由8个 span 组成的 tracer:

        [Span A]  ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]



(Span G `FollowsFrom` Span F)

时间轴的展现方式会更容易理解:

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time

[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]

我们通常通过以下方式创建一个 Tracer

otel.Tracer(tracerName)
// 或者
otel.GetTracerProvider().Tracer(tracerName)

Span

Span 是一条追踪链路中的基本组成要素,一个 span 表示一个独立的工作单元,比如可以表示一次函数调用,一次 http 请求等等。span 会记录如下基本要素:

  • 操作名称(operation name
  • 操作的开始时间和结束时间
  • K/V形式的Tags
  • K/V形式的Logs
  • SpanContext

Span 是这么多对象中使用频率最高的,因此创建 Span 也非常简便,例如:

otel.Tracer().Start(ctx, spanName, opts ...)
// 或者
otel.Tracer(tracerName).Start(ctx, spanName, opts ...)

Attributes

AttributesK/V 键值对的形式保存用户自定义标签,主要用于链路追踪结果的查询过滤。例如:http.method="GET", http.status_code=200。其中 key 值必须为字符串,value 必须是字符串,布尔型或者数值型。 span 中的 Attributes 仅自己可见,不会随着 SpanContext 传递给后续 span。 设置 Attributes 方式例如:

span.SetAttributes(
label.String("http.remote", conn.RemoteAddr().String()),
label.String("http.local", conn.LocalAddr().String()),
)

Event

EventsAttributes 类似,也是 K/V 键值对形式。与 Attributes 不同的是,Events 还会记录写入 Events 的时间,因此 Events 主要用于记录某些事件发生的时间。Eventskey 值同样必须为字符串,但对 value 类型则没有限制。例如:

span.AddEvent("http.request", trace.WithAttributes(
label.Any("http.request.header", headers),
label.String("http.request.body", bodyContent),
))

SpanContext

SpanContext 携带着一些用于跨服务通信的(跨进程)数据,主要包含:

  • 足够在系统中标识该span的信息,比如:span_id, trace_id
  • Baggage - 为整条追踪连保存跨服务(跨进程)的K/V格式的用户自定义数据。BaggageAttributes 类似,也是 K/V 键值对。与 Attributes 不同的是:
    • keyvalue 都只能是字符串格式
    • Baggage 不仅当前 span 可见,其会随着 SpanContext 传递给后续所有的子 span 。要小心谨慎的使用 Baggage - 因为在所有的span中传递这些K,V会带来不小的网络和CPU开销。

Propagator

Propagator 传播器用于端对端的数据编码/解码,例如:ClientServer 端的数据传输,TraceIdSpanIdBaggage也是需要通过传播器来管理数据传输。业务端开发者往往对Propagator无感知,只有中间件/拦截器的开发者需要知道它的作用。OpenTelemetry的标准协议实现库提供了常用的TextMapPropagator,用于常见的文本数据端到端传输。此外,为保证TextMapPropagator中的传输数据兼容性,不应当带有特殊字符,具体请参考:https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/api-propagators.md

设置示例

Propagator支持多种格式,比如:jaeger、aws的xray等

jaeger方式

// pkg/trace/tracer.go

import (
...
jaegerprop "go.opentelemetry.io/contrib/propagators/jaeger"
...
)

func InitTracerProvider(serviceName, endpoint string, options ...Option) (*tracesdk.TracerProvider, error) {
...

opts := applyOptions(options...)
tp := tracesdk.NewTracerProvider(
// set sample
tracesdk.WithSampler(tracesdk.TraceIDRatioBased(opts.SamplingRatio)),
// Always be sure to batch in production.
tracesdk.WithBatcher(exporter),
// Record information about this application in an Resource.
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
)

otel.SetTracerProvider(tp)
// 全局设置
otel.SetTextMapPropagator(jaegerprop.Jaeger{})

return tp, nil
}

xray方式

// pkg/trace/tracer.go

import (
...
"go.opentelemetry.io/contrib/propagators/aws/xray"
...
)

func InitTracerProvider(serviceName, endpoint string, options ...Option) (*tracesdk.TracerProvider, error) {
...

// A custom ID Generator to generate traceIDs that conform to
// AWS X-Ray traceID format
idg := xray.NewIDGenerator()

opts := applyOptions(options...)
tp := tracesdk.NewTracerProvider(
// set sample
tracesdk.WithSampler(tracesdk.TraceIDRatioBased(opts.SamplingRatio)),
// Always be sure to batch in production.
tracesdk.WithBatcher(exporter),
// Record information about this application in an Resource.
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
sdktrace.WithIDGenerator(idg),
)

otel.SetTracerProvider(tp)
// 全局设置
otel.SetTextMapPropagator(xray.Propagator{})

return tp, nil
}