Micro原始碼系列 - Go-Micro服務是如何註冊的

printfcoder發表於2019-07-03

前面一章我們大體講解了Go-Micro中的服務是如何構建的。接下來我們就從程式碼層面給大家演示服務註冊。

微服務架構中註冊是非常有意思的角色,服務中的客戶端通過序號產生器制定位目標服務的具體位置。服務註冊中心可以說是服務例項的資料庫,在裡面有服務的各種資訊,包括位置等。服務例項在啟動時通過序號產生器制註冊到中心,並且在關閉前從中心自動解除安裝。不過光有註冊解除安裝兩個步驟還不夠,在兩者之間,我們還需要健康檢查來確定服務是否可以持續接收請求。

同時,我們需要指出一個大家特別容易犯的錯誤:很多人都會覺得服務註冊就是為了負載均衡,其實不是,服務註冊是為了客戶端或服務端定位服務例項,並確定選擇哪一個服務來傳送請求的機制。而負載均衡只是選擇服務時如何讓各服務之間平衡提供響應的策略,它可能依賴註冊,但不是必須,因為有哪些服務可以通過很多方式告之客戶端,而並且非一律從註冊中心獲取。

Micro體系中的每一種型別的服務都包含有註冊元件(Registry)。當服務啟動時,它會把所有描述自身資訊的後設資料(metadata,比如服務名、地址、transport、編碼等等)提取出來,作為關鍵資訊,用於下一步註冊成為服務節點。爾後,如果宣告有TTL和Interval,則會定期觸發重序號產生器制。

註冊中心介面

註冊元件介面registry中:

package registry
// ...
type Registry interface {
        Init(...Option) error
        Options() Options
        Register(*Service, ...RegisterOption) error
        Deregister(*Service) error
        GetService(string) ([]*Service, error)
        ListServices() ([]*Service, error)
        Watch(...WatchOption) (Watcher, error)
        String() string
}

在go-micro包中,共有4種註冊實現consul、gossip、mdns、memory,前兩個都是基於hashicorp公司的協議,mdns則是基於組網廣播實現,memory則是本地實現。

  • consul 依賴hashicorp的元件,但是功能強大、完整
  • gossip 基於SWIM協議廣播,零依賴
  • mdns 輕量、零依賴,但是對環境有要求,某些環境不支援mdns的無法正常使用
  • memory 本地解決方案,不可跨主機訪問

另外在go-plugins中有其它註冊實現,比如etcd、eureka、k8s、nats、zk等等

大體解釋下介面中每個方法的作用

  • Init 初始化
  • Options 獲取配置選項
  • Register 註冊服務
  • Deregister 解除安裝服務
  • GetService 獲取指定服務
  • ListServices 列出所有服務
  • Watch watcher 負責偵聽變動
  • String 註冊資訊轉成字串描述

可見,介面定義的註冊元件是幾乎完全自包含,它自行註冊、解除安裝、偵聽等,服務不需要關心自己如何註冊、解除安裝,只需要將註冊中心的實現作為Option匯入自身啟動即可

通過定義註冊元件介面,我們便可以將服務與註冊中心解耦

宣告註冊中心

我們知道Go-Micro可以通過命令列引數--registry或者方法引數micro.Registry來指定服務註冊中心,但是Register方法中並沒有選擇註冊中心的過程,我們看下Go-Micro在構建服務時的動作:

命令列引數:

go run main.go --registry=consul

Go-Micro預置有4種命令列引數:

cmd.go

DefaultRegistries = map[string]func(...registry.Option) registry.Registry{
                "consul": consul.NewRegistry,
                "gossip": gossip.NewRegistry,
                "mdns":   mdns.NewRegistry,
                "memory": rmem.NewRegistry,
}

在識別命令列傳入引數後,Micro就會匹配DefaultRegistries中的key,然後把註冊元件附加給服務。

*Env方式大同小異,這裡不表

方法引數,通過micro.Registry傳入:

        micReg := consul.NewRegistry(registryOptions)
        service := micro.NewService(
                // ...
                micro.Registry(micReg),
                // ...
        )

因為Registry是自包含的,故而我們只需要將其傳入服務,讓服務呼叫即可。

服務啟動

簡單回顧下服務在Start時的動作,我們用預設的rpc_server來演示,其它如grpc_server等大同小異,不影響理解。

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 {
                // 註冊
                if err = s.Register(); err != nil {
                        log.Logf("Server %s-%s register error: %s", config.Name, config.Id, err)
                }
        }

        // ...
        // Interval、解除安裝程式碼,下面我們會講到
        return nil
}

Start()方法在檢測完資訊後便進行註冊動作,下面我們分析註冊方法Register

Micro服務在註冊時有兩個關鍵點,後設資料、自定義handler

服務向中心註冊一般可以分為如下幾個步驟:

1.解析註冊中心地址

2.準備後設資料

3.宣告節點資訊

4.宣告endpoint handlers

5.宣告服務

6.註冊

整個流程我們縮略成一個二維集合圖:

registry-pie

接下來我們分析一下注冊流程程式碼,大家請配合上面的集合圖閱讀,方便理解

func (s *rpcServer) Register() error {
        // 解析註冊中心地址
        // 忽略這部分程式碼

        // 準備後設資料
        md := make(metadata.Metadata)
        for k, v := range config.Metadata {
                md[k] = v
        }

        // 宣告節點資訊
        node := &registry.Node{
                Id:       config.Name + "-" + config.Id,
                Address:  addr,
                Port:     port,
                Metadata: md,
        }

        node.Metadata["transport"] = config.Transport.String()
        node.Metadata["broker"] = config.Broker.String()
        node.Metadata["server"] = s.String()
        node.Metadata["registry"] = config.Registry.String()
        node.Metadata["protocol"] = "mucp"

        s.RLock()

        // 宣告endpoint,map元素順序是隨機的,故而使用key排序,方便每個同名服務之間顯示一致
        var handlerList []string
        for n, e := range s.handlers { 
                if !e.Options().Internal {
                        handlerList = append(handlerList, n)
                }
        }
        sort.Strings(handlerList)

        var endpoints []*registry.Endpoint
        for _, n := range handlerList {
                endpoints = append(endpoints, s.handlers[n].Endpoints()...)
        }

        // 忽略部分程式碼

        // 宣告服務資訊
        service := &registry.Service{
                Name:      config.Name,
                Version:   config.Version,
                Nodes:     []*registry.Node{node},
                Endpoints: endpoints,
        }

        s.Lock()
        registered := s.registered
        s.Unlock()

        // 構建註冊選項
        rOpts := []registry.RegisterOption{registry.RegisterTTL(config.RegisterTTL)}

        // 註冊
        if err := config.Registry.Register(service, rOpts...); err != nil {
                return err
        }

        // 忽略部分訂閱程式碼

        s.registered = true
}

以上便是服務向註冊中心註冊時的主要流程程式碼,整個註冊過程非常簡單。服務註冊完後,我們還要定期檢查與宣告生存週期,也即是Interval與TTL(Time-To-Live)機制。

Interval

與Register註冊一樣,Interval由服務觸發,而不是由Registry觸發,因為Registry已經暴露了Register介面,而Interval的工作只是定時重新呼叫Register方法,如果再把Interval放到其中,便會導致每個Registry實現都會有相同的Interval程式碼。

我們再回顧一下上面說到的Start方法,Start方法中除了註冊之外,還有迴圈重註冊的邏輯,這一部分就是利用Interval指定的值,不間斷重複向註冊中心註冊,以達到線上的目的:

func (s *rpcServer) Start() error {
    // 忽略部分程式碼

    go func() {
        t := new(time.Ticker)

        // 僅在宣告瞭Interval時才會執行,每隔Interval指定的時間,傳送一次訊號
        if s.opts.RegisterInterval > time.Duration(0) {
            t = time.NewTicker(s.opts.RegisterInterval)
        }

        // return error chan
        var ch chan error

    Loop:
        for {
            select {
            // 當接收到Interval訊號時重新執行註冊操作
            case <-t.C:
                s.RLock()
                registered := s.registered
                s.RUnlock()
                if err = s.opts.RegisterCheck(s.opts.Context); err != nil && registered {
                    log.Logf("Server %s-%s register check error: %s, deregister it", config.Name, config.Id, err)
                    // deregister self in case of error
                    if err := s.Deregister(); err != nil {
                        log.Logf("Server %s-%s deregister error: %s", config.Name, config.Id, err)
                    }
                } else {
                    if err := s.Register(); err != nil {
                        log.Logf("Server %s-%s register error: %s", config.Name, config.Id, err)
                    }
                }
            // 直到接收到退出訊號,才停止重註冊
            case ch = <-s.exit:
                t.Stop()
                close(exit)
                break Loop
            }
        }

        // 忽略部分解除安裝、關連線的程式碼
    }()

    return nil
}

當重註冊迴圈停止時,相當於服務不再生效,故而需要解除安裝、停止偵聽連線請求等操作。

TTL

TTL與Register不同,它由註冊元件執行,並非以服務直接呼叫。故而不同的註冊中心元件有不同的實現。我們這裡不深入討論,後繼如果有機會,我們再討論每個中心的TTL機制。

解除安裝

服務解除安裝相當於註冊的逆過程。

func (s *rpcServer) Deregister() error {
    config := s.Options()

    // 忽略部分地址解析程式碼

    node := &registry.Node{
        Id:      config.Name + "-" + config.Id,
        Address: addr,
        Port:    port,
    }

    service := &registry.Service{
        Name:    config.Name,
        Version: config.Version,
        Nodes:   []*registry.Node{node},
    }

    if err := config.Registry.Deregister(service); err != nil {
        return err
    }

    s.Lock()

    if !s.registered {
        s.Unlock()
        return nil
    }

    s.registered = false

    // 忽略部分訂閱程式碼
    return nil
}

解除安裝的過程很簡單,把服務名、版本號、節點資訊向註冊元件呼叫Deregister即可。

因為一個應用例項可能註冊多個服務,故而,我們需要將服務名傳過去,讓註冊元件停止對某個服務的偵聽工作。

總結

我們在本篇中從原始碼的角度簡單給大家介紹Go-Micro服務的註冊流程,不過,我們並沒有深入各註冊中心元件去詳解,這也超過本文的範疇,會讓文章變得很重,大家有興趣可以去檢視各註冊中心的客戶端程式碼。

Micro原始碼系列

  1. Go-Micro服務的構造過程
  2. Go-Micro註冊解讀
  3. [Go-Micro請求處理(in progress)]

Micro 中文資源

  1. 中文示例集
  2. 中文教程
  3. 中文部落格
  4. Micro服務治理控制檯

相關文章