Skip to main content
Version: 1.16.x

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

一、OpenTelemetry

分布式链路跟踪( Distributed Tracing )的概念最早是由 Google 提出来的,发展至今技术已经比较成熟,也是有一些协议标准可以参考。目前在 Tracing 技术这块比较有影响力的是两大开源技术框架: Netflix 公司开源的 OpenTracingGoogle 开源的 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,往往是这么来初始化并注入 jaegerTracerProvider

// 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 组成。下图示例表示了一个由 8span 组成的 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

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()),
)

Events

EventsAttributes 类似,也是 K/V 键值对形式。与 Attributes 不同的是, Events 还会记录写入 Events 的时间,因此 Events 主要用于记录某些事件发生的时间。 Eventskey 值同样必须为字符串,但对 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 格式的用户自定义数据。 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

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

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

三、支持组件

Tracing 的实施属于架构层面的事情,仅仅靠修改一两个组件是无法成效的,而是必须在统一开发框架前提下,需要一整套框架联动的事情。在 GoFrame 开发框架层面,对接的是 OpenTelemetryGo API 接口,由于 OpenTelemetryGo 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 客户端通过提供可选择的拦截器的形式注入。支持 UnaryStream 两种通信类型。该特性是由 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 服务端通过提供可选择的拦截器的形式注入。支持 UnaryStream 两种通信类型。该特性是由 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

四、使用示例

五、参考资料