自己動手實現 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{}
}
代碼中提供了三個接口,分別是:
-
Register,用於服務註冊,服務實例將自身所屬服務名和服務元數據註冊到 Consul 中;
-
DeRegister,用於服務註銷,服務關閉時請求 Consul 將自身元數據註銷,避免無效請求;
-
DiscoverServices,用於服務發現,通過服務名向 Consul 請求對應的服務實例信息列表。
接着我們定義一個簡單的服務 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 函數中,主要進行了以下的工作:
-
實例化 ConsulClient,調用 Register 方法完成服務註冊。註冊的服務名爲 SayHello,服務實例 ID 由 UUID 生成,健康檢查地址爲 /health,服務實例端口爲 10086;
-
註冊關閉事件,監控服務關閉事件。在服務關閉時調用 closeServer 方法進行服務註銷和關閉 http 服務器;
-
啓動 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