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

目录
  • 链路跟踪实战
    • 安装zipkin
    • 程序结构
    • 安装依赖包
    • 编写服务端
    • 编写客户端
  • Wrap原理分析
    • 服务端Wrap
    • HandlerWrapper
    • Wrap Handler
    • 客户端Wrap
    • XXXWrapper
    • Wrap Client
    • 客户端Wrap和服务端Wrap的区别
    • Http服务的链路跟踪

前几天有个同学想了解下如何在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-demo/tree/main/go-micro-opentracing

到此这篇关于go-micro集成链路跟踪的方法和中间件原理的文章就介绍到这了,更多相关go micro链路跟踪内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 基于django micro搭建网站实现加水印功能

    用django_micro搭建的,给图片加文字水印的前端+后端功能开发: 大体功能是:输入水印的文字,选择要加水印的图片,最后生成加好水印的图片. 可在一页中显示多个加好水印的图片,且可点击显示或隐藏图片的缩略图.实现效果如下: 代码如下 from django_micro import route, run, configure from django.http import HttpRequest, HttpResponse from dominate.document import doc

  • go-micro使用Consul做服务发现的方法和原理解析

    目录 安装Consul 安装Consul插件 服务端使用Consul 服务注册 注册过程 健康检查 客户端使用Consul 调用服务 发现过程 效果展示 go-micro v4默认使用mdns做服务发现.不过也支持采用其它的服务发现中间件,因为多年来一直使用Consul做服务发现,为了方便和其它服务集成,所以还是选择了Consul.这篇文章将介绍go-micro使用Consul做服务发现的方法.关于Consul的使用方式请参考我的另一篇文章:搭建Consul服务发现与服务网格 . 安装Consu

  • 关于go-micro与其它gRPC框架之间的通信问题及解决方法

    目录 客户端改造 服务端改造 运行效果 在之前的文章中分别介绍了使用gRPC官方插件和go-micro插件开发gRPC应用程序的方式,都能正常走通.不过当两者混合使用的时候,互相访问就成了问题.比如使用go-micro插件生成的gRPC客户端访问基于gRPC官方插件创建的服务端时就会出现如下错误: {"id":"go.micro.client","code":501,"status":"Not Implemented

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

    目录 链路跟踪实战 安装zipkin 程序结构 安装依赖包 编写服务端 编写客户端 Wrap原理分析 服务端Wrap HandlerWrapper Wrap Handler 客户端Wrap XXXWrapper Wrap Client 客户端Wrap和服务端Wrap的区别 Http服务的链路跟踪 前几天有个同学想了解下如何在go-micro中做链路跟踪,这几天正好看到wrapper这块,wrapper这个东西在某些框架中也称为中间件,里边有个opentracing的插件,正好用来做链路追踪.op

  • 微服务分布式架构实现日志链路跟踪的方法

    Logback 背景 Logback是由log4j创始人设计的另一个开源日志组件,官方网站:http://logback.qos.ch.它当前分为下面下个模块: logback-core:其它两个模块的基础模块 logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API使你可以很方便地更换成其它日志系统如log4j或JDK14 Logging logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能 普通debug日志

  • Spring Cloud 整合Apache-SkyWalking实现链路跟踪的方法

    什么是SkyWalking 查看官网https://skywalking.apache.org/ 分布式系统的应用程序性能监视工具,专为微服务.云原生架构和基于容器(Docker.K8s.Mesos)架构而设计. 安装 进入下载页面https://skywalking.apache.org/zh/downloads/ 这里用的是ElasticSearch 7版本,所以你需要安装完成ElasticSearch 7,不再赘述. 解压后,可以修改启动端口 apache-skywalking-apm-b

  • Android ListView 单条刷新方法实践及原理解析

    对于使用listView配合adapter进行刷新的方法大家都不陌生,先刷新adapter里的数据,然后调用notifydatasetchange通知listView刷新界面. 方法虽然简单,但这里面涉及到一个效率的问题,调用notifydatasetchange其实会导致adpter的getView方法被多次调用 (画面上能显示多少就会被调用多少次),如果是很明确的知道只更新了list中的某一个项的数据(比如用户点击list某一项后更新该项的显示状态,或者 后台回调更新list某一项,等等),

  • Java方法参数传递机制原理解析

    这篇文章主要介绍了Java方法参数传递机制原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 Java方法中如果声明了形参,在调用方法时就必须给这些形参指定参数值,实际传进去的这个值就叫做实参. 这就涉及到Java中的参数传递机制,值传递. 基本数据类型 基本数据类型,值传递的体现是数值的传递. public class TransferTempTest { public static void main(String[] args) {

  • SpringCloud分布式链路跟踪的方法

    注:作者使用IDEA + Gradle 注:需要有一定的java SpringBoot and SSM+Springcloud基础 程序测试错误追责 我举个例子,我现在要做一个电商项目,项目里面有一个购买模块,那我这边可能要执行一个代码,比如减库存之类的东西,那我两个服务不就是要相互调用嘛,我自身是一个服务,我现在要调用减库存这个服务: 你调用它,你知道它一定能执行成功吗?肯定是不一定: 比如说,我现在要执行一个减库存的代码,我调用这个方法会进行库存的一个更改,这个库存减少成功还好,万一要是失败

  • Python类中的魔法方法之 __slots__原理解析

    在类中每次实例化一个对象都会生产一个字典来保存一个对象的所有的实例属性,这样非常的有用处,可以使我们任意的去设置新的属性. 每次实例化一个对象python都会分配一个固定大小内存的字典来保存属性,如果对象很多的情况下会浪费内存空间. 可通过__slots__方法告诉python不要使用字典,而且只给一个固定集合的属性分配空间 class Foo(object): __slots__ = ("x","y","z") def __init__(sel

  • ASP.NET Core整合Zipkin链路跟踪的实现方法

    前言     在日常使用ASP.NET Core的开发或学习中,如果有需要使用链路跟踪系统,大多数情况下会优先选择SkyAPM.我们之前也说过SkyAPM设计确实比较优秀,巧妙的利用DiagnosticSource诊断跟踪日志,可以做到对项目无入侵方式的集成.其实还有一款比较优秀的链路跟踪系统,也可以支持ASP.NET Core,叫Zipkin.它相对于SkyWalking来说相对轻量级,使用相对来说比较偏原生的方式,而且支持Http的形式查询和提交链路数据.因为我们总是希望能拥有多一种的解决方

  • SpringBoot+slf4j实现全链路调用日志跟踪的方法(一)

    SpringBoot中除了常见的分布式链路跟踪系统zipkin.skywalking等,如果需要快速定位一次请求的所有日志,那么该如何实现?实际slf4j提供了MDC(Mapped Diagnostic Contexts)功能,支持用户定义和修改日志的输出格式以及内容.本文将介绍 Tracer集成的slf4j MDC功能,方便用户在只简单修改日志配置文件的前提下输出当前 Tracer 上下文 TraceId. MDC介绍 MDC(Mapped Diagnostic Context,映射调试上下文

  • 详解spring cloud分布式日志链路跟踪

    首先要明白一点,为什么要使用链路跟踪? 当我们微服务之间调用的时候可能会出错,但是我们不知道是哪个服务的问题,这时候就可以通过日志链路跟踪发现哪个服务出错. 它还有一个好处:当我们在企业中,可能每个人都负责一个服务,我们可以通过日志来检查自己所负责的服务不会出错,当调用其它服务时,这时候出现错误,那么就可以判定出不是自己的服务出错,从而也可以发现责任不是自己的. 基于微服务之间的调用开始,如果看不懂的小伙伴,请先参考我上篇博客:spring cloud中微服务之间的调用以及eureka的自我保护

随机推荐