关于golang:gomicro集成链路跟踪的方法和中间件原理

129次阅读

共计 8549 个字符,预计需要花费 22 分钟才能阅读完成。

前几天有个同学想理解下如何在 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-…

播种更多架构常识,请关注微信公众号 萤火架构。原创内容,转载请注明出处。

正文完
 0