gomicro集成链路跟踪的方法和中间件原理解析
来源:脚本之家
时间:2022-12-24 14:41:46 254浏览 收藏
怎么入门Golang编程?需要学习哪些知识点?这是新手们刚接触编程时常见的问题;下面golang学习网就来给大家整理分享一些知识点,希望能够给初学者一些帮助。本篇文章就来介绍《gomicro集成链路跟踪的方法和中间件原理解析》,涉及到中间件、gomicro、链路跟踪,有需要的可以收藏一下
前几天有个同学想了解下如何在go-micro中做链路跟踪,这几天正好看到wrapper这块,wrapper这个东西在某些框架中也称为中间件,里边有个opentracing的插件,正好用来做链路追踪。opentracing是个规范,还需要搭配一个具体的实现,比如zipkin、jeager等,这里选择zipkin。
链路跟踪实战
安装zipkin
通过docker快速启动一个zipkin服务端:
docker run -d -p 9411:9411 openzipkin/zipkin
程序结构
为了方便演示,这里把客户端和服务端放到了一个项目中,程序的目录结构是这样的:
- main.go 服务端程序。client/main.go 客户端程序。
- config/config.go 程序用到的一些配置,比如服务的名称和监听端口、zipkin的访问地址等。
- zipkin/ot-zipkin.go opentracing和zipkin相关的函数。
安装依赖包
需要安装go-micro、opentracing、zipkin相关的包:
go get go-micro.dev/v4@latest go get github.com/go-micro/plugins/v4/wrapper/trace/opentracing go get -u github.com/openzipkin-contrib/zipkin-go-opentracing
编写服务端
首先定义一个服务端业务处理程序:
type Hello struct { } func (h *Hello) Say(ctx context.Context, name *string, resp *string) error { *resp = "Hello " + *name return nil }
这个程序只有一个方法Say,输入name,返回 "Hello " + name。
然后使用go-micro编写服务端框架程序:
func main() { tracer := zipkin.GetTracer(config.SERVICE_NAME, config.SERVICE_HOST) defer zipkin.Close() tracerHandler := opentracing.NewHandlerWrapper(tracer) service := micro.NewService( micro.Name(config.SERVICE_NAME), micro.Address(config.SERVICE_HOST), micro.WrapHandler(tracerHandler), ) service.Init() micro.RegisterHandler(service.Server(), &Hello{}) if err := service.Run(); err != nil { log.Println(err) } }
这里NewService的时候除了指定服务的名称和访问地址,还通过micro.WrapHandler设置了一个用于链路跟踪的HandlerWrapper。
这个HandlerWrapper是通过go-micro的opentracing插件提供的,这个插件需要传入一个tracer。这个tracer可以通过前边安装的 zipkin-go-opentracing 包来创建,我们把创建逻辑封装在了config.go中:
func GetTracer(serviceName string, host string) opentracing.Tracer { // set up a span reporter zipkinReporter = zipkinhttp.NewReporter(config.ZIPKIN_SERVER_URL) // create our local service endpoint endpoint, err := zipkin.NewEndpoint(serviceName, host) if err != nil { log.Fatalf("unable to create local endpoint: %+v\n", err) } // initialize our tracer nativeTracer, err := zipkin.NewTracer(zipkinReporter, zipkin.WithLocalEndpoint(endpoint)) if err != nil { log.Fatalf("unable to create tracer: %+v\n", err) } // use zipkin-go-opentracing to wrap our tracer tracer := zipkinot.Wrap(nativeTracer) opentracing.InitGlobalTracer(tracer) return tracer }
service创建完毕之后,还要通过 micro.RegisterHandler 来注册前边编写的业务处理程序。
最后通过 service.Run 让服务运行起来。
编写客户端
再来看一下客户端的处理逻辑:
func main() { tracer := zipkin.GetTracer(config.CLIENT_NAME, config.CLIENT_HOST) defer zipkin.Close() tracerClient := opentracing.NewClientWrapper(tracer) service := micro.NewService( micro.Name(config.CLIENT_NAME), micro.Address(config.CLIENT_HOST), micro.WrapClient(tracerClient), ) client := service.Client() go func() { for {这段代码开始也是先NewService,设置客户端程序的名称和监听地址,然后通过micro.WrapClient注入链路跟踪,这里注入的是一个ClientWrapper,也是由opentracing插件提供的。这里用的tracer和服务端tracer是一样的,都是通过config.go中GetTracer函数获取的。
然后为了方便演示,启动一个go routine,客户端每隔一秒发起一次RPC请求,并将返回结果打印出来。运行效果如图所示:
zipkin中跟踪到的访问日志:
Wrap原理分析
Wrap从字面意思上理解就是封装、嵌套,在很多的框架中也称为中间件,比如gin中,再比如ASP.NET Core中。这个部分就来分析下go-micro中Wrap的原理。
服务端Wrap
在go-micro中服务端处理请求的逻辑封装称为Handler,它的具体形式是一个func,定义为:
func(ctx context.Context, req Request, rsp interface{}) error这个部分就来看一下服务端Handler是怎么被Wrap的。
HandlerWrapper
要想Wrap一个Handler,必须创建一个HandlerWrapper类型,这其实是一个func,其定义如下:
type HandlerWrapper func(HandlerFunc) HandlerFunc它的参数和返回值都是HandlerFunc类型,其实就是上面提到的Handler的func定义。
以本文链路跟踪中使用的 tracerHandler 为例,看一下HandlerWrapper是如何实现的:
func(h server.HandlerFunc) server.HandlerFunc { return func(ctx context.Context, req server.Request, rsp interface{}) error { ... if err = h(ctx, req, rsp); err != nil { ... } }从中可以看出,Wrap一个Hander就是定义一个新Handler,在它的的内部调用传入的原Handler。
Wrap Handler
创建了一个HandlerWrapper之后,还需要把它加入到服务端的处理过程中。
go-micro在NewService的时候通过调用 micro.WrapHandler 设置这些 HandlerWrapper:
service := micro.NewService( ... micro.WrapHandler(tracerHandler), )WrapHandler的实现是这样的:
func WrapHandler(w ...server.HandlerWrapper) Option { return func(o *Options) { var wrappers []server.Option for _, wrap := range w { wrappers = append(wrappers, server.WrapHandler(wrap)) } o.Server.Init(wrappers...) } }它返回的是一个函数,这个函数会将我们传入的HandlerWrapper通过server.WrapHandler转化为一个server.Option,然后交给Server.Init进行初始化处理。
这里的server.Option其实还是一个func,看一下WrapHandler的源码:
func WrapHandler(w HandlerWrapper) Option { return func(o *Options) { o.HdlrWrappers = append(o.HdlrWrappers, w) } }这个func将我们传入的HandlerWrapper添加到了一个切片中。
那么这个函数什么时候执行呢?就在Server.Init中。看一下Server.Init中的源码:
func (s *rpcServer) Init(opts ...Option) error { ... for _, opt := range opts { opt(&s.opts) } if s.opts.Router == nil { r := newRpcRouter() r.hdlrWrappers = s.opts.HdlrWrappers ... s.router = r } ... }它会遍历传入的所有server.Option,也就是执行每一个func(o *Options)。这样Options的切片HdlrWrappers中就添加了我们设置的HandlerWrapper,同时还把这个切片传递到了rpcServer的router中。
可以看到这里的Options就是rpcServer.opts,HandlerWrapper切片同时设置到了rpcServer.router和rpcServer.opts中。
还有一个问题:WrapHandler返回的func什么时候执行呢?
这个在micro.NewService -> newService -> newOptions中:
func newOptions(opts ...Option) Options { opt := Options{ ... Server: server.DefaultServer, ... } for _, o := range opts { o(&opt) } ... }遍历opts就是执行每一个设置func,最终执行到rpcServer.Init。
到NewService执行完毕为止,我们设置的WrapHandler全部添加到了一个名为HdlrWrappers的切片中。
再来看一下服务端Wrapper的执行过程是什么样的?
执行Handler的这段代码在rpc_router.go中:
func (s *service) call(ctx context.Context, router *router, sending *sync.Mutex, mtype *methodType, req *request, argv, replyv reflect.Value, cc codec.Writer) error { defer router.freeRequest(req) ... for i := len(router.hdlrWrappers); i > 0; i-- { fn = router.hdlrWrappers[i-1](fn) } ... // execute handler return fn(ctx, r, rawStream) }根据前面的分析,可以知道router.hdlrWrappers中记录的就是所有的HandlerWrapper,这里通过遍历router.hdlrWrappers实现了HandlerWrapper的嵌套,注意这里遍历时索引采用了从大到小的顺序,后添加的先被Wrap,先添加在外层。
实际执行时就是先调用到最先添加的HandlerWrapper,然后一层层向里调用,最终调用到我们注册的业务Handler,然后再一层层的返回,每个HandlerWrapper都可以在调用下一层前后做些自己的工作,比如链路跟踪这里的检测执行时间。
客户端Wrap
在客户端中远程调用的定义在Client中,它是一个接口,定义了若干方法:
type Client interface { ... Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error ... }我们这里为了讲解方便,只关注Call方法,其它的先省略。
下面来看一下Client是怎么被Wrap的。
XXXWrapper
要想Wrap一个Client,需要通过struct嵌套这个Client,并实现Client接口的方法。至于这个struct的名字无法强制要求,一般以XXXWrapper命名。
这里以链路跟踪使用的 otWrapper 为例,它的定义如下:
type otWrapper struct { ot opentracing.Tracer client.Client } func (o *otWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { ... if err = o.Client.Call(ctx, req, rsp, opts...); err != nil { ... } ...注意XXXWrapper实现的接口方法中都去调用了被嵌套Client的对应接口方法,这是能够嵌套执行的关键。
Wrap Client
有了上面的 XXXWrapper,还需要把它注入到程序的执行流程中。
go-micro在NewService的时候通过调用 micro.WrapClient 设置这些 XXXWrapper:
service := micro.NewService( ... micro.WrapClient(tracerClient), )和WrapHandler差不多,WrapClient的参数不是直接传入XXXWrapper的实例,而是一个func,定义如下:
type Wrapper func(Client) Client这个func需要将传入的的Client包装到 XXXWrapper 中,并返回 XXXWrapper 的实例。这里传入的 tracerClient 就是这样一个func:
return func(c client.Client) client.Client { if ot == nil { ot = opentracing.GlobalTracer() } return &otWrapper{ot, c} }要实现Client的嵌套,可以给定一个初始的Client实例作为第一个此类func的输入,然后前一个func的输出作为后一个func的输入,依次执行,最终形成业务代码中要使用的Client实例,这很像俄罗斯套娃,它有很多层Client。
那么这个俄罗斯套娃是什么时候创建的呢?
在 micro.NewService -> newService -> newOptions中:
func newOptions(opts ...Option) Options { opt := Options{ ... Client: client.DefaultClient, ... } for _, o := range opts { o(&opt) } return opt }可以看到这里给Client设置了一个初始值,然后遍历这些NewService时传入的Option(WrapClient返回的也是Option),这些Option其实都是func,所以就是遍历执行这些func,执行这些func的时候会传入一些初始默认值,包括Client的初始值。
那么前一个func的输出怎么作为后一个func的输入的呢?再来看下WrapClient的源码:
func WrapClient(w ...client.Wrapper) Option { return func(o *Options) { for i := len(w); i > 0; i-- { o.Client = w[i-1](o.Client) } } }可以看到Wrap方法从Options中获取到当前的Client实例,把它传给Wrap func,然后新生成的实例又被设置到Options的Client字段中。
正是这样形成了前文所说的俄罗斯套娃。
再来看一下客户端调用的执行流程是什么样的?
通过service的Client()方法获取到Client实例,然后通过这个实例的Call()方法执行RPC调用。
client:=service.Client() client.Call()这个Client实例就是前文描述的套娃实例:
func (s *service) Client() client.Client { return s.opts.Client }前文提到过:XXXWrapper实现的接口方法中调用了被嵌套Client的对应接口方法。这就是能够嵌套执行的关键。
这里给一张图,让大家方便理解Wrap Client进行RPC调用的执行流程:
客户端Wrap和服务端Wrap的区别
一个重要的区别是:对于多次WrapClient,后添加的先被调用;对于多次WrapHandler,先添加的先被调用。
有一个比较怪异的地方是,WrapClient时如果传递了多个Wrapper实例,WrapClient会把顺序调整过来,这多个实例中前边的先被调用,这个处理和多次WrapClient处理的顺序相反,不是很理解。
func WrapClient(w ...client.Wrapper) Option { return func(o *Options) { // apply in reverse for i := len(w); i > 0; i-- { o.Client = w[i-1](o.Client) } } }客户端Wrap还提供了更低层级的CallWrapper,它的执行顺序和服务端HandlerWrapper的执行顺序一致,都是先添加的先被调用。
// wrap the call in reverse for i := len(callOpts.CallWrappers); i > 0; i-- { rcall = callOpts.CallWrappers[i-1](rcall) }还有一个比较大的区别是,服务端的Wrap是调用某个业务Handler之前临时加上的,客户端的Wrap则是在调用Client.Call时就已经创建好。这样做的原因是什么呢?这个可能是因为在服务端,业务Handler和HandlerWrapper是分别注册的,注册业务Handler时HandlerWrapper可能还不存在,只好采用动态Wrap的方式。而在客户端,通过Client.Call发起调用时,Client是发起调用的主体,用户有很多获取Client的方式,无法要求用户在每次调用前都临时Wrap。
Http服务的链路跟踪
关于Http或者说是Restful服务的链路跟踪,go-micro的httpClient支持CallWrapper,可以用WrapCall来添加链路跟踪的CallWrapper;但是其httpServer实现的比较简单,把http内部的Handler处理完全交出去了,不能用WrapHandler,只能自己在http的框架中来做这件事,比如go-micro+gin开发的Restful服务可以使用gin的中间件机制来做链路追踪。
代码已经上传到Github,欢迎访问:https://github.com/bosima/go-demo/tree/main/go-micro-opentracing
今天关于《gomicro集成链路跟踪的方法和中间件原理解析》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于golang的内容请关注golang学习网公众号!
-
290 收藏
-
229 收藏
-
167 收藏
-
377 收藏
-
208 收藏
-
419 收藏
-
234 收藏
-
155 收藏
-
457 收藏
-
309 收藏
-
225 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习