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

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

前几天有个同学想了解下如何在go-micro中做链路跟踪,这几天正好看到wrapper这块,wrapper这个东西在某些框架中也称为中间件,里边有个opentracing的插件,正好用来做链路追踪。opentracing是个规范,还需要搭配一个具体的实现,比如zipkin、jeager等,这里选择zipkin。

1689144742842.png


链路跟踪实战


安装zipkin


通过docker快速启动一个zipkin服务端:

docker run -d -p 9411:9411 openzipkin/zipkin


程序结构


为了方便演示,这里把客户端和服务端放到了一个项目中,程序的目录结构是这样的:

1689144799313.png

  • 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请求,并将返回结果打印出来。运行效果如图所示:


1689144970686.png

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),
  )

实现:

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都可以在调用下一层前后做些自己的工作,比如链路跟踪这里的检测执行时间。


相关文章
|
29天前
|
机器学习/深度学习 Python
堆叠集成策略的原理、实现方法及Python应用。堆叠通过多层模型组合,先用不同基础模型生成预测,再用元学习器整合这些预测,提升模型性能
本文深入探讨了堆叠集成策略的原理、实现方法及Python应用。堆叠通过多层模型组合,先用不同基础模型生成预测,再用元学习器整合这些预测,提升模型性能。文章详细介绍了堆叠的实现步骤,包括数据准备、基础模型训练、新训练集构建及元学习器训练,并讨论了其优缺点。
49 3
|
1月前
|
缓存 监控 前端开发
Go 语言中如何集成 WebSocket 与 Socket.IO,实现高效、灵活的实时通信
本文探讨了在 Go 语言中如何集成 WebSocket 与 Socket.IO,实现高效、灵活的实时通信。首先介绍了 WebSocket 和 Socket.IO 的基本概念及其优势,接着详细讲解了 Go 语言中 WebSocket 的实现方法,以及二者集成的重要意义和具体步骤。文章还讨论了集成过程中需要注意的问题,如协议兼容性、消息格式、并发处理等,并提供了实时聊天、数据监控和在线协作工具等应用案例,最后提出了性能优化策略,包括数据压缩、缓存策略和连接管理优化。旨在帮助开发者更好地理解并应用这些技术。
57 3
|
2月前
|
SQL 关系型数据库 MySQL
Go语言项目高效对接SQL数据库:实践技巧与方法
在Go语言项目中,与SQL数据库进行对接是一项基础且重要的任务
90 11
|
2月前
|
人工智能 JavaScript 网络安全
ToB项目身份认证AD集成(三完):利用ldap.js实现与windows AD对接实现用户搜索、认证、密码修改等功能 - 以及针对中文转义问题的补丁方法
本文详细介绍了如何使用 `ldapjs` 库在 Node.js 中实现与 Windows AD 的交互,包括用户搜索、身份验证、密码修改和重置等功能。通过创建 `LdapService` 类,提供了与 AD 服务器通信的完整解决方案,同时解决了中文字段在 LDAP 操作中被转义的问题。
|
3月前
|
大数据 Shell Go
GO方法与自定义类型
本文详细介绍了 Go 语言中的自定义数据类型与方法。不同于传统的面向对象编程语言,Go 通过结构体 (`struct`) 和方法 (`method`) 来扩展自定义类型的功能。文章解释了如何定义结构体、创建方法,并探讨了值接收器与指针接收器的区别及应用场景。此外,还介绍了方法的可见性以及接收器的命名惯例。通过具体示例,帮助读者更好地理解和应用这些概念。
|
3月前
|
Kubernetes Go 持续交付
一个基于Go程序的持续集成/持续部署(CI/CD)
本教程通过一个简单的Go程序示例,展示了如何使用GitHub Actions实现从代码提交到Kubernetes部署的CI/CD流程。首先创建并版本控制Go项目,接着编写Dockerfile构建镜像,再配置CI/CD流程自动化构建、推送Docker镜像及部署应用。此流程基于GitHub仓库,适用于快速迭代开发。
79 3
|
3月前
|
Kubernetes 持续交付 Go
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
|
2月前
|
SQL 数据库连接 数据库
管理系统中的Visual Studio与SQL集成技巧与方法
在现代软件开发和管理系统中,Visual Studio(VS)作为强大的集成开发环境(IDE),与SQL数据库的紧密集成是构建高效、可靠应用程序的关键
|
2月前
|
SQL 监控 数据库
管理系统VS SQL:高效集成的关键技巧与方法
在现代企业信息化建设中,管理系统(如ERP、CRM等)与SQL数据库之间的紧密集成是确保数据流动顺畅、业务逻辑高效执行的关键
|
4月前
|
存储 中间件 数据库
go-zero 是如何追踪你的请求链路
go-zero 是如何追踪你的请求链路

热门文章

最新文章