跳到主要内容
版本:1.16.x

作为一款工程化完备的开发框架, GoFrame 实现了标准化的分布式链路跟踪( Distributed Tracing)特性,通过 GoFrame 开发框架开发者可以非常简便地使用 Tracing 特性。

一、OpenTelemetry​

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

  1. OpenTracing介绍
  2. OpenTelemetry介绍

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

  1. https://github.com/open-telemetry/opentelemetry-go
  2. 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

TracerProvider​

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

// InitJaeger initializes and registers jaeger to global TracerProvider.
//
// The output parameter `tp` is used for waiting exported trace spans to be uploaded,
// which is useful if your program is ending and you do not want to lose recent spans.
func InitJaeger(serviceName, endpoint string) (tp *trace.TracerProvider, err error) {
var endpointOption jaeger.EndpointOption
if strings.HasPrefix(endpoint, "http") {
// HTTP.
endpointOption = jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(endpoint))
} else {
// UDP.
endpointOption = jaeger.WithAgentEndpoint(jaeger.WithAgentHost(endpoint))
}

// Create the Jaeger exporter
exp, err := jaeger.New(endpointOption)
if err != nil {
return nil, err
}
tp = trace.NewTracerProvider(
// Always be sure to batch in production.
trace.WithBatcher(exp),
// Record information about this application in an Resource.
trace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}

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)
// 或者
gtrace.NewTracer(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 ...)
// 或者
gtrace.NewSpan(ctx, spanName, opts...)

Attributes​

Attributes 以 K/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()),
)

Events​

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

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

SpanContext​

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

  • 足够在系统中标识该 span 的信息,比如: span_id, trace_id。

  • Baggage - 为整条追踪连保存跨服务(跨进程)的 K/V 格式的用户自定义数据。 Baggage 与 Attributes 类似,也是 K/V 键值对。与 Attributes 不同的是:

  • 其 key 跟 value 都只能是字符串格式

  • Baggage 不仅当前 span 可见,其会随着 SpanContext 传递给后续所有的子 span。要小心谨慎的使用 Baggage - 因为在所有的 span 中传递这些 K,V 会带来不小的网络和 CPU 开销。

Propagator​

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

GoFrame 框架通过 gtrace 模块使用了以下传播器对象,并全局设置到了 OpenTelemetry 中:

// defaultTextMapPropagator is the default propagator for context propagation between peers.
defaultTextMapPropagator = propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)

三、支持组件​

Tracing 的实施属于架构层面的事情,仅仅靠修改一两个组件是无法成效的,而是必须在统一开发框架前提下,需要一整套框架联动的事情。在 GoFrame 开发框架层面,对接的是 OpenTelemetry 的 Go API 接口,由于 OpenTelemetry 的 Go API 只是标准协议的接口层,并无具体的业务逻辑实现,因此在没有实例化注入具体的 TracerProvider 的情况下,不会对执行性能造成影响。 GoFrame 大部分组件会自动检测是否开启 Tracing,没有开启 Tracing 特性的情况下组件什么都不会做。部分组件需要开发者手动注入 Tracing 拦截器来启用 Tracing 特性(如 HTTP/gRPC 请求拦截器)。

Http Client​

HTTP 客户端通过提供可选择的 拦截器 的形式注入和启用 Tracing 特性。

该特性需要 HTTP 客户端拦截器功能支持,拦截器定义:

// MiddlewareClientTracing is a client middleware that enables tracing feature using standards of OpenTelemetry.
func MiddlewareClientTracing(c *Client, r *http.Request) (*ClientResponse, error)

使用方式,通过 Use 方法设置客户端拦截器即可:

client := g.Client().Use(ghttp.MiddlewareClientTracing)

具体使用示例请参考后续示例章节。

开发者也可以给 HTTP Client 定义和注入自定义的 Tracing 拦截器哦。

Http Server​

HTTP 服务端通过提供可选择的 拦截器/中间件 的形式注入和启用 Tracing 特性。

拦截器定义:

// MiddlewareServerTracing is a serer middleware that enables tracing feature using standards of OpenTelemetry.
func MiddlewareServerTracing(r *Request)

使用方式,通过 Use 方法设置服务端中间件即可:

s := g.Server()
s.Group("/", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareServerTracing)
// ...
})

具体使用示例请参考后续示例章节。

开发者也可以给 HTTP Server 定义和注入自定义的 Tracing 拦截器哦。

gRPC Client​

gRPC 客户端通过提供可选择的拦截器的形式注入。支持 Unary 和 Stream 两种通信类型。该特性是由 Katyusha 微服务框架实现,通过手动添加以下 gRPC 拦截器启用客户端的 Tracing 特性。

// UnaryTracing is an unary interceptor for adding tracing feature for gRPC client using OpenTelemetry.
func (c *krpcClient) UnaryTracing(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error

// StreamTracing is a stream interceptor for adding tracing feature for gRPC client using OpenTelemetry.
func (c *krpcClient) StreamTracing(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, callOpts ...grpc.CallOption) (grpc.ClientStream, error)

使用示例:

grpcClientOptions := make([]grpc.DialOption, 0)
grpcClientOptions = append(
grpcClientOptions,
grpc.WithInsecure(),
grpc.WithBlock(),
grpc.WithChainUnaryInterceptor(
krpc.Client.UnaryTracing,
),
)

conn, err := grpc.Dial(":8000", grpcClientOptions...)
// ...

gRPC Server​

gRPC 服务端通过提供可选择的拦截器的形式注入。支持 Unary 和 Stream 两种通信类型。该特性是由 Katyusha 微服务框架实现,通过手动添加以下 gRPC 拦截器启用服务端的 Tracing 特性。

// UnaryTracing is an unary interceptor for adding tracing feature for gRPC server using OpenTelemetry.
func (s *krpcServer) UnaryTracing(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error)

// StreamTracing is a stream unary interceptor for adding tracing feature for gRPC server using OpenTelemetry.
func (s *krpcServer) StreamTracing(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error

使用示例:

s := grpc.NewServer(
grpc.ChainUnaryInterceptor(
krpc.Server.UnaryTracing,
),
)

Logging​

日志内容中需要注入当前请求的 TraceId,以方便通过日志快速查找定位问题点。组件可以自动识别当前请求链路是否开启 Tracing 特性,有则自动启动自身 Tracing 特性,并将 TraceId 自动读取出来输出到内容中;没有则忽略,什么也不会做。该特性是由 glog 组件实现,这需要开发者在输出日志的时候调用 Ctx 链式操作方法将 context.Context 上下文变量传递到当前输出日志操作链路中,没有传递 context.Context 上下文变量,就会丢失日志内容中的 TraceId。

Orm​

数据库的执行是很重要的链路环节, Orm 组件需要将自身的执行情况投递到链路中,作为执行链路的一部分。组件可以自动识别当前请求链路是否开启 Tracing 特性,有则自动启动自身 Tracing 特性,没有则忽略。

Redis​

Redis 的执行也是很重要的链路环节, Redis 需要将自身的执行情况投递到链路中,作为执行链路的一部分。组件可以自动识别当前请求链路是否开启 Tracing 特性,有则自动启动自身 Tracing 特性,没有则忽略。

Utils​

对于 Tracing 特性的管理需要做一定的封装,主要考虑的是可扩展性和易用性两方面。该封装由 gtrace 模块实现,文档地址: https://godoc.org/github.com/gogf/gf/net/gtrace

四、使用示例​

五、参考资料​