▌文章的主要內容包括: * 服務注冊/發現 * 服務注冊表 * 健康檢查
在上一篇文章的開始,我們提到解決微服務架構中的通信問題,基本只要解決下面三個問題:
- 服務網路通信能力
- 服務間的資料互動格式
- 服務間如何相互發現與呼叫
網路的互通保證了服務之間是可以通信的,通過對JSON 的序列化和反序列化來實作網路請求中的資料互動,數智化效能平臺豬齒魚Choerodon 的 API 網關則統一了所有來自客戶端的請求,并將請求路由到具體的后端服務上,然而這里就會有一個疑問,API 網關是如何與后端服務保持通信的,后端服務之間又是如何來進行通信的?當然我們能想到最簡單的方式就是通過 URL + 埠的形式直接訪問(例如:http://127.0.0.1:8080/v1/hello),
在實際的生產中,我們認為這種方式應該是被避免的,因為 Choerodon 的每個服務實體都部署在K8S的不同 pod中,每一個服務實體的 IP 地址和埠都可以改變,同時服務間相互呼叫的介面地址如何管理,服務本身集群化后又是如何進行負載均衡,這些都是我們需要考慮的,
為了解決這個問題,自然就想到了微服務架構中的注冊中心,一個注冊中心應該包含下面幾個部分:
- 服務注冊/發現:服務注冊是微服務啟動時,將自己的資訊注冊到注冊中心的程序,服務發現是注冊中心監聽所有可用微服務,查詢串列及其網路地址,
- 服務注冊表:用來紀錄各個微服務的資訊,
- 服務檢查:注冊中心使用一定的機制定時檢測已注冊的服務,如果發現某實體長時間無法訪問,就會從服務注冊表中移除該實體,
Choerodon 中服務注冊的程序如下圖所示:

服務注冊/發現
當我們通過介面去呼叫其他服務時,呼叫方則需要知道對應服務實體的 IP 地址和埠,對于傳統的應用而言,服務實體的網路地址是相對不變的,這樣可以通過固定的組態檔來讀取網路地址,很容易地使用 HTTP/REST 呼叫另一個服務的介面,
但是在微服務架構中,服務實體的網路地址是動態分配的,而且當服務進行自動擴展,更新等操作時,服務實體的網路地址則會經常變化,這樣我們的客戶端則需要一套精確地服務發現機制,
Eureka 是 Netflix 開源的服務發現組件,本身是一個基于REST的服務,它包含 Server 和 Client 兩部分,
Eureka Server 用作服務注冊服務器,提供服務發現的能力,當一個服務實體被啟動時,會向 Eureka Server 注冊自己的資訊(例如IP、埠、微服務名稱等),這些資訊會被寫到注冊表上;當服務實體終止時,再從注冊表中洗掉,這個服務實體的注冊表通過心跳機制動態重繪,這個程序就是服務注冊,當服務實體注冊到注冊中心以后,也就相當于注冊中心發現了服務實體,完成了服務注冊/發現的程序,
閱讀 Spring Cloud Eureka 的原始碼可以看到,在eureka-client-1.6.2.jar 的包中,com.netflix.discovery. DiscoveryClient 啟動的時候,會初始化一個定時任務,定時的把本地的服務配置資訊,即需要注冊到遠端的服務資訊自動重繪到注冊服務器上,該類包含了Eureka Client 向 Eureka Server 注冊的相關方法,
在 DiscoveryClient 類有一個服務注冊的方法 register(),該方法是通過 HTTP 請求向Eureka Server 注冊,其代碼如下:
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
對于 Choerodon 而言,客戶端依舊采用 Eureka Client,而服務端采用 GoLang 撰寫,結合 K8S,通過主動監聽 K8S 下 pod 的啟停,發現服務實體上線,Eureka Client 則通過 HTTP 請求獲取注冊表,來實作服務注冊/發現程序,
注冊中心啟動時,會構造一個podController,用來監聽pod 的生命周期,代碼如下:
func Run(s *options.ServerRunOptions, stopCh <-chan struct{}) error {
... ...
podController := controller.NewController(kubeClient, kubeInformerFactory, appRepo)
go kubeInformerFactory.Start(stopCh)
go podController.Run(instance, stopCh, lockSingle)
return registerServer.PrepareRun().Run(appRepo, stopCh)
}
在github.com/choerodon/go-register-server/controller/controller.go 中定義了 Controller,提供了 Run() 方法,該方法會啟動兩個行程,用來監聽環境變數 REGISTER_SERVICE_NAMESPACE 中配置的對應 namespace 中的pod,然后在pod 啟動時,將 pod 資訊轉化為自定義的服務注冊資訊,存盤起來,在 pod 下線時,從存盤中洗掉服務資訊,其代碼如下:
func (c *Controller) syncHandler(key string, instance chan apps.Instance, lockSingle apps.RefArray) (bool, error) {
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return true, nil
}
pod, err := c.podsLister.Pods(namespace).Get(name)
if err != nil {
if errors.IsNotFound(err) {
if ins := c.appRepo.DeleteInstance(key); ins != nil {
ins.Status = apps.DOWN
if lockSingle[0] > 0 {
glog.Info("create down event for ", key)
instance <- *ins
}
}
runtime.HandleError(fmt.Errorf("pod '%s' in work queue no longer exists", key))
return true, nil
}
return false, err
}
_, isContainServiceLabel := pod.Labels[ChoerodonServiceLabel]
_, isContainVersionLabel := pod.Labels[ChoerodonVersionLabel]
_, isContainPortLabel := pod.Labels[ChoerodonPortLabel]
if !isContainServiceLabel || !isContainVersionLabel || !isContainPortLabel {
return true, nil
}
if pod.Status.ContainerStatuses == nil {
return true, nil
}
if container := pod.Status.ContainerStatuses[0]; container.Ready && container.State.Running != nil && len(pod.Spec.Containers) > 0 {
if in := convertor.ConvertPod2Instance(pod); c.appRepo.Register(in, key) {
ins := *in
ins.Status = apps.UP
if lockSingle[0] > 0 {
glog.Info("create up event for ", key)
instance <- ins
}
}
} else {
if ins := c.appRepo.DeleteInstance(key); ins != nil {
ins.Status = apps.DOWN
if lockSingle[0] > 0 {
glog.Info("create down event for ", key)
instance <- *ins
}
}
}
return true, nil
}
github.com/choerodon/go-register-server/eureka/repository/repository 中的 ApplicationRepository 提供了 Register() 方法,該方法手動將服務的資訊作為注冊表存盤在注冊中心中,
func (appRepo *ApplicationRepository) Register(instance *apps.Instance, key string) bool {
if _, ok := appRepo.namespaceStore.Load(key); ok {
return false
} else {
appRepo.namespaceStore.Store(key, instance.InstanceId)
}
appRepo.instanceStore.Store(instance.InstanceId, instance)
return true
}
通過上面的代碼我們可以了解到Choerodon 注冊中心是如何實作服務注冊的,有了注冊中心后,下面我們來介紹下服務發現中的服務注冊表,
服務注冊表
在微服務架構中,服務注冊表是一個很關鍵的系統組件,當服務向注冊中心的其他服務發出請求時,請求呼叫方需要獲取注冊中心的服務實體,知道所有服務實體的請求地址,
Choerodon 沿用 Spring Cloud Eureka 的模式,由注冊中心保存服務注冊表,同時客戶端快取一份服務注冊表,每經過一段時間去注冊中心拉取最新的注冊表,
在github.com/choerodon/go-register-server/eureka/apps/types 中定義了Instance 物件,宣告了一個微服務實體包含的欄位,代碼如下:
type Instance struct {
InstanceId string `xml:"instanceId" json:"instanceId"`
HostName string `xml:"hostName" json:"hostName"`
App string `xml:"app" json:"app"`
IPAddr string `xml:"ipAddr" json:"ipAddr"`
Status StatusType `xml:"status" json:"status"`
OverriddenStatus StatusType `xml:"overriddenstatus" json:"overriddenstatus"`
Port Port `xml:"port" json:"port"`
SecurePort Port `xml:"securePort" json:"securePort"`
CountryId uint64 `xml:"countryId" json:"countryId"`
DataCenterInfo DataCenterInfo `xml:"dataCenterInfo" json:"dataCenterInfo"`
LeaseInfo LeaseInfo `xml:"leaseInfo" json:"leaseInfo"`
Metadata map[string]string `xml:"metadata" json:"metadata"`
HomePageUrl string `xml:"homePageUrl" json:"homePageUrl"`
StatusPageUrl string `xml:"statusPageUrl" json:"statusPageUrl"`
HealthCheckUrl string `xml:"healthCheckUrl" json:"healthCheckUrl"`
VipAddress string `xml:"vipAddress" json:"vipAddress"`
SecureVipAddress string `xml:"secureVipAddress" json:"secureVipAddress"`
IsCoordinatingDiscoveryServer bool `xml:"isCoordinatingDiscoveryServer" json:"isCoordinatingDiscoveryServer"`
LastUpdatedTimestamp uint64 `xml:"lastUpdatedTimestamp" json:"lastUpdatedTimestamp"`
LastDirtyTimestamp uint64 `xml:"lastDirtyTimestamp" json:"lastDirtyTimestamp"`
ActionType string `xml:"actionType" json:"actionType"`
}
客戶端可以通過訪問注冊中心的/eureka/apps 介面獲取對應的注冊表資訊,如下所示:
{
"name": "iam-service",
"instance": [
{
"instanceId": "10.233.73.39:iam-service:8030",
"hostName": "10.233.73.39",
"app": "iam-service",
"ipAddr": "10.233.73.39",
"status": "UP",
"overriddenstatus": "UNKNOWN",
"port": {
"@enabled": true,
"$": 8030
},
"securePort": {
"@enabled": false,
"$": 443
},
"countryId": 8,
"dataCenterInfo": {
"name": "MyOwn",
"@class": "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo"
},
"leaseInfo": {
"renewalIntervalInSecs": 10,
"durationInSecs": 90,
"registrationTimestamp": 1542002980,
"lastRenewalTimestamp": 1542002980,
"evictionTimestamp": 0,
"serviceUpTimestamp": 1542002980
},
"metadata": {
"VERSION": "2018.11.12-113155-master"
},
"homePageUrl": "http://10.233.73.39:8030/",
"statusPageUrl": "http://10.233.73.39:8031/info",
"healthCheckUrl": "http://10.233.73.39:8031/health",
"vipAddress": "iam-service",
"secureVipAddress": "iam-service",
"isCoordinatingDiscoveryServer": true,
"lastUpdatedTimestamp": 1542002980,
"lastDirtyTimestamp": 1542002980,
"actionType": "ADDED"
}
]
}
我們可以在服務注冊表中獲取到所有服務的 IP 地址、埠以及服務的其他資訊,通過這些資訊,服務直接就可以通過 HTTP 來進行訪問,有了注冊中心和注冊表之后,我們的注冊中心又是如何來確保服務是健康可用的,則需要通過健康檢查機制來實作,
健康檢查
在我們提供了注冊中心以及服務注冊表之后,我們還需要確保我們的服務注冊表中的資訊,與服務實際的運行狀態保持一致,需要提供一種機制來保證服務自身是可被訪問的,在Choerodon微服務架構中處理此問題的方法是提供一個健康檢查的端點,當我們通過HTTP 進行訪問時,如果能夠正常訪問,則應該回復HTTP 狀態碼200,表示健康,
Spring Boot 提供了默認的健康檢查埠,需要添加spring-boot-starter-actuator 依賴,
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
訪問/health 端點后,則會回傳如下類似的資訊表示服務的狀態,可以看到 HealthEndPoint 給我們提供默認的監控結果,包含磁盤檢測和資料庫檢測等其他資訊,
{
"status": "UP",
"diskSpace": {
"status": "UP",
"total": 398458875904,
"free": 315106918400,
"threshold": 10485760
},
"db": {
"status": "UP",
"database": "MySQL",
"hello": 1
}
}
K8S通過,/health通過K8S通過,/health未通過K8S未通過,/health通過
第一種情況,當兩種都通過的話,服務是可以被訪問的,
第二種情況,K8S 認為服務是正常運行的,但注冊中心認為服務是不健康的,注冊表中不會記錄該服務,這樣其他服務則不能獲取該服務的注冊資訊,也就不會通過介面進行服務呼叫,則服務間不能正常訪問,如下圖所示,

第三種情況,服務通過心跳告知注冊中心自己是可用的,但是可能因為網路的原因,K8S 將 pod 標識為不可訪問,這樣當其他服務來請求該服務時,則不可以訪問,這種情況下服務間也是不能正常訪問的,如下圖所示,

同時,當我們配置了管理埠之后,該端點則需要通過管理埠進行訪問,可以在組態檔中添加如下配置來修改管理埠,
management.port: 8081
但是在這種情況下,會使我們的健康檢查變得更加復雜,健康檢查并不能獲取服務真正的健康狀態,
在這種情況下,Choerodon 使用 K8S 來監聽服務的健康埠,同時需要保證服務的埠與管理埠都能被正常訪問,才算通過健康檢查,可以在部署的 deploy 檔案中添加readinessProbe 引數,
apiVersion: v1
kind: Pod
spec:
containers:
readinessProbe:
exec:
command:
- /bin/sh
- -c
- curl -s localhost:8081/health --fail && nc -z localhost 8080
failureThreshold: 3
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 10
這樣,當我們的服務啟動之后,才會被注冊中心正常的識別,當服務狀態例外時,也可以盡快的從注冊表中移除,
本文由豬齒魚技術團隊原創,轉載請注明出處
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/399512.html
標籤:其他
