1. 背景
新业务线带来新项目启动,需要改进原有项目的基础框架和组件能力,以提升后续开发和维护效率。项目搭建主要包括技术选型、框架搭建、基础服务搭建等。这其中就涉及到链路追踪的内容,结合其中的踩坑情况,用一篇文章来说明完整的链路搭建过程。
2. 技术选型
2.1 方案对比
图【1】来自网络,请自行对比验证
图 1
2.2 选型
本项目基于golang
和gin
框架,以及链路中间件对比,选择jaeger
作为工具进行集成。
3. 核心实现
3.1 jaeger服务搭建
这里就借助网上的all-in-one的docker方式搭建即可,无需多费力。
docker run --rm --name jaeger \-e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \-p 6831:6831/udp \-p 6832:6832/udp \-p 5778:5778 \-p 16686:16686 \-p 4317:4317 \-p 4318:4318 \-p 14250:14250 \-p 14268:14268 \-p 14269:14269 \-p 9411:9411 \jaegertracing/all-in-one:1.60
这里启动了很多端口,详细的自查即可,这里两个比较重要的端口如下:
16686:服务前端端口,ip:port 可访问UI
6831:项目中连接端口
jaeger界面图【2】所示。
图 2
3.2 protoc编译器及语言插件安装
protobuf
协议,是一种跨语言、跨平台的数据结构序列化和反序列化框架,类似于thrift
协议
1 编写 .proto ⽂件,⽬的是为了定义结构对象(message)及属性内容。
2 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中。
3 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进行设置和获取,和对 message 对象进行序列化和反序列化
安装
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
编写proto文件
syntax = "proto3";option go_package = "./proto";
package helloworld;service Greeter {rpc SayHello (HelloReq) returns (HelloResp);
}message HelloReq {string name = 1;
}message HelloResp {string message = 1;
}
编译proto
protoc --go_out ../ helloword.proto
3.2 项目实现
项目架构是上层服务http
,下层服务rpc
,选用protobuf
协议(跨语言、跨平台的数据结构序列化和反序列化框架,类似于thrift
协议)作为就以简单的helloword为例实现。
核心插件jaeger、opentracing和otgrpc
在上层增加中间件并注册到engine上。
package middlewareimport ("github.com/gin-gonic/gin""github.com/opentracing/opentracing-go""github.com/uber/jaeger-client-go"jaegercfg "github.com/uber/jaeger-client-go/config"
)func Trace() gin.HandlerFunc {return func(ctx *gin.Context) {cfg := jaegercfg.Configuration{ServiceName: "demo", //服务名称Sampler: &jaegercfg.SamplerConfig{Type: jaeger.SamplerTypeConst,Param: 1,},Reporter: &jaegercfg.ReporterConfig{LogSpans: true,LocalAgentHostPort: "xxxx:6831", //jaeger 服务器ip},}tracer, closer, err := cfg.NewTracer()if err != nil {panic(err)}opentracing.SetGlobalTracer(tracer)defer closer.Close()startSpan := tracer.StartSpan(ctx.Request.URL.Path)defer startSpan.Finish()ctx.Set("tracer", tracer)ctx.Set("parentSpan", startSpan)ctx.Next()}
}
// rpc调用设置globaltracer
consul := ServerConfig.Consul
conn, err := grpc.Dial(fmt.Sprintf("consul://%s:%s/%s?wait=14s", consul.Host, consul.Port, ServerName),grpc.WithTransportCredentials(insecure.NewCredentials()),grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`),grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(opentracing.GlobalTracer())), //这里把全局tracer传下去
)
// 注册路由中间件
engine.Use(middleware.Trace())
启动项目访问就可以在jaeger上看到链路信息。
图 3
点击进入详情页查看信息,这时候大家发现一个问题,这里只有http层的调用信息,没有rpc层,这也是网上所有介绍的文档里没有说明白的一点。
图 4
真正原因是中间件代码里的tracer和parentSpan没有传递下去。
startSpan := tracer.StartSpan(ctx.Request.URL.Path)
defer startSpan.Finish()
ctx.Set("tracer", tracer)
ctx.Set("parentSpan", startSpan) // 虽然设置了,没有跟随rpc传下去
ctx.Next()
怎么传递呢?(这里是关键)
首先是在rpc服务发现时传递全局tracer
#引入传递tracer的包
go get github.com/lisw358/tts_otgrpc
源码里有一个地方别人做了改动,来实现tracer传递,改动如下:
//=============================自己改源码部分开始
//获取到父span和trace
//此处需要在ctx上有这个key来向下传递tracer和parentSpan
ginConetext := ctx.Value("ginContext")
switch ginConetext.(type) {
case *gin.Context://获取traceif itrace, ok := ginConetext.(*gin.Context).Get("tracer"); ok {tracer = itrace.(opentracing.Tracer)}//获取父spanif parentSpan, ok := ginConetext.(*gin.Context).Get("parentSpan"); ok {parentCtx = parentSpan.(*jaeger.Span).Context()}
}
//=============================自己改源码部分结束
因此全局定义一个ContextWapper,来增加ginContext的设置
func ContextWrapper(c *gin.Context) context.Context {return context.WithValue(context.Background(), GinContext, c)
}
然后在调用时处理一下context即可
req := &proto.HelloReq{}
req.Name = c.DefaultQuery("name", "guest")//这里包装了一下context,来保证otgrpc包里能正常判断ginContext变量,来生成子span
res, _ := services.SayHello(global.ContextWrapper(c), req)
c.JSON(0, res.Message)
jaeger链路追踪结果如图【5】所示
图 5
本文完
参考文档
- jaeger tracing document
- https://chenquan.me/posts/tracing-system-analysis/
- https://github.com/lisw358/tts_otgrpc
- https://blog.51cto.com/u_15481067/11749036
如有侵权,烦请联系删除