「码动四季·开源同行」go实战案例:如何在微服务中集成 Zipkin 组件?

本文我们就来进行案例实战,选择当前流行的链路追踪组件 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> &amp; 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 &lt;- 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。如图所示:

相关推荐
小陈工2 小时前
2026年4月3日技术资讯洞察:微服务理性回归、AI代码生成争议与开源安全新挑战
开发语言·数据库·人工智能·python·安全·微服务·回归
蚂蚁数据AntData2 小时前
DB-GPT V0.8.0 版本更新|范式跃迁:AI + Data 驱动的数据分析交互体验升级
大数据·数据库·人工智能·数据分析·开源
二宝1522 小时前
互联网大厂Java面试实战演练:谢飞机的三轮提问与深入解析
java·spring boot·redis·微服务·面试·kafka·oauth2
孤岛站岗2 小时前
【AI大模型入门】B02:Stable Diffusion——开源绘图,让AI绘画飞入寻常百姓家
人工智能·stable diffusion·开源
牛奔3 小时前
g:Go 版本管理器安装与使用指南
开发语言·后端·golang
cch891813 小时前
汇编与Go:底层到高层的编程差异
java·汇编·golang
Allen_LVyingbo14 小时前
斯坦福HAI官网完整版《2025 AI Index Report》全面解读
人工智能·数学建模·开源·云计算·知识图谱
一匹电信狗15 小时前
【Linux我做主】进程程序替换和exec函数族
linux·运维·服务器·c++·ubuntu·小程序·开源
s1mple“”17 小时前
大厂Java面试实录:从Spring Boot到AI技术的电商场景深度解析
spring boot·redis·微服务·kafka·向量数据库·java面试·ai技术