go-micro集成链路跟踪的方法和中间件原理2

简介: go-micro集成链路跟踪的方法和中间件原理2

客户端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调用的执行流程:1689145618099.png


客户端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,欢迎访问:github.com/bosima/go-d…


相关文章
|
3月前
|
缓存 前端开发 中间件
[go 面试] 前端请求到后端API的中间件流程解析
[go 面试] 前端请求到后端API的中间件流程解析
|
1月前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
54 11
|
1月前
|
SQL 中间件 关系型数据库
那些年,我们在Go中间件上踩过的坑
作者总结了过去在Go中间件上踩过的坑,这些坑也促进了阿里内部Go中间件的完善,希望大家学习本文后,不犯同样的错误。
|
2月前
|
大数据 Shell Go
GO方法与自定义类型
本文详细介绍了 Go 语言中的自定义数据类型与方法。不同于传统的面向对象编程语言,Go 通过结构体 (`struct`) 和方法 (`method`) 来扩展自定义类型的功能。文章解释了如何定义结构体、创建方法,并探讨了值接收器与指针接收器的区别及应用场景。此外,还介绍了方法的可见性以及接收器的命名惯例。通过具体示例,帮助读者更好地理解和应用这些概念。
|
2月前
|
Kubernetes Go 持续交付
一个基于Go程序的持续集成/持续部署(CI/CD)
本教程通过一个简单的Go程序示例,展示了如何使用GitHub Actions实现从代码提交到Kubernetes部署的CI/CD流程。首先创建并版本控制Go项目,接着编写Dockerfile构建镜像,再配置CI/CD流程自动化构建、推送Docker镜像及部署应用。此流程基于GitHub仓库,适用于快速迭代开发。
49 3
|
2月前
|
Kubernetes 持续交付 Go
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
|
3月前
|
存储 中间件 数据库
go-zero 是如何追踪你的请求链路
go-zero 是如何追踪你的请求链路
|
3月前
|
数据可视化 Linux Android开发
Flutter相关痛点解决问题之研发链路中的环境配置和打包集成问题如何解决
Flutter相关痛点解决问题之研发链路中的环境配置和打包集成问题如何解决
|
3月前
|
监控 中间件 Go
Go 中间件的优雅实现:处理请求的艺术
Go 中间件的优雅实现:处理请求的艺术
|
3月前
|
存储 Ubuntu Go
在Ubuntu 16.04上安装Go 1.6的方法
在Ubuntu 16.04上安装Go 1.6的方法
47 1