go-micro之原始碼剖析: Registry

lucifer_L發表於2019-05-27

go-micro提供了分散式系統開發的核心需求,包括RPC和事件驅動的通訊機制。關於go-micro的詳細內容請參考git上的go-micro專案,這篇文章主要來講go-micro的元件register的原始碼剖析。

go-micro的結構圖如下(來源git倉庫)。

圖1.1

go-micro之原始碼剖析: Registry

可以看到go-micro底層分為6個元件,分別是broker、Codec、Register、Selector、Transport。 Registry是go-micro的註冊模組,它提供可插拔的服務註冊與發現功能,它目前的實現的方式有Consul,etcd,記憶體和k8s。我們以consul為例子,來看一下go-micro是如何完成整個註冊實現的。

準備工作

  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註冊部分的的程式碼。

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是怎麼知道該使用哪個註冊器呢。

先看一下registry包的結構。

圖1.2

go-micro之原始碼剖析: Registry

參考包的目錄結構,我們可以知道註冊器支援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之原始碼剖析: Registry

總結

通過go-micro的程式碼review,我們瞭解了它內部的細節實現。通常當我們想要review原始碼的時候,首先要從整體把握專案的結構,這個可以通過文件或者是包名去知道。瞭解了它的專案結構之後,我們可以著重關注某一個感興趣的子元件,然後深入去閱讀原始碼。閱讀程式碼的同時帶著自己的疑問,然後去尋找答案,同時學習一些程式碼的寫法,最終把學到的東西記錄下來,我想這就是學習和閱讀原始碼的意義。

相關文章