前言
現(xiàn)在流行的微服務(wù)體系結(jié)構(gòu)正在改變我們構(gòu)建應(yīng)用程序的方式,從單一的單體服務(wù)轉(zhuǎn)變?yōu)樵絹碓叫〉目蓡为?dú)部署的服務(wù)(稱為微服務(wù)),共同構(gòu)成了我們的應(yīng)用程序。當(dāng)進(jìn)行一個(gè)業(yè)務(wù)時(shí)不可避免就會(huì)存在多個(gè)服務(wù)之間調(diào)用,假如一個(gè)服務(wù) A 要訪問在另一臺服務(wù)器部署的服務(wù) B,那么前提是服務(wù) A 要知道服務(wù) B 所在機(jī)器的 IP 地址和服務(wù)對應(yīng)的端口,最簡單的方式就是讓服務(wù) A 自己去維護(hù)一份服務(wù) B 的配置(包含 IP 地址和端口等信息),但是這種方式有幾個(gè)明顯的缺點(diǎn):隨著我們調(diào)用服務(wù)數(shù)量的增加,配置文件該如何維護(hù);缺乏靈活性,如果服務(wù) B 改變 IP 地址或者端口,服務(wù) A 也要修改相應(yīng)的文件配置;還有一個(gè)就是進(jìn)行服務(wù)的動(dòng)態(tài)擴(kuò)容或縮小不方便。
一個(gè)比較好的解決方案就是 服務(wù)發(fā)現(xiàn)(Service Discovery)。它抽象出來了一個(gè)注冊中心,當(dāng)一個(gè)新的服務(wù)上線時(shí),它會(huì)將自己的 IP 和端口注冊到注冊中心去,會(huì)對注冊的服務(wù)進(jìn)行定期的心跳檢測,當(dāng)發(fā)現(xiàn)服務(wù)狀態(tài)異常時(shí)將其從注冊中心剔除下線。服務(wù) A 只要從注冊中心中獲取服務(wù) B 的信息即可,即使當(dāng)服務(wù) B 的 IP 或者端口變更了,服務(wù) A 也無需修改,從一定程度上解耦了服務(wù)。服務(wù)發(fā)現(xiàn)目前業(yè)界有很多開源的實(shí)現(xiàn),比如 apache 的 zookeeper、 Netflix 的 eureka、hashicorp 的 consul、 CoreOS 的 etcd。
Eureka 是什么
Eureka 在 github 上對其的定義為
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 模式進(jìn)行設(shè)計(jì),基于 http 協(xié)議和使用 Restful Api 開發(fā)的服務(wù)注冊與發(fā)現(xiàn)組件,提供了完整的服務(wù)注冊和服務(wù)發(fā)現(xiàn),可以和 Spring Cloud 無縫集成。其中 Server 端扮演著服務(wù)注冊中心的角色,主要是為 Client 端提供服務(wù)注冊和發(fā)現(xiàn)等功能,維護(hù)著 Client 端的服務(wù)注冊信息,同時(shí)定期心跳檢測已注冊的服務(wù)當(dāng)不可用時(shí)將服務(wù)剔除下線,Client 端可以通過 Server 端獲取自身所依賴服務(wù)的注冊信息,從而完成服務(wù)間的調(diào)用。遺憾的是從其官方的 github wiki 可以發(fā)現(xiàn),2.0 版本已經(jīng)不再開源。但是不影響我們對其進(jìn)行深入了解,畢竟服務(wù)注冊、服務(wù)發(fā)現(xiàn)相對來說還是比較基礎(chǔ)和通用的,其它開源實(shí)現(xiàn)框架的思想也是想通的。
服務(wù)注冊中心(Eureka Server)
我們在項(xiàng)目中引入 Eureka Server 的相關(guān)依賴,然后在啟動(dòng)類加上注解 @EnableEurekaServer,就可以將其作為注冊中心,啟動(dòng)服務(wù)后訪問頁面如下:

我們繼續(xù)添加兩個(gè)模塊 service-provider,service-consumer,然后在啟動(dòng)類加上注解 @EnableEurekaClient 并指定注冊中心地址為我們剛剛啟動(dòng)的 Eureka Server,再次訪問可以看到兩個(gè)服務(wù)都已經(jīng)注冊進(jìn)來了。

