服務發現組件之 — Eureka

  • 2020 年 3 月 18 日
  • 筆記

前言

現在流行的微服務體系結構正在改變我們構建應用程式的方式,從單一的單體服務轉變為越來越小的可單獨部署的服務(稱為微服務),共同構成了我們的應用程式。當進行一個業務時不可避免就會存在多個服務之間調用,假如一個服務 A 要訪問在另一台伺服器部署的服務 B,那麼前提是服務 A 要知道服務 B 所在機器的 IP 地址和服務對應的埠,最簡單的方式就是讓服務 A 自己去維護一份服務 B 的配置(包含 IP 地址和埠等資訊),但是這種方式有幾個明顯的缺點:隨著我們調用服務數量的增加,配置文件該如何維護;缺乏靈活性,如果服務 B 改變 IP 地址或者埠,服務 A 也要修改相應的文件配置;還有一個就是進行服務的動態擴容或縮小不方便。
一個比較好的解決方案就是 服務發現(Service Discovery)。它抽象出來了一個註冊中心,當一個新的服務上線時,它會將自己的 IP 和埠註冊到註冊中心去,會對註冊的服務進行定期的心跳檢測,當發現服務狀態異常時將其從註冊中心剔除下線。服務 A 只要從註冊中心中獲取服務 B 的資訊即可,即使當服務 B 的 IP 或者埠變更了,服務 A 也無需修改,從一定程度上解耦了服務。服務發現目前業界有很多開源的實現,比如 apachezookeeperNetflixeurekahashicorpconsulCoreOSetcd

Eureka 是什麼

Eurekagithub 上對其的定義為

Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers.
At Netflix, Eureka is used for the following purposes apart from playing a critical part in mid-tier load balancing.

Eureka 是由 Netflix 公司開源,採用的是 Client / Server 模式進行設計,基於 http 協議和使用 Restful Api 開發的服務註冊與發現組件,提供了完整的服務註冊和服務發現,可以和 Spring Cloud 無縫集成。其中 Server 端扮演著服務註冊中心的角色,主要是為 Client 端提供服務註冊和發現等功能,維護著 Client 端的服務註冊資訊,同時定期心跳檢測已註冊的服務當不可用時將服務剔除下線,Client 端可以通過 Server 端獲取自身所依賴服務的註冊資訊,從而完成服務間的調用。遺憾的是從其官方的 github wiki 可以發現,2.0 版本已經不再開源。但是不影響我們對其進行深入了解,畢竟服務註冊、服務發現相對來說還是比較基礎和通用的,其它開源實現框架的思想也是想通的。

服務註冊中心(Eureka Server)

我們在項目中引入 Eureka Server 的相關依賴,然後在啟動類加上註解 @EnableEurekaServer,就可以將其作為註冊中心,啟動服務後訪問頁面如下:

eureka-server-homepage.png

我們繼續添加兩個模組 service-providerservice-consumer,然後在啟動類加上註解 @EnableEurekaClient 並指定註冊中心地址為我們剛剛啟動的 Eureka Server,再次訪問可以看到兩個服務都已經註冊進來了。

instance-registered-currently.png

Demo 倉庫地址:https://github.com/mghio/depth-in-springcloud

可以看到 Eureka 的使用非常簡單,只需要添加幾個註解和配置就實現了服務註冊和服務發現,接下來我們看看它是如何實現這些功能的。

服務註冊(Register)

註冊中心提供了服務註冊介面,用於當有新的服務啟動後進行調用來實現服務註冊,或者心跳檢測到服務狀態異常時,變更對應服務的狀態。服務註冊就是發送一個 POST 請求帶上當前實例資訊到類 ApplicationResourceaddInstance 方法進行服務註冊。

eureka-server-applicationresource-addinstance.png

可以看到方法調用了類 PeerAwareInstanceRegistryImplregister 方法,該方法主要分為兩步:

  1. 調用父類 AbstractInstanceRegistryregister 方法把當前服務註冊到註冊中心
  2. 調用 replicateToPeers 方法使用非同步的方式向其它的 Eureka Server 節點同步服務註冊資訊

服務註冊資訊保存在一個嵌套的 map 中,它的結構如下:

eureka-server-registry-structure.png

第一層 mapkey 是應用名稱(對應 Demo 里的 SERVICE-PROVIDER),第二層 mapkey 是應用對應的實例名稱(對應 Demo 里的 mghio-mbp:service-provider:9999),一個應用可以有多個實例,主要調用流程如下圖所示:

eureka-server-register-sequence-chart.png

服務續約(Renew)

服務續約會由服務提供者(比如 Demo 中的 service-provider)定期調用,類似於心跳,用來告知註冊中心 Eureka Server 自己的狀態,避免被 Eureka Server 認為服務時效將其剔除下線。服務續約就是發送一個 PUT 請求帶上當前實例資訊到類 InstanceResourcerenewLease 方法進行服務續約操作。

eureka-server-instanceresource-renew.png

進入到 PeerAwareInstanceRegistryImplrenew 方法可以看到,服務續約步驟大體上和服務註冊一致,先更新當前 Eureka Server 節點的狀態,服務續約成功後再用非同步的方式同步狀態到其它 Eureka Server 節上,主要調用流程如下圖所示:

