Skip to content

35案例:如何在微服务中集成Zipkin组件?

这一课时我们就来进行案例实战,选择当前流行的链路追踪组件 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 的支持,所以集成工作会比较轻松。在开始之前,需要下载以下依赖:

js
# 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

作为链路追踪的"第一站"和"最后一站",网关会将客户端的请求转发给对应的业务服务,并将响应的结果返回给客户端。我们需要截获到达网关的所有请求,记录追踪信息。在下面这个示例中,网关是作为外部请求的服务端,同时作为字符串服务的客户端(反向代理内部实现),其代码实现如下:

js
// 创建环境变量
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 监听,代码如下所示:

js
//创建反向代理
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。代码如下所示:

js
// 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)。定义如下:

js
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 中设置追踪参数。代码如下:

js
// 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 文件。

js
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 语言文件:

js
protoc string.proto --go_out=plugins=grpc:.

生成的 string.pb.go 可以参见源码,此处不再展开。

2. 定义 gRPC Server

在字符串服务中增加 gRPC server 的实现,并织入 gRPC 链路追踪的相关代码。

js
	//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 实现,代码如下:

js
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,代码如下:

js
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 方法,代码如下:

js
	//... 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 服务端。调用输出的结果如下:

js
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。如图所示: