go-micro之源码剖析: Registry

go-micro提供了分布式系统开发的核心需求,包括RPC和事件驱动的通讯机制。关于go-micro的详细内容请参考git上的go-micro项目,这篇文章主要来说go-micro的组件register的源码剖析。git

go-micro的结构图以下(来源git仓库)。github

图1.1

能够看到go-micro底层分为6个组件,分别是broker、Codec、Register、Selector、Transport。 Registry是go-micro的注册模块,它提供可插拔的服务注册与发现功能,它目前的实现的方式有Consul,etcd,内存和k8s。咱们以consul为例子,来看一下go-micro是如何完成整个注册实现的。bash

准备工做

  1. 须要consul,你能够在consul官网上下载consul的二进制可执行文件,能够直接使用命令./consul agent -dev -client 0.0.0.0 -ui来启用consul
  2. 参考go-micro doc里的greeter例子。设置MICRO_REGISTRY=consul环境变量,而后跑通demo。
  3. 我使用的是mac版本的goland,能够方便源代码跟踪,你能够使用goland或者是delve相似的工具进行debug。

代码剖析

服务注册在go-micro服务端实现,找到service demo里的service.Run(),它是最外层service启动的入口。Run的实如今service.go里。进入到Run方法里里找到(s *service) Start()服务器

func (s *service) Start() error {
	for _, fn := range s.opts.BeforeStart {
		if err := fn(); err != nil {
			return err
		}
	}

	if err := s.opts.Server.Start(); err != nil {
		return err
	}

	for _, fn := range s.opts.AfterStart {
		if err := fn(); err != nil {
			return err
		}
	}

	return nil
}
复制代码

这个方法内部按照顺序执行,进行了服务器启动前事件处理,服务启动,和服务结束事件处理的三个流程。核心代码在s.opts.Server.Start()。跟踪代码进入这个start函数,进入到(s *rpcServer) Start()里面,这里就是go-micro里service的核心代码。代码行数比较多,咱们直接关注重点也就是register的功能。找到Register注册部分的的代码。app

func (s *rpcServer) Start() error {
    ...
    // use RegisterCheck func before register
	if err = s.opts.RegisterCheck(s.opts.Context); err != nil {
		log.Logf("Server %s-%s register check error: %s", config.Name, config.Id, err)
	} else {
		// announce self to the world
		if err = s.Register(); err != nil {
			log.Log("Server %s-%s register error: %s", config.Name, config.Id, err)
		}
	}
	...
}
复制代码

这里,首先检查了register环境的上下文。若是没有问题则进行注册操做。进入到s.Register()里面,(s *rpcServer) Register()这个函数就是注册功能的核心代码部分。不看前面的预处理,只看咱们关注的核心部分,找到下面的代码行:分布式

func (s *rpcServer) Register() error {
    ...
    if err := config.Registry.Register(service, rOpts...); err != nil {
    	return err
    }
    ...
}    
复制代码

不难猜出,这里就是注册的功能实现了,可是go-micro是怎么知道该使用哪一个注册器呢。ide

先看一下registry包的结构。函数

图1.2

参考包的目录结构,咱们能够知道注册器支持4种类型的注册操做,分别是consul、gossip、mdns、memory。 咱们设置了MICRO_REGISTRY=consul的环境变量,来告诉go-micro使用consul方式注册。那么,config.Registry究竟是在哪设置成consulRegister的呢。工具

答案是,在service.Init()服务初始化的里面进行了设置。回到service demo里学习

// Init will parse the command line flags.
service.Init()
复制代码

跟踪进入Init函数

func (s *service) Init(opts ...Option) {
	// process options
	for _, o := range opts {
		o(&s.opts)
	}

	s.once.Do(func() {
		// Initialise the command flags, overriding new service
		_ = s.opts.Cmd.Init(
			cmd.Broker(&s.opts.Broker),
			cmd.Registry(&s.opts.Registry),
			cmd.Transport(&s.opts.Transport),
			cmd.Client(&s.opts.Client),
			cmd.Server(&s.opts.Server),
		)
	})
}
复制代码

Init里面,for循环执行了一系列预处理的函数。而后使用了sync.Once里的once操做,保证里面的函数只执行一次。重点关注Cmd.Init这个方法,它这里接收的参数使用的比较绕。

func (c *cmd) Init(opts ...Option) error {
	for _, o := range opts {
		o(&c.opts)
	}
	c.app.Name = c.opts.Name
	c.app.Version = c.opts.Version
	c.app.HideVersion = len(c.opts.Version) == 0
	c.app.Usage = c.opts.Description
	c.app.RunAndExitOnError()
	return nil
}
复制代码

首先cmd.Init的参数接受type Option func(o *Options)类型的方法,而后依次执行,而后再进行变量赋值和函数处理。

它接受的方法以broker为例。

func Broker(b *broker.Broker) Option {
	return func(o *Options) {
		o.Broker = b
	}
}
复制代码

Broker方法有点绕,要和前面两个Init一块儿看。Broker方法接受了一个Broker类型的指针。它返回了一个Option方法,Option方法接受一个Options类型的指针。而后把o *Options的Broker设置成外层函数参数传进来的Broker。

进入到cmd.Init里for循环依次执行了同Broker相似的方法,o(&c.opts)也就是把c.opts对应的组件进行赋值。所赋的值就是service.Init里的s.opts.Brokers.opts.Registry等组件。

因此它一系列的操做就是s.opts.Cmd.opts去复用service.opts上的组件。这里能够学习一下它这种写法,在对象不可见的状况下传递对象的字段。(这种写法其余的好处,请大佬必定诉我)

咱们继续找,进入c.app.RunAndExitOnError()的这个方法里,而后进入到a.Run()

func (a *App) Run(arguments []string) (err error) {
    ...
    if a.Before != nil {
		err = a.Before(context)
		if err != nil {
			fmt.Fprintf(a.Writer, "%v\n\n", err)
			ShowAppHelp(context)
			return err
		}
	}
	...
}
复制代码

直接告诉你设置Register的位置是在a.Before这里。这个方法没有本身定义,也是复用了cmd.Before方法。cmd.go的newCmd(opts ...Option)方法里你会找到cmd.app.Before = cmd.Before,就是在这里设置的。最终赋值的地方就是在这个cmd.Before里。

func (c *cmd) Before(ctx *cli.Context) error {
    ...
    if name := ctx.String("registry"); len(name) > 0 && (*c.opts.Registry).String() != name {
		r, ok := c.opts.Registries[name]
		if !ok {
			return fmt.Errorf("Registry %s not found", name)
		}

		*c.opts.Registry = r()
		serverOpts = append(serverOpts, server.Registry(*c.opts.Registry))
		clientOpts = append(clientOpts, client.Registry(*c.opts.Registry))
		...
	}	
    ...
}
复制代码

那么到这里咱们就知道了,它会去从环境变量里拿registry的值,咱们设置的是consul,对应的就是consulRegistry。而后*c.opts.Registry = r()这里最终设置了这个注册器。

费劲千辛万苦终于知道从哪里拿的Registry。接下来回到上面的Register函数调用那里。咱们去找consulRegistry的Register函数

func (c *consulRegistry) Register(s *registry.Service, opts ...registry.RegisterOption) error {
    ...
    if err := c.Client.Agent().ServiceRegister(asr); err != nil {
		return err
	}
	...
}
复制代码

这个函数比较长,它主要作的处理是和consul服务进行通讯。在Agent().ServiceRegister(asr)里面,它发起了一个PUT请求,具体的请求内容能够本身去实际看一下发包。

func (a *Agent) ServiceRegister(service *AgentServiceRegistration) error {
	r := a.c.newRequest("PUT", "/v1/agent/service/register")
	r.obj = service
	_, resp, err := requireOK(a.c.doRequest(r))
	if err != nil {
		return err
	}
	resp.Body.Close()
	return nil
}
复制代码

到这里,go-micro就讲服务注册到了consul上。最终consul会链接你的服务端和客户端,让它们之间进行通讯。

到此咱们从源码角度,了解了go-micro的服务注册流程,选择注册器,而后进行服务的注册,其余的注册器的逻辑也是同样的就再也不复述。

下面是我记录的代码流程图

图1.3

总结

经过go-micro的代码review,咱们了解了它内部的细节实现。一般当咱们想要review源码的时候,首先要从总体把握项目的结构,这个能够经过文档或者是包名去知道。了解了它的项目结构以后,咱们能够着重关注某一个感兴趣的子组件,而后深刻去阅读源代码。阅读代码的同时带着本身的疑问,而后去寻找答案,同时学习一些代码的写法,最终把学到的东西记录下来,我想这大概就是学习和阅读源码的意义。

相关文章
相关标签/搜索