Go2sky — Golang用skywalking实现全链路追踪

一、背景介绍

由于在微服务架构中,服务之间的调用关系多而复杂,所以有必要对它们之间的调用链路进行追踪、分析,判断是哪里出了问题,或者哪里耗时过多。

最近接到了这个需求,添加全链路追踪,所以研究并实践了一下,还不太深刻,若有错误的地方欢迎指正。

二、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. 结果展示

链路追踪:

拓扑图: