自己動手實現 Go 的服務註冊與發現(上)

你好,我是 aoho,今天和大家分享的是動手實現 Go 的服務註冊與發現!

通過服務發現與註冊中心,可以很方便地管理系統中動態變化的服務實例信息。與此同時,它也可能成爲系統的瓶頸和故障點。因爲服務之間的調用信息來自於服務註冊與發現中心,當它不可用時,服務之間的調用可能無法正常進行。因此服務發現與註冊中心一般會多實例部署,提供高可用性和高穩定性。

我們將基於 Consul 實現 Golang Web 的服務註冊與發現。首先我們會通過原生態的方式,直接通過 HTTP 方式與 Consul 進行交互;然後我們會通過 Go Kit 框架提供的 Consul Client 接口實現與 Consul 之間的交互,並比較它們之間的不同。

Consul 的安裝與啓動

在此之前,我們首先需要搭建一個簡單的 Consul 服務,Consul 的下載地址爲 https://www.consul.io/downloads.html,根據操作系統的不同進行下載。在 Unix 環境下 (Mac、Linux),下載下來的文件是一個二進制可執行文件,可以直接通過它執行 Consul 的相關命令。Window 環境下是一個 .exe 的可執行文件。

以筆者自身的 Linux 環境爲例,直接在 consul 文件所在的目錄執行:

./consul version

能夠直接獲取到剛纔下載的 consul 的版本:

Consul v1.5.1
Protocol 2 spoken by default,
understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

如果我們想要將 consul 歸於系統命令下,可以使用以下命令將 consul 移動到 /usr/local/bin 文件下:

sudo mv consul /usr/local/bin/

接着我們通過以下命令啓動 Consul:

consul agent -dev

-dev 選項說明 Consul 以開發模式啓動,該模式下會快速部署一個單節點的 Consul 服務,部署好的節點既是 Server 也是 Leader。在生產環境不建議以這種模式啓動,因爲它不會持久化任何數據,數據僅存在於內存中。

啓動好之後就可以在瀏覽器訪問 http://localhost:8500 地址,如圖所示:

服務註冊與發現接口

爲了減少代碼的重複度,我們首先定義一個 Consul 客戶端接口,源碼位於 ch7-discovery/ConsulClient.go 下,代碼如下所示,

type ConsulClient interface {

 /**
  * 服務註冊接口
  * @param serviceName 服務名
  * @param instanceId 服務實例Id
  * @param instancePort 服務實例端口
  * @param healthCheckUrl 健康檢查地址
  * @param meta 服務實例元數據
  */
 Register(serviceName, instanceId, healthCheckUrl string, instancePort int, meta map[string]string, logger *log.Logger) bool

 /**
  * 服務註銷接口
  * @param instanceId 服務實例Id
  */
 DeRegister(instanceId string, logger *log.Logger) bool

 /**
  * 服務發現接口
  * @param serviceName 服務名
  */
 DiscoverServices(serviceName string) []interface{}
}

代碼中提供了三個接口,分別是:

接着我們定義一個簡單的服務 main 函數,它將啓動 Web 服務器,使用 ConsulClient 將自身服務實例元數據註冊到 Consul,提供一個 /health 端點用於健康檢查,並在服務下線時從 Consul 註銷自身。源碼位於 ch7-discovery/main/SayHelloService.go 中,代碼如下所示:

var consulClient ch7_discovery.ConsulClient
var logger *log.Logger

func main()  {

 // 1.實例化一個 Consul 客戶端,此處實例化了原生態實現版本
 consulClient = diy.New("127.0.0.1", 8500)
 // 實例失敗,停止服務
 if consulClient == nil{
  panic(0)
 }

 // 通過 go.uuid 獲取一個服務實例ID
 instanceId := uuid.NewV4().String()
 logger = log.New(os.Stderr, "", log.LstdFlags)
 // 服務註冊
 if !consulClient.Register("SayHello", instanceId, "/health", 10086, nil, logger) {
  // 註冊失敗,服務啓動失敗
  panic(0)
 }

 // 2.建立一個通道監控系統信號
 exit := make(chan os.Signal)
 // 僅監控 ctrl + c
 signal.Notify(exit, syscall.SIGINT, syscall.SIGTERM)
 var waitGroup sync.WaitGroup
 // 註冊關閉事件,等待 ctrl + c 系統信號通知服務關閉
 go closeServer(&waitGroup, exit, instanceId, logger)

 // 3. 在主線程啓動http服務器
 startHttpListener(10086)

 // 等待關閉事件執行結束,結束主線程
 waitGroup.Wait()
 log.Println("Closed the Server!")

}

在這個簡單的微服務 main 函數中,主要進行了以下的工作:

  1. 實例化 ConsulClient,調用 Register 方法完成服務註冊。註冊的服務名爲 SayHello,服務實例 ID 由 UUID 生成,健康檢查地址爲 /health,服務實例端口爲 10086;

  2. 註冊關閉事件,監控服務關閉事件。在服務關閉時調用 closeServer 方法進行服務註銷和關閉 http 服務器;

  3. 啓動 http 服務器。

在服務關閉之前,我們會調用 ConsulClient#Deregister 方法,將服務實例從 Consul  中註銷,代碼位於 closeServer 方法中,如下所示:

func closeServer( waitGroup *sync.WaitGroup, exit <-chan os.Signal, instanceId string, logger *log.Logger)  {
 // 等待關閉信息通知
 <- exit
 // 主線程等待
 waitGroup.Add(1)
 // 服務註銷
 consulClient.DeRegister(instanceId, logger)
 // 關閉 http 服務器
 err := server.Shutdown(nil)
 if err != nil{
  log.Println(err)
 }
 // 主線程可繼續執行
 waitGroup.Done()
}

closeServer 方法除了進行服務註銷,還會將本地服務的 http 服務關閉。在 startHttpListener 方法中,我們註冊了三個 http 接口,分別爲 /health 用於 Consul 的健康檢查,/sayHello 用於檢查服務是否可用,以及 /discovery 用於將從 Consul 中發現的服務實例信息打印出來,代碼如下所示:

func startHttpListener(port int)  {
 server = &http.Server{
  Addr: ch7_discovery.GetLocalIpAddress() + ":" +strconv.Itoa(port),
 }
 http.HandleFunc("/health", CheckHealth)
 http.HandleFunc("/sayHello", sayHello)
 http.HandleFunc("/discovery", discoveryService)
 err := server.ListenAndServe()
 if err != nil{
  logger.Println("Service is going to close...")
 }
}

checkHealth 用於處理來自 Consul 的健康檢查,我們這裏僅是直接簡單返回,實際使用時可以檢測實例的性能和負載情況,返回有效的健康檢查信息。代碼如下所示:

func CheckHealth(writer http.ResponseWriter, reader *http.Request) c{
 logger.Println("Health check starts!")
 _, err := fmt.Fprintln(writer, "Server is OK!")
 if err != nil{
  logger.Println(err)
 }
}

discoveryService 從請求參數中獲取 serviceName,並調用 ConsulClient#DiscoverServices 方法從 Consul 中發現對應服務的服務實例列表,然後將結果返回到 response 中。代碼如下所示:

func discoveryService(writer http.ResponseWriter, reader *http.Request)  {
 serviceName := reader.URL.Query().Get("serviceName")
 instances := consulClient.DiscoverServices(serviceName)
 writer.Header().Set("Content-Type""application/json")
 err := json.NewEncoder(writer).Encode(instances)
 if err != nil{
  logger.Println(err)
 }
}

瞭解完整個微服務結構,我們將開始編寫核心的 ConsulClient 接口的實現,完成這個簡單微服務和 Consul 之間服務註冊與發現的流程。

小結

僅有服務註冊與發現中心是不夠,還需要各個服務實例的鼎力配合,整個服務註冊與發現體系才能良好運作。一個服務實例需要完成以下的事情:

下面的文章將會繼續實現微服務與 Consul 的註冊與服務查詢等交互。

完整代碼,從我的 Github 獲取,https://github.com/longjoy/micro-go-book

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zIFzkfz8fEIsYm2JhlCWxw