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

本文是 Choerodon 的微服務之路系列推文第三篇。在上一篇《Choerodon的微服務之路(二):微服務網關》中,介紹了Choerodon 在搭建微服務網關時考慮的一些問題以及兩種常見的微服務網關模式,而且經過代碼介紹了Choerodon 的網關是如何實現的。本篇文章將介紹Choerodon 的註冊中心,經過代碼的形式介紹 Choerodon 微服務框架中,是如何來實現服務註冊和發現的。java

▌文章的主要內容包括:git

  • 服務註冊/發現
  • 服務註冊表
  • 健康檢查

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

  • 服務網絡通訊能力
  • 服務間的數據交互格式
  • 服務間如何相互發現與調用

網絡的互通保證了服務之間是能夠通訊的,經過對JSON 的序列化和反序列化來實現網絡請求中的數據交互。Choerodon 的 API 網關則統一了全部來自客戶端的請求,並將請求路由到具體的後端服務上。然而這裏就會有一個疑問,API 網關是如何與後端服務保持通訊的,後端服務之間又是如何來進行通訊的?固然咱們能想到最簡單的方式就是經過 URL + 端口的形式直接訪問(例如:http://127.0.0.1:8080/v1/hello)。spring

在實際的生產中,咱們認爲這種方式應該是被避免的。由於 Choerodon 的每一個服務實例都部署在 K8S 的不一樣 pod 中,每個服務實例的 IP 地址和端口均可以改變。同時服務間相互調用的接口地址如何管理,服務自己集羣化後又是如何進行負載均衡。這些都是咱們須要考慮的。數據庫

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

  • 服務註冊/發現:服務註冊是微服務啓動時,將本身的信息註冊到註冊中心的過程。服務發現是註冊中心監聽全部可用微服務,查詢列表及其網絡地址。
  • 服務註冊表:用來紀錄各個微服務的信息。
  • 服務檢查:註冊中心使用必定的機制定時檢測已註冊的服務,若是發現某實例長時間沒法訪問,就會從服務註冊表中移除該實例。

Choerodon 中服務註冊的過程以下圖所示:後端

服務註冊/發現

當咱們經過接口去調用其餘服務時,調用方則須要知道對應服務實例的 IP 地址和端口。對於傳統的應用而言,服務實例的網絡地址是相對不變的,這樣能夠經過固定的配置文件來讀取網絡地址,很容易地使用 HTTP/REST 調用另外一個服務的接口。api

可是在微服務架構中,服務實例的網絡地址是動態分配的。並且當服務進行自動擴展,更新等操做時,服務實例的網絡地址則會常常變化。這樣咱們的客戶端則須要一套精確地服務發現機制。緩存

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 認爲服務是正常運行的,但註冊中心認爲服務是不健康的,註冊表中不會記錄該服務,這樣其餘服務則不能獲取該服務的註冊信息,也就不會經過接口進行服務調用。則服務間不能正常訪問,以下圖所示:

第三種狀況,服務經過心跳告知註冊中心本身是可用的,可是可能由於網絡的緣由,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

這樣,當咱們的服務啓動以後,纔會被註冊中心正常的識別。當服務狀態異常時,也能夠儘快的從註冊表中移除。

總結

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

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

Choerodon的微服務之路(二):微服務網關

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

關於Choerodon豬齒魚

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

你們也能夠經過如下社區途徑瞭解豬齒魚的最新動態、產品特性,以及參與社區貢獻:

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