
本文我们就来进行案例实战,选择当前流行的链路追踪组件 Zipkin作为示例,演示如何在 Go 微服务中集成 Zipkin。对于很多使用了Go 微服务框架的用户来说,其框架本身就拥有Trace 模块,如 Gokit。所以本文我们就在 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 支持 OpenTracingAPl,并使用opentracing-go 包为其服务器和客户端提供追踪中间件。Zipkin、LightStep 和 AppDash是已支持的追踪组件,通过OpenTracing APl 与Go-kit一起使用。
应用架构图
本文将会介绍如何在 Go-kit 中集成 Zipkin进行链路调用的追踪,包括HTTP 和gRPC 两种调用方式在具体介绍这两种调用方式之前,我们先来看一下 Go-kit 集成 Zipkin 的应用架构,如下图所示:

从架构图中可以看到:我们构建了一个服务网关,通过API网关调用具体的微服务,所有的服务都注册到Consul上;当客户端的请求到来之时,网关作为服务端的门户,会根据配置的规则,从Consul中获取对应服务的信息,并将请求反向代理到指定的服务实例。
涉及的业务服务与组件包含以下4个:
- Consul,本地安装并启动;
- Zipkin,本地安装并启动;
- APlGateWay,微服务网关;
- String Service,字符串服务,是基于 Kit 构建的,提供基本的字符串操作。
HTTP调用方式的链路追踪
关于HTTP调用方式的链路追踪,下面我们将依次构建微服务网关、业务服务,并进行结果验证。
1.API网关构建
在网关(gateway)中增加链路追踪的采集逻辑,同时在反向代理中增加追踪(tracer)设置。
Go-kit 在 tracing 包中默认添加了 Zipkin 的支持,所以集成工作会比较轻松。在开始之前,需要下载以下依赖:
Go
#zipkin官方库
go get github.com/openzipkin/zipkin-go
下面三个包都是依赖,按需下载
bash
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
作为链路追踪的"第一站"和最后一站",网关会将客户端的请求转发给对应的业务服务,并将响应的结果返回给客户端。我们需要截获到达网关的所有请求,记录追踪信息。在下面这个示例中,网关是作为外部请求的服务端,同时作为字符串服务的客户端(反向代理内部实现),其代码实现如下:
Go
// 创建环境变量
var (
// consul 环境变量省略
zipkinURL = flag.String("zipkin.url", "HTTP://1ocalhost: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监听,代码如下所示:
Go
// 创建反向代理
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,其中的 traceld将作为本次请求的唯一编号,网关必须把这个tracelD传递给字符串服务,字符串服务才能为该请求持续记录追踪信息。在 ReverseProxy 中能够完成这一任务的就是Transport,我们可以使用 zipkin-go 的 middleware/HTTP 包提供的 NewTransport替换系统默认的 HTTP.DefaultTransport。代码如下所示:
Go
// NewReverseProxy 创建反向代理处理方法
func NewReverseProxy(client *api.Client, zikkinTracer *zipkin.Tracer, logger
log.Logger) *HTTPutil.ReverseProxy {
<span class="hljs-comment">//创建 Director</span>
<span class="hljs-attr">director</span> := func(req *HTTP.Request) {
<span class="h1js-comment">//省略</span>
}
<spanclass="hljs-comment">//为反向代理增加追踪逻辑,使用如下RoundTrip代替默认
Transport</span>
roundTrip, <span class="h1js-attr">_</span> :=
zipkinHTTPsvr.NewTransport(zikkinTracer, zipkinHTTPsvr.TransportTrace(<spanclass="h1js- literal">true</span>))
<span class="h1js-keyword">return</span> & HTTPuti1.ReverseProxy{
<span class="hljs-attr">Director</span>: director,
<span class="hljs-attr">Transport</span>: roundTrip,
}
}
至此,API网关服务的搭建就完成了。
2.业务服务构建
创建追踪器与网关的处理方式一样,我们就不再描述。字符串服务对外提供了两个接口:字符串操作(/op/{type}/{a}/{b})和健康检查(/health)。定义如下:
Go
endpoint := MakeStringEndpoint(svc)
// 添加追踪,设置 span 的名称为 string-endpoint
endpoint = Kitzipkin.TraceEndpoint(zipkinTracer, "string-endpoint") (endpoint)
// 创建健康检查的Endpoint
healthEndpoint := MakeHealth CheckEndpoint(svc)
//添加追踪,设置 span 的名称为 health-endpoint
healthEndpoint = Kitzipkin.TraceEndpoint(zipkinTracer, "health-endpoint") (healthEndpoint)
Go-kit 提供了对 zipkin-go 的封装,上面的实现中,直接调用中间件TraceEndpoint 对字符串服务的两个Endpoint进行设置。
除了Endpoint,还需要追踪 Transport。可以修改 transports.go 的 MakeHTTPHandler 方法,增加参数 zipkinTracer,然后在 SerVerOption 中设置追踪参数。代码如下:
Go
// MakeHTTPHandler make HTTP handler use mux
func MakeHTTPHandler(ctx context.Context, endpoints ArithmeticEndpoints,
zipkinTracer *gozipkin.Tracer, logger log.Logger) HTTP.Handler {
r := mux.NewRouter()
<span class="hljs-attr">zipkinServer</span>
zipkin.HTTPServerTrace(zipkinTracer, zipkin.Name (<span class="hljs-
string">"HTTP-transport"</span>))
<span class="hljs-attr">options</span> := []kitHTTP.ServerOption{
KitHTTP.ServerErrorLogger(logger),
KitHTTP.SerVerErrorEncoder(KitHTTP.DefaultErrorEncoder),zipkinserver,
}
<span class="h1js-comment">// . . .</span>
<span class="hljs-keyword">return</span> r
}
至此,所有的代码修改工作已经完成,下一步就是启动测试、对结果验证了。
3.结果验证
我们可以访问 http://localhost:9090/string-Service/op/Diff/abc/bcd,查看字符串服务的请求结果,如下图所示:

可以看到,通过网关,我们可以正常访问字符串服务提供的接口。下面我们通过ZipkinU来查看本次路调用的信息,如下图所示:

在浏览器请求之后,可以在 ZipkinU中看到发送的请求记录(单击上方"Try LensUI"切换成了LensUI,效果还不错),点击查看详细的链路调用情况,如下图所示:

从调用链中可以看到,本次请求涉及两个服务:gateway-service 和 string-service。
整个链路有 3 个 Span:gateWay、HTTP-transport 和 string-endpoint,确实如我们所定义的一样。这里我们主要看一下网关中的GateWay Span 详情,如下图所示:

GateWay访问字符串服务的时候,其实是作为一个客户端建立连接并发起调用,然后等待 Server写回响应结果,最后结束客户端的调用。通过上图的展开,我们清楚地了解这次调用(Span)打的标签(tag),包括method、path 等。
gRPC调用方式的链路追踪
上面我们分析了微服务中HTTP 调用方式的链路追踪,Go-kit 中的 transport 层可以方便地切换 RPC调用方式,所以下面我们就来介绍下基于gRPC 调用方式的链路追踪。本案例的实现是在前面HTTP 调用的代码基础上进行修改,并增加测试的调用客户端。
1.定义protobuf文件
我们首先来定义 protobuf 文件及生成对应的 Go 文件。
Go
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语言文件:
Go
protoc string-proto --go_out=plugins=grpc:.
生成的 string·pb.go 可以参见Verify two-factor authentication,此处不再展开。
2.定义 gRPC Server
在字符串服务中增加gRPC serVer 的实现,并织入gRPC 链路追踪的相关代码。
Go
// 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"))
<span class="hljs-attr">handler</span> := NewGRPCServer(ctx, endpts,
serverTracer)
<span class="hljs-attr">gRPCServer</span> := grpc.NewServer()
pb.RegisterStringServiceServer(gRPCServer, handler)
errChan <- gRPCServer.Serve(listener)
}()
要增加 Trace 的中间件,其实就是在gRPC 的 SerVerOption 中追加 GRPCSerVerTrace。我们增加的通用 Span 名为:string-grpc-transport。接下来就是在endpoint 中,增加暴露接口的gRPC 实现,代码如下:
Go
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,代码如下:
Go
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 {
<span class="hljs-keyword">var</span> ep = grpctransport.Newclient(conn,
<span class="hljs-string">"pb.Stringservice"</span>,
<span class="hljs-string">"Diff"</span>,
EncodeGRPCStringRequest,<span class="hljs-comment">// 请求的编码</span>
DecodeGRPCStringResponse,<span class="hljs-comment">// 响应的解码</span>
pb.StringResponse{},<span class="hljs-comment">// 定义返回的对象</span>
clientTracer,<span class="hljs-comment">// 客户端的 GRPcclientTrace</span>
) .Endpoint)
<span class="hljs-attr">StringEp</span> := endpts.StringEndpoints{
<span class="hljs-attr">StringEndpoint</span>: ep,
}
<span class="hljs-keyword">return</span> StringEp
}
从客户端调用的定义可以看到,传入的是grpc 连接和客户端的 trace上下文。这里需要注意的是GRPCClientTrace 的初始化,测试 gRPC 调用的客户端时将会传入该参数。
4.测试gRPC调用的客户端
编写client_test.go,调用我们在前面已经定义的 client.StringDiff 方法,代码如下:
Go
//...zipkinTracer的构造省略
tr := zipkinTracer
// 设定根Span的名称
parentSpan := tr.Startspan("test")
deferparentSpan.Flush() // 写入上下文
<span class="hljs-attr">ctx</span> := zipkin.NewContext(context.Background(),parentspan)
<span class="h1js-comment">//初始化 GRPcclientTrace</span>
<span class="hljs-attr">clientTracer</span> := kitzipkin.GRPcclientTrace(tr)
conn, <span class="h1js-attr">err</span> := grpc.Dial(*grpcAddr,
grpc.withInsecure(), grpc.WithTimeout (<span class="hljs-
number"'>l</span>*time.Second))
<span class="hljs-keyword">if</span> err != nil {
fmt.Println(<span class="hljs-string">"gRPC dial err:"</span>, err)
}
defer conn.Close()
<spanclass="hljs-comment">//获取rpc 调用的endpoint,发起调用</span>
<span class="hljs-attr">svr</span> := client.StringDiff(conn, clientTracer)
result,<span class="hljs-attr">err</span> := svr.Diff(ctx, <span class="hljs-
string">"Add"</span>,<span class="h1js-string">"ppsdd"</span>)
<span class="hljs-keyword">if</span> err != nil {
fmt.Println(<span class="hljs-string">"Diff error"</span>, err.Error())
}
fmt.Println(<span class="hljs-string">"result ="</span>, result)
客户端在调用之前,我们构建了要传入的 GRPCClientTrace,作为获取rpc调用的 endpoint 的参数,设定调用的父 Span 名称,这个上下文信息会传入Zipkin服务端。调用输出的结果如下:
Go
ts=2020-9-24T15:27:06.817056z ca11er=client_test.go:51 tracer=Zipkin type=Native
URL=http://1oca1host: 9411/api/v2/spans
result = dd
测试用例的调用结果正确,我们来看一下Zipkin中记录的调用链信息。点击查看详情,可以看到本次请求涉及两个服务:test-service 和 string-service。如图所示:
