您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Go语言

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

时间:2022-05-05 10:05:53  来源:  作者:萤火架构

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

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

 

链路跟踪实战

安装zipkin

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

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

程序结构

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

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

 

  • 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: %+vn", err)
    }

    // initialize our tracer
    nativeTracer, err := zipkin.NewTracer(zipkinReporter, zipkin.WithLocalEndpoint(endpoint))
    if err != nil {
        log.Fatalf("unable to create tracer: %+vn", 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请求,并将返回结果打印出来。运行效果如图所示:

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

 

zipkin中跟踪到的访问日志:

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

 

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调用的执行流程:

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

 

客户端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



Tags:go-micro   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
go-micro集成链路跟踪的方法和中间件原理
前几天有个同学想了解下如何在go-micro中做链路跟踪,这几天正好看到wrapper这块,wrapper这个东西在某些框架中也称为中间件,里边有个opentracing的插件,正好用来做链路追踪。ope...【详细内容】
2022-05-05  Search: go-micro  点击:(341)  评论:(0)  加入收藏
go-micro的安装和使用
go-micro是基于 Go 语言用于开发的微服务的 RPC 框架,主要功能如下:服务发现,负载均衡 ,消息编码,请求/响应,Async Messaging,可插拔接口,最后这个功能牛p安装步骤安装proto...【详细内容】
2021-09-06  Search: go-micro  点击:(1580)  评论:(0)  加入收藏
▌简易百科推荐
宝藏级Go语言开源项目——教你自己动手开发互联网搜索引擎
DIYSearchEngine 是一个能够高速采集海量互联网数据的开源搜索引擎,采用 Go 语言开发。Github 地址:https://github.com/johnlui/DIYSearchEngine运行方法首先,给自己准备一杯...【详细内容】
2024-03-12  OSC开源社区    Tags:Go语言   点击:(29)  评论:(0)  加入收藏
Go Gin框架实现优雅地重启和停止
在Web应用程序中,有时候我们需要重启或停止服务器,无论是因为更新代码还是进行例行维护。在这种情景下,我们需要保证应用程序的可用性和数据的一致性。这就需要优雅地关闭和重...【详细内容】
2024-01-30  源自开发者  微信公众号  Tags:Go   点击:(71)  评论:(0)  加入收藏
如何让Go程序以后台进程或daemon方式运行
本文探讨了如何通过Go代码实现在后台运行的程序。最近我用Go语言开发了一个WebSocket服务,我希望它能在后台运行,并在异常退出时自动重新启动。我的整体思路是将程序转为后台...【详细内容】
2024-01-26  Go语言圈  微信公众号  Tags:Go程序   点击:(61)  评论:(0)  加入收藏
深入Go底层原理,重写Redis中间件实战
Go语言以其简洁、高效和并发性能而闻名,深入了解其底层原理可以帮助我们更好地利用其优势。在本文中,我们将探讨如何深入Go底层原理,以及如何利用这些知识重新实现一个简单的Re...【详细内容】
2024-01-25  547蓝色星球    Tags:Go   点击:(76)  评论:(0)  加入收藏
Go 内存优化与垃圾收集
Go提供了自动化的内存管理机制,但在某些情况下需要更精细的微调从而避免发生OOM错误。本文将讨论Go的垃圾收集器、应用程序内存优化以及如何防止OOM(Out-Of-Memory)错误。Go...【详细内容】
2024-01-15  DeepNoMind  微信公众号  Tags:Go   点击:(65)  评论:(0)  加入收藏
Go函数指针是如何让你的程序变慢的?
导读Go 语言的常规优化手段无需赘述,相信大家也能找到大量的经典教程。但基于 Go 的函数值问题,业界还没有太多深度讨论的内容分享。本文作者根据自己对 Go 代码的使用与调优...【详细内容】
2024-01-15  腾讯云开发者  微信公众号  Tags:Go函数   点击:(92)  评论:(0)  加入收藏
Go编程中调用外部命令的几种场景
在很多场合, 使用Go语言需要调用外部命令来完成一些特定的任务, 例如: 使用Go语言调用Linux命令来获取执行的结果,又或者调用第三方程序执行来完成额外的任务。在go的标准库...【详细内容】
2024-01-09  suntiger    Tags:Go编程   点击:(124)  评论:(0)  加入收藏
Go 语言不支持并发读写 Map,为什么?
Go语言的map类型不支持并发读写的主要原因是并发读写会导致数据竞态(data race),这意味着多个 goroutine 可能同时访问并修改同一个 map,从而引发不确定的结果。在Go语言的设计...【详细内容】
2024-01-05  Go语言圈  微信公众号  Tags:Go 语言   点击:(83)  评论:(0)  加入收藏
Go微服务入门到容器化实践
Go微服务入门到容器化实践Go 是一门高效、现代化、快速增长的编程语言,非常适合构建 Web 应用程序。而 Docker 是一种轻量级的容器化技术,能够使得您的应用程序在任何地方运行...【详细内容】
2024-01-01  大雷家吃饭    Tags:Go微服务   点击:(70)  评论:(0)  加入收藏
你是否想知道如何应对高并发?Go语言为你提供了答案!
并发编程是当前软件领域中不可忽视的一个关键概念。随着CPU等硬件的不断发展,我们都渴望让我们的程序运行速度更快、更快。而Go语言在语言层面天生支持并发,充分利用现代CPU的...【详细内容】
2023-12-29  灵墨AI探索室  微信公众号  Tags:Go语言   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条