前几天有个同学想理解下如何在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 {
<-time.After(time.Second)
result := new(string)
request := client.NewRequest(config.SERVICE_NAME, "Hello.Say", "FireflySoft")
err := client.Call(context.TODO(), request, result)
if err != nil {
log.Println(err)
continue
}
log.Println(*result)
}
}()
service.Run()
}
这段代码开始也是先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-…