eureka-server-renew-sequence-chart.png

服務下線(Cancel)

當服務提供者(比如 Demo 中的 service-provider)停止服務時,會發送請求告知註冊中心 Eureka Server 進行服務剔除下線操作,防止服務消費者從註冊中心調用到不存在的服務。服務下線就是發送一個 DELETE 請求帶上當前實例資訊到類 InstanceResourcecancelLease 方法進行服務剔除下線操作。

eureka-server-instanceresource-cancellease.png

進入到 PeerAwareInstanceRegistryImplcancel 方法可以看到,服務續約步驟大體上和服務註冊一致,先在當前 Eureka Server 節點剔除下線該服務,服務下線成功後再用非同步的方式同步狀態到其它 Eureka Server 節上,主要調用流程如下圖所示:

eureka-server-cancellease-sequence-chart.png

服務剔除(Eviction)

服務剔除是註冊中心 Eureka Server 在啟動時就啟動一個守護執行緒 evictionTimer 來定期(默認為 60 秒)執行檢測服務的,判斷標準就是超過一定時間沒有進行 Renew 的服務,默認的失效時間是 90 秒,也就是說當一個已註冊的服務在 90 秒內沒有向註冊中心 Eureka Server 進行服務續約(Renew),就會被從註冊中心剔除下線。失效時間可以通過配置 eureka.instance.leaseExpirationDurationInSeconds 進行修改,定期執行檢測服務可以通過配置 eureka.server.evictionIntervalTimerInMs 進行修改,主要調用流程如下圖所示:

eureka-server-evict-sequence-chart.png

服務提供者(Service Provider)

對於服務提供方(比如 Demo 中的 service-provider 服務)來說,主要有三大類操作,分別為 服務註冊(Register)服務續約(Renew)服務下線(Cancel),接下來看看這三個操作是如何實現的。

服務註冊(Register)

一個服務要對外提供服務,首先要在註冊中心 Eureka Server 進行服務相關資訊註冊,能進行這一步的前提是你要配置 eureka.client.register-with-eureka=true,這個默認值為 true,註冊中心不需要把自己註冊到註冊中心去,把這個配置設為 false,這個調用比較簡單,主要調用流程如下圖所示:

service-provider-register-sequence-chart.png

服務續約(Renew)

服務續約是由服務提供者方定期(默認為 30 秒)發起心跳的,主要是用來告知註冊中心 Eureka Server 自己狀態是正常的還活著,可以通過配置 eureka.instance.lease-renewal-interval-in-seconds 來修改,當然服務續約的前提是要配置 eureka.client.register-with-eureka=true,將該服務註冊到註冊中心中去,主要調用流程如下圖所示:

service-provider-renew-sequence-chart.png

服務下線(Cancel)

當服務提供者方服務停止時,要發送 DELETE 請求告知註冊中心 Eureka Server 自己已經下線,好讓註冊中心將自己剔除下線,防止服務消費方從註冊中心獲取到不可用的服務。這個過程實現比較簡單,在類 DiscoveryClientshutdown 方法加上註解 @PreDestroy,當服務停止時會自動觸發服務剔除下線,執行服務下線邏輯,主要調用流程如下圖所示:

service-provider-cancel-sequence-chart.png

服務消費者(Service Consumer)

這裡的服務消費者如果不需要被其它服務調用的話,其實只會涉及到兩個操作,分別是從註冊中心 獲取服務列表(Fetch)更新服務列表(Update)。如果同時也需要註冊到註冊中心對外提供服務的話,那麼剩下的過程和上文提到的服務提供者是一致的,這裡不再闡述,接下來看看這兩個操作是如何實現的。

獲取服務列表(Fetch)

服務消費者方啟動之後首先肯定是要先從註冊中心 Eureka Server 獲取到可用的服務列表同時本地也會快取一份。這個獲取服務列表的操作是在服務啟動後 DiscoverClient 類實例化的時候執行的。

service-consumer-fetchregistry.png

可以看出,能發生這個獲取服務列表的操作前提是要保證配置了 eureka.client.fetch-registry=true,該配置的默認值為 true,主要調用流程如下圖所示:

service-consumer-fetch-sequence-chart.png

更新服務列表(Update)

由上面的 獲取服務列表(Fetch) 操作過程可知,本地也會快取一份,所以這裡需要定期的去到註冊中心 Eureka Server 獲取服務的最新配置,然後比較更新本地快取,這個更新的間隔時間可以通過配置 eureka.client.registry-fetch-interval-seconds 修改,默認為 30 秒,能進行這一步更新服務列表的前提是你要配置 eureka.client.register-with-eureka=true,這個默認值為 true。主要調用流程如下圖所示:

service-consumer-update-sequence-chart.png

總結

工作中項目使用的是 Spring Cloud 技術棧,它有一套非常完善的開源程式碼來整合 Eureka,使用起來非常方便。之前都是直接加註解和修改幾個配置屬性一氣呵成的,沒有深入了解過源碼實現,本文主要是闡述了服務註冊、服務發現等相關過程和實現方式,對 Eureka 服務發現組件有了更近一步的了解。


參考文章
Netflix Eureka
Service Discovery in a Microservices Architecture