Demo 倉庫地址:https://github.com/mghio/depth-in-springcloud
可以看到 Eureka 的使用非常簡單,只需要添加幾個(gè)注解和配置就實(shí)現(xiàn)了服務(wù)注冊和服務(wù)發(fā)現(xiàn),接下來我們看看它是如何實(shí)現(xiàn)這些功能的。
服務(wù)注冊(Register)
注冊中心提供了服務(wù)注冊接口,用于當(dāng)有新的服務(wù)啟動(dòng)后進(jìn)行調(diào)用來實(shí)現(xiàn)服務(wù)注冊,或者心跳檢測到服務(wù)狀態(tài)異常時(shí),變更對應(yīng)服務(wù)的狀態(tài)。服務(wù)注冊就是發(fā)送一個(gè) POST 請求帶上當(dāng)前實(shí)例信息到類 ApplicationResource 的 addInstance 方法進(jìn)行服務(wù)注冊。

可以看到方法調(diào)用了類 PeerAwareInstanceRegistryImpl 的 register 方法,該方法主要分為兩步:
- 調(diào)用父類
AbstractInstanceRegistry的register方法把當(dāng)前服務(wù)注冊到注冊中心 - 調(diào)用
replicateToPeers方法使用異步的方式向其它的Eureka Server節(jié)點(diǎn)同步服務(wù)注冊信息
服務(wù)注冊信息保存在一個(gè)嵌套的 map 中,它的結(jié)構(gòu)如下:

第一層 map 的 key 是應(yīng)用名稱(對應(yīng) Demo 里的 SERVICE-PROVIDER),第二層 map 的 key 是應(yīng)用對應(yīng)的實(shí)例名稱(對應(yīng) Demo 里的 mghio-mbp:service-provider:9999),一個(gè)應(yīng)用可以有多個(gè)實(shí)例,主要調(diào)用流程如下圖所示:

服務(wù)續(xù)約(Renew)
服務(wù)續(xù)約會(huì)由服務(wù)提供者(比如 Demo 中的 service-provider)定期調(diào)用,類似于心跳,用來告知注冊中心 Eureka Server 自己的狀態(tài),避免被 Eureka Server 認(rèn)為服務(wù)時(shí)效將其剔除下線。服務(wù)續(xù)約就是發(fā)送一個(gè) PUT 請求帶上當(dāng)前實(shí)例信息到類 InstanceResource 的 renewLease 方法進(jìn)行服務(wù)續(xù)約操作。

進(jìn)入到 PeerAwareInstanceRegistryImpl 的 renew 方法可以看到,服務(wù)續(xù)約步驟大體上和服務(wù)注冊一致,先更新當(dāng)前 Eureka Server 節(jié)點(diǎn)的狀態(tài),服務(wù)續(xù)約成功后再用異步的方式同步狀態(tài)到其它 Eureka Server 節(jié)上,主要調(diào)用流程如下圖所示:

服務(wù)下線(Cancel)
當(dāng)服務(wù)提供者(比如 Demo 中的 service-provider)停止服務(wù)時(shí),會(huì)發(fā)送請求告知注冊中心 Eureka Server 進(jìn)行服務(wù)剔除下線操作,防止服務(wù)消費(fèi)者從注冊中心調(diào)用到不存在的服務(wù)。服務(wù)下線就是發(fā)送一個(gè) DELETE 請求帶上當(dāng)前實(shí)例信息到類 InstanceResource 的 cancelLease 方法進(jìn)行服務(wù)剔除下線操作。

進(jìn)入到 PeerAwareInstanceRegistryImpl 的 cancel 方法可以看到,服務(wù)續(xù)約步驟大體上和服務(wù)注冊一致,先在當(dāng)前 Eureka Server 節(jié)點(diǎn)剔除下線該服務(wù),服務(wù)下線成功后再用異步的方式同步狀態(tài)到其它 Eureka Server 節(jié)上,主要調(diào)用流程如下圖所示:

服務(wù)剔除(Eviction)
服務(wù)剔除是注冊中心 Eureka Server 在啟動(dòng)時(shí)就啟動(dòng)一個(gè)守護(hù)線程 evictionTimer 來定期(默認(rèn)為 60 秒)執(zhí)行檢測服務(wù)的,判斷標(biāo)準(zhǔn)就是超過一定時(shí)間沒有進(jìn)行 Renew 的服務(wù),默認(rèn)的失效時(shí)間是 90 秒,也就是說當(dāng)一個(gè)已注冊的服務(wù)在 90 秒內(nèi)沒有向注冊中心 Eureka Server 進(jìn)行服務(wù)續(xù)約(Renew),就會(huì)被從注冊中心剔除下線。失效時(shí)間可以通過配置 eureka.instance.leaseExpirationDurationInSeconds 進(jìn)行修改,定期執(zhí)行檢測服務(wù)可以通過配置 eureka.server.evictionIntervalTimerInMs 進(jìn)行修改,主要調(diào)用流程如下圖所示:

服務(wù)提供者(Service Provider)
對于服務(wù)提供方(比如 Demo 中的 service-provider 服務(wù))來說,主要有三大類操作,分別為 服務(wù)注冊(Register)、服務(wù)續(xù)約(Renew)、服務(wù)下線(Cancel),接下來看看這三個(gè)操作是如何實(shí)現(xiàn)的。
服務(wù)注冊(Register)
一個(gè)服務(wù)要對外提供服務(wù),首先要在注冊中心 Eureka Server 進(jìn)行服務(wù)相關(guān)信息注冊,能進(jìn)行這一步的前提是你要配置 eureka.client.register-with-eureka=true,這個(gè)默認(rèn)值為 true,注冊中心不需要把自己注冊到注冊中心去,把這個(gè)配置設(shè)為 false,這個(gè)調(diào)用比較簡單,主要調(diào)用流程如下圖所示:

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

服務(wù)下線(Cancel)
當(dāng)服務(wù)提供者方服務(wù)停止時(shí),要發(fā)送 DELETE 請求告知注冊中心 Eureka Server 自己已經(jīng)下線,好讓注冊中心將自己剔除下線,防止服務(wù)消費(fèi)方從注冊中心獲取到不可用的服務(wù)。這個(gè)過程實(shí)現(xiàn)比較簡單,在類 DiscoveryClient 的 shutdown 方法加上注解 @PreDestroy,當(dāng)服務(wù)停止時(shí)會(huì)自動(dòng)觸發(fā)服務(wù)剔除下線,執(zhí)行服務(wù)下線邏輯,主要調(diào)用流程如下圖所示:

服務(wù)消費(fèi)者(Service Consumer)
這里的服務(wù)消費(fèi)者如果不需要被其它服務(wù)調(diào)用的話,其實(shí)只會(huì)涉及到兩個(gè)操作,分別是從注冊中心 獲取服務(wù)列表(Fetch) 和 更新服務(wù)列表(Update)。如果同時(shí)也需要注冊到注冊中心對外提供服務(wù)的話,那么剩下的過程和上文提到的服務(wù)提供者是一致的,這里不再闡述,接下來看看這兩個(gè)操作是如何實(shí)現(xiàn)的。
獲取服務(wù)列表(Fetch)
服務(wù)消費(fèi)者方啟動(dòng)之后首先肯定是要先從注冊中心 Eureka Server 獲取到可用的服務(wù)列表同時(shí)本地也會(huì)緩存一份。這個(gè)獲取服務(wù)列表的操作是在服務(wù)啟動(dòng)后 DiscoverClient 類實(shí)例化的時(shí)候執(zhí)行的。

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

更新服務(wù)列表(Update)
由上面的 獲取服務(wù)列表(Fetch) 操作過程可知,本地也會(huì)緩存一份,所以這里需要定期的去到注冊中心 Eureka Server 獲取服務(wù)的最新配置,然后比較更新本地緩存,這個(gè)更新的間隔時(shí)間可以通過配置 eureka.client.registry-fetch-interval-seconds 修改,默認(rèn)為 30 秒,能進(jìn)行這一步更新服務(wù)列表的前提是你要配置 eureka.client.register-with-eureka=true,這個(gè)默認(rèn)值為 true。主要調(diào)用流程如下圖所示:

總結(jié)
工作中項(xiàng)目使用的是 Spring Cloud 技術(shù)棧,它有一套非常完善的開源代碼來整合 Eureka,使用起來非常方便。之前都是直接加注解和修改幾個(gè)配置屬性一氣呵成的,沒有深入了解過源碼實(shí)現(xiàn),本文主要是闡述了服務(wù)注冊、服務(wù)發(fā)現(xiàn)等相關(guān)過程和實(shí)現(xiàn)方式,對 Eureka 服務(wù)發(fā)現(xiàn)組件有了更近一步的了解。
參考文章
Netflix Eureka
Service Discovery in a Microservices Architecture