这一课时我们就来进行案例实战,选择当前流行的链路追踪组件 Zipkin 作为示例,演示如何在 Go 微服务中集成 Zipkin。对于很多使用了 Go 微服务框架的用户来说,其框架本身就拥有 Trace 模块,如 Go-kit。所以本课时我们就在 Go-kit 微服务的案例中集成 Zipkin。
Zipkin 社区提供了诸如 zipkin-go、zipkin-go-opentracing、go-zipkin 等 Go 客户端库,后面我们会介绍如何将其中的 zipkin-go-opentracing(组件地址参见 https://github.com/openzipkin-contrib/zipkin-go-opentracing)集成到微服务中并加以应用。
Go-kit 微服务框架的 tracing 包为服务提供了 Dapper 样式的请求追踪。Go-kit 支持 OpenTracing API,并使用 opentracing-go 包为其服务器和客户端提供追踪中间件。Zipkin、LightStep 和 AppDash 是已支持的追踪组件,通过 OpenTracing API 与 Go-kit 一起使用。
应用架构图
本课时将会介绍如何在 Go-kit 中集成 Zipkin 进行链路调用的追踪,包括HTTP 和 gRPC 两种调用方式。在具体介绍这两种调用方式之前,我们先来看一下 Go-kit 集成 Zipkin 的应用架构,如下图所示:
Go-kit 集成 Zipkin 的应用架构图
从架构图中可以看到:我们构建了一个服务网关,通过 API 网关调用具体的微服务,所有的服务都注册到 Consul 上;当客户端的请求到来之时,网关作为服务端的门户,会根据配置的规则,从 Consul 中获取对应服务的信息,并将请求反向代理到指定的服务实例。
涉及的业务服务与组件包含以下 4 个:
Consul,本地安装并启动;
Zipkin,本地安装并启动;
API Gateway,微服务网关;
String Service,字符串服务,是基于 Kit 构建的,提供基本的字符串操作。
HTTP 调用方式的链路追踪
关于 HTTP 调用方式的链路追踪,下面我们将依次构建微服务网关、业务服务,并进行结果验证。
1. API 网关构建
在网关(gateway)中增加链路追踪的采集逻辑,同时在反向代理中增加追踪(tracer)设置。
Go-kit 在 tracing 包中默认添加了 Zipkin 的支持,所以集成工作会比较轻松。在开始之前,需要下载以下依赖:
1
2
3
4
5
6
7
8
9
|
# zipkin 官方库
go get github.com/openzipkin/zipkin-go
# 下面三个包都是依赖,按需下载
git clone https://github.com/googleapis/googleapis.git [your GOPATH]/ src/google.golang.org/genproto
git clone https://github.com/grpc/grpc-go.git [your GOPATH]/src/google. golang.org/grpc
git clone https://github.com/golang/text.git [your GOPATH]/src/golang. org/text
|
作为链路追踪的“第一站”和“最后一站”,网关会将客户端的请求转发给对应的业务服务,并将响应的结果返回给客户端。我们需要截获到达网关的所有请求,记录追踪信息。在下面这个示例中,网关是作为外部请求的服务端,同时作为字符串服务的客户端(反向代理内部实现),其代码实现如下:
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
|
// 创建环境变量
var (
// consul 环境变量省略
zipkinURL = flag.String("zipkin.url", "HTTP://localhost:9411/api/ v2/spans", "Zipkin server url")
)
flag.Parse()
var zipkinTracer *zipkin.Tracer
{
var (
err error
hostPort = "localhost:9090"
serviceName = "gateway-service"
useNoopTracer = (*zipkinURL == "")
reporter = zipkinHTTP.NewReporter(*zipkinURL)
) // zipkin 相关的配置变量
defer reporter.Close()
zEP, _ := zipkin.NewEndpoint(serviceName, hostPort)
// 构建 zipkinTracer
zipkinTracer, err = zipkin.NewTracer(
reporter, zipkin.WithLocalEndpoint(zEP), zipkin.WithNoopTracer (useNoopTracer),
)
if err != nil {
logger.Log("err", err)
os.Exit(1)
}
if !useNoopTracer {
logger.Log("tracer", "Zipkin", "type", "Native", "URL", *zipkinURL)
}
}
|
我们使用的传输方式为 HTTP,可以使用 zipkin-go 提供的 middleware/HTTP 包,它采用装饰者模式把我们的 HTTP.Handler 进行封装,然后启动 HTTP 监听,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//创建反向代理
proxy := NewReverseProxy(consulClient, zipkinTracer, logger)
tags := map[string]string{
"component": "gateway_server",
}
handler := zipkinHTTPsvr.NewServerMiddleware(
zipkinTracer,
zipkinHTTPsvr.SpanName("gateway"),
zipkinHTTPsvr.TagResponseSize(true),
zipkinHTTPsvr.ServerTags(tags),
)(proxy)
|
网关接收请求后,会创建一个 Span,其中的 traceId 将作为本次请求的唯一编号,网关必须把这个 traceID 传递给字符串服务,字符串服务才能为该请求持续记录追踪信息。在 ReverseProxy 中能够完成这一任务的就是 Transport,我们可以使用 zipkin-go 的 middleware/HTTP 包提供的 NewTransport 替换系统默认的 HTTP.DefaultTransport。代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// NewReverseProxy 创建反向代理处理方法
func NewReverseProxy(client *api.Client, zikkinTracer *zipkin.Tracer, logger log.Logger) *HTTPutil.ReverseProxy {
//创建 Director
director := func(req *HTTP.Request) {
//省略
}
// 为反向代理增加追踪逻辑,使用如下 RoundTrip 代替默认 Transport
roundTrip, _ := zipkinHTTPsvr.NewTransport(zikkinTracer, zipkinHTTPsvr.TransportTrace(true))
return &HTTPutil.ReverseProxy{
Director: director,
Transport: roundTrip,
}
}
|
至此,API 网关服务的搭建就完成了。
2. 业务服务构建
创建追踪器与网关的处理方式一样,我们就不再描述。字符串服务对外提供了两个接口:字符串操作(/op/{type}/{a}/{b})和健康检查(/health)。定义如下:
1
2
3
4
5
6
7
8
9
|
endpoint := MakeStringEndpoint(svc)
//添加追踪,设置 span 的名称为 string-endpoint
endpoint = Kitzipkin.TraceEndpoint(zipkinTracer, "string-endpoint") (endpoint)
//创建健康检查的 Endpoint
healthEndpoint := MakeHealthCheckEndpoint(svc)
//添加追踪,设置 span 的名称为 health-endpoint
healthEndpoint = Kitzipkin.TraceEndpoint(zipkinTracer, "health-endpoint") (healthEndpoint)
|
Go-kit 提供了对 zipkin-go 的封装,上面的实现中,直接调用中间件 TraceEndpoint 对字符串服务的两个 Endpoint 进行设置。
除了 Endpoint,还需要追踪 Transport。可以修改 transports.go 的 MakeHTTPHandler 方法,增加参数 zipkinTracer,然后在 ServerOption 中设置追踪参数。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// MakeHTTPHandler make HTTP handler use mux
func MakeHTTPHandler(ctx context.Context, endpoints ArithmeticEndpoints, zipkinTracer *gozipkin.Tracer, logger log.Logger) HTTP.Handler {
r := mux.NewRouter()
zipkinServer := zipkin.HTTPServerTrace(zipkinTracer, zipkin.Name ("HTTP-transport"))
options := []KitHTTP.ServerOption{
KitHTTP.ServerErrorLogger(logger),
KitHTTP.ServerErrorEncoder(KitHTTP.DefaultErrorEncoder),
zipkinServer,
}
// ...
return r
}
|
至此,所有的代码修改工作已经完成,下一步就是启动测试、对结果验证了。
3. 结果验证
我们可以访问 http://localhost:9090/string-service/op/Diff/abc/bcd,查看字符串服务的请求结果,如下图所示:
结果验证截图
可以看到,通过网关,我们可以正常访问字符串服务提供的接口。下面我们通过 Zipkin UI 来查看本次链路调用的信息,如下图所示:
Zipkin UI 查看链路调用的信息截图
在浏览器请求之后,可以在 Zipkin UI 中看到发送的请求记录(单击上方“Try Lens UI”切换成了 Lens UI,效果还不错),点击查看详细的链路调用情况,如下图所示:
Lens UI 截图
从调用链中可以看到,本次请求涉及两个服务:gateway-service 和 string-service。
整个链路有 3 个 Span:gateway、HTTP-transport 和 string-endpoint,确实如我们所定义的一样。这里我们主要看一下网关中的 Gateway Span 详情,如下图所示:
Gateway Span 详情截图
Gateway 访问字符串服务的时候,其实是作为一个客户端建立连接并发起调用,然后等待 Server 写回响应结果,最后结束客户端的调用。通过上图的展开,我们清楚地了解这次调用(Span)打的标签(tag),包括 method、path 等。
gRPC 调用方式的链路追踪
上面我们分析了微服务中 HTTP 调用方式的链路追踪,Go-kit 中的 transport 层可以方便地切换 RPC 调用方式,所以下面我们就来介绍下基于 gRPC 调用方式的链路追踪。本案例的实现是在前面HTTP 调用的代码基础上进行修改,并增加测试的调用客户端。
1. 定义 protobuf 文件
我们首先来定义 protobuf 文件及生成对应的 Go 文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
syntax = "proto3";
package pb;
service StringService{
rpc Diff(StringRequest) returns (StringResponse){}
}
message StringRequest {
string request_type = 1;
string a = 2;
string b = 3;
}
message StringResponse {
string result = 1;
string err = 2;
}
|
这里提供了字符串服务中的 Diff 方法,客户端通过 gRPC 调用字符串服务。使用 proto 工具生成对应的 Go 语言文件:
1
|
protoc string.proto --go_out=plugins=grpc:.
|
生成的 string.pb.go 可以参见源码,此处不再展开。
2. 定义 gRPC Server
在字符串服务中增加 gRPC server 的实现,并织入 gRPC 链路追踪的相关代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//grpc server
go func() {
fmt.Println("grpc Server start at port" + *grpcAddr)
listener, err := net.Listen("tcp", *grpcAddr)
if err != nil {
errChan <- err
return
}
serverTracer := kitzipkin.GRPCServerTrace(zipkinTracer, kitzipkin.Name("string-grpc-transport"))
handler := NewGRPCServer(ctx, endpts, serverTracer)
gRPCServer := grpc.NewServer()
pb.RegisterStringServiceServer(gRPCServer, handler)
errChan <- gRPCServer.Serve(listener)
}()
|
要增加 Trace 的中间件,其实就是在 gRPC 的 ServerOption 中追加 GRPCServerTrace。我们增加的通用 Span 名为:string-grpc-transport。接下来就是在 endpoint 中,增加暴露接口的 gRPC 实现,代码如下:
1
2
3
4
5
6
7
8
9
|
func (se StringEndpoints) Diff(ctx context.Context, a, b string) (string, error) {
resp, err := se.StringEndpoint(ctx, StringRequest{
RequestType: "Diff",
A: a,
B: b,
})
response := resp.(StringResponse)
return response.Result, err
}
|
在构造 StringRequest 时,我们根据调用的 Diff 方法,指定了请求参数为“Diff”,下面即可定义 RPC 调用的客户端。
3. 定义服务 gRPC 调用的客户端
字符串服务提供对外的客户端调用,定义方法名为 StringDiff,返回 StringEndpoint,代码如下:
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
|
import (
grpctransport "github.com/go-kit/kit/transport/grpc"
kitgrpc "github.com/go-kit/kit/transport/grpc"
"github.com/longjoy/micro-go-course/section35/zipkin-kit/pb"
endpts "github.com/longjoy/micro-go-course/section35/zipkin-kit/string-service/endpoint"
"github.com/longjoy/micro-go-course/section35/zipkin-kit/string-service/service"
"google.golang.org/grpc"
)
func StringDiff(conn *grpc.ClientConn, clientTracer kitgrpc.ClientOption) service.Service {
var ep = grpctransport.NewClient(conn,
"pb.StringService",
"Diff",
EncodeGRPCStringRequest, // 请求的编码
DecodeGRPCStringResponse, // 响应的解码
pb.StringResponse{}, //定义返回的对象
clientTracer, //客户端的 GRPCClientTrace
).Endpoint()
StringEp := endpts.StringEndpoints{
StringEndpoint: ep,
}
return StringEp
}
|
从客户端调用的定义可以看到,传入的是 grpc 连接和客户端的 trace 上下文。这里需要注意的是 GRPCClientTrace 的初始化,测试 gRPC 调用的客户端时将会传入该参数。
4. 测试 gRPC 调用的客户端
编写 client_test.go,调用我们在前面已经定义的 client.StringDiff 方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//... zipkinTracer 的构造省略
tr := zipkinTracer
// 设定根 Span 的名称
parentSpan := tr.StartSpan("test")
defer parentSpan.Flush() // 写入上下文
ctx := zipkin.NewContext(context.Background(), parentSpan)
//初始化 GRPCClientTrace
clientTracer := kitzipkin.GRPCClientTrace(tr)
conn, err := grpc.Dial(*grpcAddr, grpc.WithInsecure(), grpc.WithTimeout (1*time.Second))
if err != nil {
fmt.Println("gRPC dial err:", err)
}
defer conn.Close()
// 获取 rpc 调用的 endpoint,发起调用
svr := client.StringDiff(conn, clientTracer)
result, err := svr.Diff(ctx, "Add", "ppsdd")
if err != nil {
fmt.Println("Diff error", err.Error())
}
fmt.Println("result =", result)
|
客户端在调用之前,我们构建了要传入的 GRPCClientTrace,作为获取 rpc 调用的 endpoint 的参数,设定调用的父 Span 名称,这个上下文信息会传入 Zipkin 服务端。调用输出的结果如下:
1
2
|
ts=2020-9-24T15:27:06.817056Z caller=client_test.go:51 tracer=Zipkin type=Native URL=http://localhost:9411/api/v2/spans
result = dd
|
测试用例的调用结果正确,我们来看一下 Zipkin 中记录的调用链信息。点击查看详情,可以看到本次请求涉及两个服务:test-service 和 string-service。如图所示:
-– ### 精选评论