一、背景介绍
由于在微服务架构中,服务之间的调用关系多而复杂,所以有必要对它们之间的调用链路进行追踪、分析,判断是哪里出了问题,或者哪里耗时过多。
最近接到了这个需求,添加全链路追踪,所以研究并实践了一下,还不太深刻,若有错误的地方欢迎指正。
二、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端 (下游服务)
定义请求信息的结构体:
type Req struct {
A int
Header string // 添加此字段,用于传递context信息
}
定义context信息的注入方法:
func (p *Req) Set(key, value string) error {
p.Header = fmt.Sprintf("%s:%s", key, value)
return nil
}
创建reporter和tracer:
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关联起来。
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端 (上游服务)
定义请求信息的结构体:
type ReqBody struct {
A int
Header string
}
定义context信息的提取方法:
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:
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信息
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. 结果展示
链路追踪:
拓扑图: