Choerodon 的微服務之路(三):服務註冊與發現

Choerodon豬齒魚發表於2018-11-20
本文是 Choerodon 的微服務之路系列推文第三篇。在上一篇《Choerodon的微服務之路(二):微服務閘道器》中,介紹了Choerodon 在搭建微服務閘道器時考慮的一些問題以及兩種常見的微服務閘道器模式,並且通過程式碼介紹了Choerodon 的閘道器是如何實現的。本篇文章將介紹Choerodon 的註冊中心,通過程式碼的形式介紹 Choerodon 微服務框架中,是如何來實現服務註冊和發現的。


▌文章的主要內容包括:

  • 服務註冊/發現
  • 服務登錄檔
  • 健康檢查


在上一篇文章的開始,我們提到解決微服務架構中的通訊問題,基本只要解決下面三個問題:

  • 服務網路通訊能力
  • 服務間的資料互動格式
  • 服務間如何相互發現與呼叫

網路的互通保證了服務之間是可以通訊的,通過對JSON 的序列化和反序列化來實現網路請求中的資料互動。Choerodon 的 API 閘道器則統一了所有來自客戶端的請求,並將請求路由到具體的後端服務上。然而這裡就會有一個疑問,API 閘道器是如何與後端服務保持通訊的,後端服務之間又是如何來進行通訊的?當然我們能想到最簡單的方式就是通過 URL + 埠的形式直接訪問(例如:http://127.0.0.1:8080/v1/hello)。

在實際的生產中,我們認為這種方式應該是被避免的。因為 Choerodon 的每個服務例項都部署在 K8S 的不同 pod 中,每一個服務例項的 IP 地址和埠都可以改變。同時服務間相互呼叫的介面地址如何管理,服務本身叢集化後又是如何進行負載均衡。這些都是我們需要考慮的。

為了解決這個問題,自然就想到了微服務架構中的註冊中心。一個註冊中心應該包含下面幾個部分:

  • 服務註冊/發現:服務註冊是微服務啟動時,將自己的資訊註冊到註冊中心的過程。服務發現是註冊中心監聽所有可用微服務,查詢列表及其網路地址。
  • 服務登錄檔:用來紀錄各個微服務的資訊。
  • 服務檢查:註冊中心使用一定的機制定時檢測已註冊的服務,如果發現某例項長時間無法訪問,就會從服務登錄檔中移除該例項。

Choerodon 中服務註冊的過程如下圖所示:

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    }}複製程式碼

​但是因為 Choerodon 使用的是 K8S 作為執行環境。我們知道 K8S 提供了 liveness probes 來檢查我們的應用程式。而對於 Eureka Client 而言,服務是通過心跳來告知註冊中心自己是 UP 還是 DOWN的。這樣我們的系統中則會出現兩種檢查機制,則會出現如下幾種情況。

  • K8S 通過,/health 通過
  • K8S 通過,/health 未通過
  • K8S 未通過,/health 通過

第一種情況,當兩種都通過的話,服務是可以被訪問的。

第二種情況,K8S 認為服務是正常執行的,但註冊中心認為服務是不健康的,登錄檔中不會記錄該服務,這樣其他服務則不能獲取該服務的註冊資訊,也就不會通過介面進行服務呼叫。則服務間不能正常訪問,如下圖所示:

Choerodon 的微服務之路(三):服務註冊與發現

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

Choerodon 的微服務之路(三):服務註冊與發現

同時,當我們配置了管理埠之後,該端點則需要通過管理埠進行訪問。可以再配置檔案中新增如下配置來修改管理埠。

management.port: 8081複製程式碼

​當我們開啟管理埠後,這樣會使我們的健康檢查變得更加複雜,健康檢查並不能獲取服務真正的健康狀態。

在這種情況下,Choerodon 使用 K8S 來監聽服務的健康埠,同時需要保證服務的埠與管理埠都能被正常訪問,才算通過健康檢查。可以在部署的 deploy 檔案中新增 readinessProbe 引數。

apiVersion: v1kind: Podspec:  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複製程式碼

這樣,當我們的服務啟動之後,才會被註冊中心正常的識別。當服務狀態異常時,也可以儘快的從登錄檔中移除。

總結

回顧一下這篇文章,我們介紹了 Choerodon 的註冊中心,通過程式碼的形式介紹了 Choerodon 微服務框架中,是如何來實現服務註冊和發現的,其中 Spring Cloud 的版本為 Dalston.SR4。具體的程式碼可以參見我們的 github 地址(https://github.com/choerodon/go-register-server)。

更多關於微服務系列的文章,點選藍字可閱讀 ▼

Choerodon的微服務之路(二):微服務閘道器

Choerodon的微服務之路(一):如何邁出關鍵的第一步

關於Choerodon豬齒魚

Choerodon豬齒魚是一個開源企業服務平臺,是基於Kubernetes的容器編排和管理能力,整合DevOps工具鏈、微服務和移動應用框架,來幫助企業實現敏捷化的應用交付和自動化的運營管理的開源平臺,同時提供IoT、支付、資料、智慧洞察、企業應用市場等業務元件,致力幫助企業聚焦於業務,加速數字化轉型。

大家也可以通過以下社群途徑瞭解豬齒魚的最新動態、產品特性,以及參與社群貢獻:

歡迎加入Choerodon豬齒魚社群,共同為企業數字化服務打造一個開放的生態平臺


相關文章