- Created by 郭强, last modified on Mar 29, 2021
作为一款工程化完备的开发框架,GoFrame
实现了标准化的分布式链路跟踪(Distributed Tracing
)特性,通过GoFrame
开发框架开发者可以非常简便地使用Tracing
特性。
一、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
TracerProvider
主要负责创建Tracer
,一般是需要第三方的分布式链路跟踪管理平台提供具体的实现。默认情况是一个空的TracerProvider (NoopTracerProvider)
,虽然也能创建Tracer
但是内部其实不会执行具体的数据流传输逻辑。举个例子,假如使用jaeger
,往往是这么来初始化并注入jaeger
的TracerProvider
:
// InitJaeger initializes and registers jaeger to global TracerProvider. // // The output parameter `flush` 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) (flush func(), err error) { var endpointOption jaeger.EndpointOption if strings.HasPrefix(endpoint, "http") { // HTTP. endpointOption = jaeger.WithCollectorEndpoint(endpoint) } else { // UDP. endpointOption = jaeger.WithAgentEndpoint(endpoint) } return jaeger.InstallNewPipeline( endpointOption, jaeger.WithProcess(jaeger.Process{ ServiceName: serviceName, }), jaeger.WithSDK(&trace.Config{ DefaultSampler: trace.AlwaysSample(), }), ) }
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
四、使用示例
五、参考资料
- https://opentracing.io
- https://opencensus.io
- https://opentelemetry.io
- https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification
- No labels
2 Comments
智刚
"日志内容中需要注入当前请求的
TraceId
,以方便通过日志快速查找定位问题点。组件可以自动识别当前请求链路是否开启Tracing
特性,有则自动启动自身Tracing
特性,并将TraceId
自动读取出来输出到内容中;没有则忽略,什么也不会做。该特性是由glog
组件实现,这需要开发者在输出日志的时候调用Ctx
链式操作方法将context.Context
上下文变量传递到当前输出日志操作链路中,没有没有传递context.Context
上下文变量,就会丢失日志内容中的TraceId
。"这里的
TraceId
是固定的还是取配置文件中日志配置参数:CtxKeys
吗?郭强
不是。
glog
日志组件旧版本实现的一版链路跟踪信息不是标准化的。新版本会保留CtxKeys
设计,并且新版本增加了标准化协议OpenTelemetry
的支持,使得链路跟踪信息设置、获取和打印更加便捷,也就是说你无需再手动配置CtxKeys
,日志组件会自动读取链路中的TraceId
。此外,GoFrame
未来版本中也会根据需要扩展原有的CtxKeys
的能力。