- Created by 郭强, last modified on Mar 29, 2021
在微服务架构的系统中,请求在各服务之间流转,调用链错综复杂,一旦出现了问题和异常,很难追查定位,这个时候就需要链路追踪来帮忙了。链路追踪系统能追踪并记录请求在系统中的调用顺序,调用时间等一系列关键信息,从而帮助我们定位异常服务和发现性能瓶颈。
Opentracing
Opentracing
是分布式链路追踪的一种规范标准,是CNCF
(云原生计算基金会)下的项目之一。和一般的规范标准不同,Opentracing
不是传输协议,消息格式层面上的规范标准,而是一种语言层面上的API标准。以Go
语言为例,只要某链路追踪系统实现了Opentracing
规定的接口(interface
),符合Opentracing
定义的表现行为,那么就可以说该应用符合Opentracing
标准。这意味着开发者只需修改少量的配置代码,就可以在符合Opentracing
标准的链路追踪系统之间自由切换:https://github.com/opentracing/opentracing-go
在使用Opentracing
来实现全链路追踪前,有必要先了解一下它所定义的数据模型。
Trace
Trace
表示一次完整的追踪链路,trace
由一个或多个span
组成。下图示例表示了一个由8
个span
组成的trace
:
[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··]
示例来源:https://github.com/opentracing/specification/blob/master/specification.md#the-opentracing-data-model
Span
Span
是一条追踪链路中的基本组成要素,一个span
表示一个独立的工作单元,比如可以表示一次函数调用,一次http
请求等等。span
会记录如下基本要素:
- 服务名称(
operation name
) - 服务的开始时间和结束时间
K/V
形式的Tags
K/V
形式的Logs
SpanContext
References
:该span
对一个或多个span
的引用(通过引用SpanContext
)。
Tags
Tags
以K/V
键值对的形式保存用户自定义标签,主要用于链路追踪结果的查询过滤。例如: http.method="GET",http.status_code=200
。其中key
值必须为字符串,value
必须是字符串,布尔型或者数值型。 span
中的tag
仅自己可见,不会随着 SpanContext
传递给后续span
。 例如:
span.SetTag("http.method","GET") span.SetTag("http.status_code", 200)
Logs
Logs
与tags
类似,也是K/V
键值对形式。与tags
不同的是,logs
还会记录写入logs
的时间,因此logs
主要用于记录某些事件发生的时间。logs
的key
值同样必须为字符串,但对value
类型则没有限制。例如:
span.LogFields( log.String("event", "soft error"), log.String("type", "cache timeout"), log.Int("waited.millis", 1500), )
Opentracing
列举了一些惯用的Tags
和Logs
:https://github.com/opentracing/specification/blob/master/semantic_conventions.md
SpanContext
SpanContext
携带着一些用于跨服务通信的(跨进程)数据,主要包含:
- 足够在系统中标识该
span
的信息,比如:span_id, trace_id
。 Baggage Items
,为整条追踪连保存跨服务(跨进程)的K/V
格式的用户自定义数据。Baggage Items
与tags
类似,也是K/V
键值对。与tags
不同的是:
- 其
key
跟value
都只能是字符串格式 Baggage items
不仅当前span
可见,其会随着SpanContext
传递给后续所有的子span
。要小心谨慎的使用baggage items
- 因为在所有的span
中传递这些K,V
会带来不小的网络和CPU
开销。
- 其
References
Opentracing
定义了两种引用关系:ChildOf
和FollowFrom
。
ChildOf
: 父span
的执行依赖子span
的执行结果时,此时子span
对父span
的引用关系是ChildOf
。比如对于一次RPC
调用,服务端的span
(子span
)与客户端调用的span
(父span
)是ChildOf
关系。FollowFrom
:父span
的执不依赖子span
执行结果时,此时子span
对父span
的引用关系是FollowFrom
。FollowFrom
常用于异步调用的表示,例如消息队列中consumer span
与producer span
之间的关系。
使用示例
对Opentracing
的概念有初步了解后,下面使用Jaeger
来演示如何在程序中使用实现链路追踪。
为方便演示,以下示例为进程内部方法的链路跟踪记录,更多详细的示例可参考: Opentracing Go Tutorial
Jaeger
Jaeger\ˈyā-gər\ 是Uber开源的分布式追踪系统,是遵循Opentracing
的系统之一,也是CNCF
项目。本篇将使用Jaeger
来演示如何在系统中引入分布式追踪。
Quick Start
Jaeger
提供了all-in-one
镜像,方便我们快速开始测试:
docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 9411:9411 \ jaegertracing/all-in-one:1.14
镜像启动后,通过 http://localhost:16686 可以打开Jaeger UI
。
下载客户端library
:
go get github.com/jaegertracing/jaeger-client-go
初始化Jaeger tracer
:
import ( "context" "errors" "fmt" "io" "time" "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/log" "github.com/uber/jaeger-client-go" jaegerCfg "github.com/uber/jaeger-client-go/config" ) // initJaeger 将jaeger tracer设置为全局tracer func initJaeger(service string) io.Closer { cfg := jaegerCfg.Configuration{ // 将采样频率设置为1,每一个span都记录,方便查看测试结果 Sampler: &jaegerCfg.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &jaegerCfg.ReporterConfig{ LogSpans: true, // 将span发往jaeger-collector的服务地址 CollectorEndpoint: "http://localhost:14268/api/traces", }, } closer, err := cfg.InitGlobalTracer(service, jaegerCfg.Logger(jaeger.StdLogger)) if err != nil { panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err)) } return closer }
创建tracer
,生成root span
:
func main() { closer := initJaeger("in-process") defer closer.Close() // 获取jaeger tracer tracer := opentracing.GlobalTracer() // 创建root span span := tracer.StartSpan("in-process-service") // main执行完结束这个span defer span.Finish() // 将span传递给Foo ctx := opentracing.ContextWithSpan(context.Background(), span) Foo(ctx) }
上述代码创建了一个root span
,并将该span
通过context
传递给Foo
方法,以便在Foo
方法中将追踪链继续延续下去:
func Foo(ctx context.Context) { // 开始一个span, 设置span的operation_name为Foo span, ctx := opentracing.StartSpanFromContext(ctx, "Foo") defer span.Finish() // 将context传递给Bar Bar(ctx) // 模拟执行耗时 time.Sleep(1 * time.Second) } func Bar(ctx context.Context) { // 开始一个span,设置span的operation_name为Bar span, ctx := opentracing.StartSpanFromContext(ctx, "Bar") defer span.Finish() // 模拟执行耗时 time.Sleep(2 * time.Second) // 假设Bar发生了某些错误 err := errors.New("something wrong") span.LogFields( log.String("event", "error"), log.String("message", err.Error()), ) span.SetTag("error", true) }
Foo
方法调用了Bar
,假设在Bar
中发生了一些错误,可以通过span.LogFields
和span.SetTag
将错误记录在追踪链中。 通过上面的例子可以发现,如果要确保追踪链在程序中不断开,需要将函数的第一个参数设置为context.Context
,通过opentracing.ContextWithSpan
将保存到context
中,通过opentracing.StartSpanFromContext
开始一个新的子span
。
效果查看
执行完上面的程序后,打开Jaeger UI
: http://localhost:16686/search,可以看到链路追踪的结果:
点击详情可以查看具体信息:
通过链路追踪系统,我们可以方便的掌握链路中各span
的调用顺序,调用关系,执行时间轴,以及记录一些tag
和log
信息,极大的方便我们定位系统中的异常和发现性能瓶颈。
其他参考
- No labels