一、背景介绍
由于在微服务架构中,服务之间的调用关系多而复杂,所以有必要对它们之间的调用链路进行追踪、分析,判断是哪里出了问题,或者哪里耗时过多。
最近接到了这个需求,添加全链路追踪,所以研究并实践了一下,还不太深刻,若有错误的地方欢迎指正。
二、OpenTracing相关概念介绍
首先,要实现全链路追踪,必须先理解OpenTracing的一些基本概念。OpenTracing为分布式链路追踪制定了一个统一的标准。只要是按照此标准实现的服务,就能够完整的进行分布式追踪。
1. Span
Span可以被翻译为跨度,可以理解为一次方法调用,一个程序块的调用,或者一次RPC/数据库访问。
Span之间是有关系的,child of 和 follow of。比如一次RPC的调用,RPC客户端和服务端的span就形成了父子关系。
2. Trace
Trace表示一个调用链,比如在分布式服务中,一个客户端的请求,在后台可能经过了层层的调用,那么每一次调用就相当于一个span,而这一整条调用链路,可以理解成一个trace。
Trace有一个全局唯一的ID。
三、Go2sky简介
Go2sky是Golang提供给开发者实现SkyWalking agent探针的包,可以通过它来实现向SkyWalking Collector上报数据。
快速入门:GitHub-Go2Sky
1. 创建Reporter、Tracer
SkyWalking支持http和gRpc两种方式收集数据,在Go2sky中,想要上报数据,先创建一个GRPCReporter.
Tracer代表了本程序中的一条调用链路。
本程序中的所有span都会与服务名为example的服务相关联。
2. 创建Span
Span有三种类型:LocalSpan、EntrySpan、ExitSpan。
LocalSpan:可以用来表示本程序内的一次调用。
EntrySpan:用来从下游服务提取context信息。
ExitSpan: 用来向上游服务注入context信息。
在创建span时,上下文参数传入context.Backround() ,就表示它是root span。
3. 创建sub span
在创建LocalSpan和EntrySpan的时候,返回值会返回一个context信息(ctx),通过它来创建sub span,来与root span形成父子关系。
4. End Span
必须要确保结束span,它们才可以被上传给skywalking。
5. 关联Span
我们在程序中创建的span,是怎么关联起来形成一个调用链的呢。
在同一个程序中,向上面那样,创建root span 和 sub span即可。
在不同的程序中,下游服务使用ExitSpan向上游注入context信息,上游服务使用EntrySpan从下游提取context信息。Entry和Exit使得skywalking可以分析,从而生成拓扑图和度量指标。
四、实战 -- 跨程序追踪RPC调用
看到这里,有了基本的概念,以及Go2sky的基本用法,但是仍然不能够对RPC进行有效的追踪。
因为上图中的例子使用的是http请求,它本身就封装了Get和Set方法,可以很轻松的注入和提取context信息。但是RPC请求并没有,想要追踪别的类型跨程序的调用也没有。
所以我们要自己将context信息在进行调用的时候,从下游服务传给上游服务,然后自己定义注入和提取的方法。
下面只贴出了链路追踪部分的代码,其它的比如rpc相关的部分代码省略了(不然又臭又长,还难看)。
1. Client端 (下游服务)
定义请求信息的结构体:
1 2 3 4 | type Req struct { A int Header string // 添加此字段,用于传递context信息 } |
定义context信息的注入方法:
1 2 3 4 | func (p *Req) Set(key, value string) error { p.Header = fmt.Sprintf("%s:%s", key, value) return nil } |
创建reporter和tracer:
1 2 3 4 5 6 7 8 9 10 11 12 13 | r, err = reporter.NewGRPCReporter("192.168.204.130:11800") if err != nil { logs.Info("[New GRPC Reporter Error]: [%v]", err) return } // 这个程序中所有的span都会跟服务名叫RTS_Test的服务关联起来 tracer, err = go2sky.NewTracer("RTS_Test", go2sky.WithReporter(r), go2sky.WithInstance("RTS_Test_1")) if err != nil { logs.Info("[New Tracer Error]: [%v]", err) return } tracer.WaitUntilRegister() |
rpc调用以及创建span:
在创建ExitSpan的时候,传入了一个函数,函数实现就是我们定义的如何注入context信息的函数。
它会在CreateExitSpan()函数的内部被调用,header的值不需要我们管,它在CreateExitSpan函数内部生成的。我们只需要负责在上游服务中把它提取出来即可。
我目前的理解是,只需要在下游服务中负责把这个header按一定规则拼接,传给上游服务,然后在上游服务中按照规则将header解析出来,skywalking通过分析,即可将上下游的span关联起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | func OnSnapshot() { // client := GetClinet() // 表示收到客户端请求,因为只追踪后台服务之间的链路,所以这里不需要提取context信息 span2, ctx, err := tracer.CreateEntrySpan(context.Background(), "/API/Snapshot", func() (string, error){ return "", nil }) if err != nil { logs.Info("[Create Exit Span Error]: [%v]", err) return } span2.SetComponent(5200) // 表示rpc调用的span,这里需要向上游服务注入context信息,即参数中的header req := Req{3, ""} span1, err := tracer.CreateExitSpan(ctx, "/Service/OnSnapshot", "RTS_Server", func(header string) error{ return req.Set(propagation.Header, header) }) if err != nil { logs.Info("[Create Exit Span Error]: [%v]", err) return } span1.SetComponent(5200) // Golang程序使用范围是[5000, 6000),还要在skywalking中配置,config目录下的component-libraries.yml文件 var res Res // rpc调用 err = conn.Call("Req.Snapshot", req, &res) if err != nil { logs.Info("[RPC Call Snapshot Error]: [%v]", err) return } else { logs.Info("[RPC Call Snapshot Success]: [%s]", res) } span1.End() span2.End() // 一定要确保span被结束 // s1 := ReportedSpan(span1) // s2 := ReportedSpan(span2) // spans := []go2sky.ReportedSpan{s1, s2} // r.Send(spans) } |
2. Server端 (上游服务)
定义请求信息的结构体:
1 2 3 4 | type ReqBody struct { A int Header string } |
定义context信息的提取方法:
1 2 3 4 5 6 7 8 | func (p *ReqBody) Get(key string) string { subs := strings.Split(p.Header, ":") if len(subs) != 2 || subs[0] != key { return "" } return subs[1] } |
创建reporter和tracer:
1 2 3 4 5 6 7 8 9 10 11 12 | r, err = reporter.NewGRPCReporter("192.168.204.130:11800") if err != nil { logs.Info("[New GRPC Reporter Error]: [%v]\n", err) return } tracer, err = go2sky.NewTracer("Service_Test", go2sky.WithReporter(r), go2sky.WithInstance("Service_Test_1")) if err != nil { logs.Info("[New Tracer Error]: [%v]\n", err) return } tracer.WaitUntilRegister() |
创建span:
在创建EntrySpan时,调用Get()方法提取context信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | func (p *Req)Snapshot(req ReqBody, res *Res) error { // 表示收到 rpc 客户端的请求,这里需要提取context信息 span1, ctx, err := tracer.CreateEntrySpan(context.Background(), "/Service/OnSnapshot/QueringSnapshot", func() (string, error){ return req.Get(propagation.Header), nil }) if err != nil { logs.Info("[Create Exit Span Error]: [%v]\n", err) return err } span1.SetComponent(5200) // span1.SetPeer("Service_Test") // 表示去请求了一次数据库 span2, err := tracer.CreateExitSpan(ctx, "/database/QuerySnapshot", "APIService", func(header string) error { return nil }) span2.SetComponent(5200) time.Sleep(time.Millisecond * 6) *res = "Return Snapshot Info" span2.End() span1.End() // s1 := ReportedSpan(span1) // s2 := ReportedSpan(span2) // spans := []go2sky.ReportedSpan{s1, s2} // r.Send(spans) return nil } |
3. 结果展示
链路追踪:
拓扑图: