Eureka服務注冊與發(fā)現(xiàn)

整體架構(gòu)圖:


image.png

Eureka結(jié)構(gòu)圖:


image.png

Eureka原理:
Eureka是Netflix開源的一款提供服務注冊和發(fā)現(xiàn)的產(chǎn)品,github地址為https://github.com/Netflix/eureka。注冊中心是分布式開發(fā)的核心組件之一,而eureka是spring cloud推薦的注冊中心實現(xiàn),因此對于Java開發(fā)同學來說,還是有必要學習eureka的,特別是其架構(gòu)及設計思想。

官方文檔定義是: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. We call this service, the Eureka Server. Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier. The client also has a built-in load balancer that does basic round-robin load balancing.
Eureka是一個REST (Representational State Transfer)服務,它主要用于AWS云,用于定位服務,以實現(xiàn)中間層服務器的負載平衡和故障轉(zhuǎn)移,我們稱此服務為Eureka服務器。Eureka也有一個基于java的客戶端組件,Eureka客戶端,這使得與服務的交互更加容易,同時客戶端也有一個內(nèi)置的負載平衡器,它執(zhí)行基本的循環(huán)負載均衡。

Eureka提供了完整的Service Registry和Service Discovery實現(xiàn),并且也經(jīng)受住了Netflix自己的生產(chǎn)環(huán)境考驗,相對使用起來會比較省心(同時Spring Cloud還有一套非常完善的開源代碼來整合Eureka,所以使用起來非常方便)

本文主要內(nèi)容有:eureka基礎概念及架構(gòu)、服務發(fā)現(xiàn)原理、eureka server/client流程分析及優(yōu)缺點分析,最后做個小結(jié)。由于本文側(cè)重于原理分析,因此eureka(結(jié)合spring cloud)的使用就不再贅述了,感興趣的小伙伴可以看下 程序猿DD 關于spring cloud的相關教程。

eureka基礎

eureka架構(gòu)圖

image
  • Eureka Server:提供服務注冊和發(fā)現(xiàn),多個Eureka Server之間會同步數(shù)據(jù),做到狀態(tài)一致(最終一致性)
  • Service Provider:服務提供方,將自身服務注冊到Eureka,從而使服務消費方能夠找到
  • Service Consumer:服務消費方,從Eureka獲取注冊服務列表,從而能夠消費服務

注意,上圖中的3個角色都是邏輯角色,在實際運行中,這幾個角色甚至可以是同一個項目(JVM進程)中。

自我保護機制

自我保護機制主要在Eureka Client和Eureka Server之間存在網(wǎng)絡分區(qū)的情況下發(fā)揮保護作用,在服務器端和客戶端都有對應實現(xiàn)。假設在某種特定的情況下(如網(wǎng)絡故障), Eureka Client和Eureka Server無法進行通信,此時Eureka Client無法向Eureka Server發(fā)起注冊和續(xù)約請求,Eureka Server中就可能因注冊表中的服務實例租約出現(xiàn)大量過期而面臨被剔除的危險,然而此時的Eureka Client可能是處于健康狀態(tài)的(可接受服務訪問),如果直接將注冊表中大量過期的服務實例租約剔除顯然是不合理的,自我保護機制提高了eureka的服務可用性。

當自我保護機制觸發(fā)時,Eureka不再從注冊列表中移除因為長時間沒收到心跳而應該過期的服務,仍能查詢服務信息并且接受新服務注冊請求,也就是其他功能是正常的。這里思考下,如果eureka節(jié)點A觸發(fā)自我保護機制過程中,有新服務注冊了然后網(wǎng)絡回復后,其他peer節(jié)點能收到A節(jié)點的新服務信息,數(shù)據(jù)同步到peer過程中是有網(wǎng)絡異常重試的,也就是說,是能保證最終一致性的。

服務發(fā)現(xiàn)原理

eureka server可以集群部署,多個節(jié)點之間會進行(異步方式)數(shù)據(jù)同步,保證數(shù)據(jù)最終一致性,Eureka Server作為一個開箱即用的服務注冊中心,提供的功能包括:服務注冊、接收服務心跳、服務剔除、服務下線等。需要注意的是,Eureka Server同時也是一個Eureka Client,在不禁止Eureka Server的客戶端行為時,它會向它配置文件中的其他Eureka Server進行拉取注冊表、服務注冊和發(fā)送心跳等操作。

eureka server端通過appNameinstanceInfoId來唯一區(qū)分一個服務實例,服務實例信息是保存在哪里呢?其實就是一個Map中:

// 第一層的key是appName,第二層的key是instanceInfoId
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry 
    = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

服務注冊

Service Provider啟動時會將服務信息(InstanceInfo)發(fā)送給eureka server,eureka server接收到之后會寫入registry中,服務注冊默認過期時間DEFAULT_DURATION_IN_SECS = 90秒。InstanceInfo寫入到本地registry之后,然后同步給其他peer節(jié)點,對應方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers

寫入本地registry

服務信息(InstanceInfo)保存在Lease中,寫入本地registry對應方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register,Lease統(tǒng)一保存在內(nèi)存的ConcurrentHashMap中,在服務注冊過程中,首先加個讀鎖,然后從registry中判斷該Lease是否已存在,如果已存在則比較lastDirtyTimestamp時間戳,取二者最大的服務信息,避免發(fā)生數(shù)據(jù)覆蓋。使用InstanceInfo創(chuàng)建一個新的InstanceInfo:

if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
    // 已存在Lease則比較時間戳,取二者最大值
    registrant = existingLease.getHolder();
}
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
    // 已存在Lease則取上次up時間戳
    lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}

public Lease(T r, int durationInSecs) {
    holder = r;
    registrationTimestamp = System.currentTimeMillis(); // 當前時間
    lastUpdateTimestamp = registrationTimestamp;
    duration = (durationInSecs * 1000);
}

不知道小伙伴看了上述方法的代碼有沒有這樣的疑問?

通過讀鎖并且 registry 的讀取和寫入不是原子的,那么在并發(fā)時其實是有可能發(fā)生數(shù)據(jù)覆蓋的,如果發(fā)生數(shù)據(jù)覆蓋豈不是有問題了!猛一看會以為臟數(shù)據(jù)不就是有問題么?換個角度想,臟數(shù)據(jù)就一定有問題么?
其實針對這個問題,eureka的處理方式是沒有問題的,該方法并發(fā)時,針對InstanceInfo Lease的構(gòu)造,二者的信息是基本一致的,因為registrationTimestamp取的就是當前時間,所以并并發(fā)的數(shù)據(jù)不會產(chǎn)生問題。

同步給其他peer

InstanceInfo寫入到本地registry之后,然后同步給其他peer節(jié)點,對應方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers。如果當前節(jié)點接收到的InstanceInfo本身就是另一個節(jié)點同步來的,則不會繼續(xù)同步給其他節(jié)點,避免形成“廣播效應”;InstanceInfo同步時會排除當前節(jié)點。

InstanceInfo的狀態(tài)有依以下幾種:Heartbeat, Register, Cancel, StatusUpdate, DeleteStatusOverride,默認情況下同步操作時批量異步執(zhí)行的,同步請求首先緩存到Map中,key為requestType+appName+id,然后由發(fā)送線程將請求發(fā)送到peer節(jié)點。

Peer之間的狀態(tài)是采用異步的方式同步的,所以不保證節(jié)點間的狀態(tài)一定是一致的,不過基本能保證最終狀態(tài)是一致的。結(jié)合服務發(fā)現(xiàn)的場景,實際上也并不需要節(jié)點間的狀態(tài)強一致。在一段時間內(nèi)(比如30秒),節(jié)點A比節(jié)點B多一個服務實例或少一個服務實例,在業(yè)務上也是完全可以接受的(Service Consumer側(cè)一般也會實現(xiàn)錯誤重試和負載均衡機制)。所以按照CAP理論,Eureka的選擇就是放棄C,選擇AP。
如果同步過程中,出現(xiàn)了異常怎么辦呢,這時會根據(jù)異常信息做對應的處理,如果是讀取超時或者網(wǎng)絡連接異常,則稍后重試;如果其他異常則打印錯誤日志不再后續(xù)處理。

服務續(xù)約

Renew(服務續(xù)約)操作由Service Provider定期調(diào)用,類似于heartbeat。主要是用來告訴Eureka Server Service Provider還活著,避免服務被剔除掉。renew接口實現(xiàn)方式和register基本一致:首先更新自身狀態(tài),再同步到其它Peer,服務續(xù)約也就是把過期時間設置為當前時間加上duration的值。

注意:服務注冊如果InstanceInfo不存在則加入,存在則更新;而服務預約只是進行更新,如果InstanceInfo不存在直接返回false。

服務下線

Cancel(服務下線)一般在Service Provider shutdown的時候調(diào)用,用來把自身的服務從Eureka Server中刪除,以防客戶端調(diào)用不存在的服務,eureka從本地”刪除“(設置為刪除狀態(tài))之后會同步給其他peer,對應方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#cancel

服務失效剔除

Eureka Server中有一個EvictionTask,用于檢查服務是否失效。Eviction(失效服務剔除)用來定期(默認為每60秒)在Eureka Server檢測失效的服務,檢測標準就是超過一定時間沒有Renew的服務。默認失效時間為90秒,也就是如果有服務超過90秒沒有向Eureka Server發(fā)起Renew請求的話,就會被當做失效服務剔除掉。失效時間可以通過eureka.instance.leaseExpirationDurationInSeconds進行配置,定期掃描時間可以通過eureka.server.evictionIntervalTimerInMs進行配置。

服務剔除#evict方法中有很多限制,都是為了保證Eureka Server的可用性:比如自我保護時期不能進行服務剔除操作、過期操作是分批進行、服務剔除是隨機逐個剔除,剔除均勻分布在所有應用中,防止在同一時間內(nèi)同一服務集群中的服務全部過期被剔除,以致大量剔除發(fā)生時,在未進行自我保護前促使了程序的崩潰。

eureka server/client流程

服務信息拉取

Eureka consumer服務信息的拉取分為全量式拉取和增量式拉取,eureka consumer啟動時進行全量拉取,運行過程中由定時任務進行增量式拉取,如果網(wǎng)絡出現(xiàn)異常,可能導致先拉取的數(shù)據(jù)被舊數(shù)據(jù)覆蓋(比如上一次拉取線程獲取結(jié)果較慢,數(shù)據(jù)已更新情況下使用返回結(jié)果再次更新,導致數(shù)據(jù)版本落后),產(chǎn)生臟數(shù)據(jù)。對此,eureka通過類型AtomicLong的fetchRegistryGeneration對數(shù)據(jù)版本進行跟蹤,版本不一致則表示此次拉取到的數(shù)據(jù)已過期。

fetchRegistryGeneration過程是在拉取數(shù)據(jù)之前,執(zhí)行fetchRegistryGeneration.get獲取當前版本號,獲取到數(shù)據(jù)之后,通過fetchRegistryGeneration.compareAndSet來判斷當前版本號是否已更新。
注意:如果增量式更新出現(xiàn)意外,會再次進行一次全量拉取更新。

Eureka server的伸縮容

Eureka Server是怎么知道有多少Peer的呢?Eureka Server在啟動后會調(diào)用EurekaClientConfig.getEurekaServerServiceUrls來獲取所有的Peer節(jié)點,并且會定期更新。定期更新頻率可以通過eureka.server.peerEurekaNodesUpdateIntervalMs配置。

這個方法的默認實現(xiàn)是從配置文件讀取,所以如果Eureka Server節(jié)點相對固定的話,可以通過在配置文件中配置來實現(xiàn)。如果希望能更靈活的控制Eureka Server節(jié)點,比如動態(tài)擴容/縮容,那么可以override getEurekaServerServiceUrls方法,提供自己的實現(xiàn),比如我們的項目中會通過數(shù)據(jù)庫讀取Eureka Server列表。

eureka server啟動時把自己當做是Service Consumer從其它Peer Eureka獲取所有服務的注冊信息。然后對每個服務信息,在自己這里執(zhí)行Register,isReplication=true,從而完成初始化。

Service Provider

Service Provider啟動時首先時注冊到Eureka Service上,這樣其他消費者才能進行服務調(diào)用,除了在啟動時之外,只要實例狀態(tài)信息有變化,也會注冊到Eureka Service。需要注意的是,需要確保配置eureka.client.registerWithEureka=true。register邏輯在方法AbstractJerseyEurekaHttpClient.register中,Service Provider會依次注冊到配置的Eureka Server Url上,如果注冊出現(xiàn)異常,則會繼續(xù)注冊其他的url。

Renew操作會在Service Provider端定期發(fā)起,用來通知Eureka Server自己還活著。 這里instance.leaseRenewalIntervalInSeconds屬性表示Renew頻率。默認是30秒,也就是每30秒會向Eureka Server發(fā)起Renew操作。這部分邏輯在HeartbeatThread類中。在Service Provider服務shutdown的時候,需要及時通知Eureka Server把自己剔除,從而避免客戶端調(diào)用已經(jīng)下線的服務,邏輯本身比較簡單,通過對方法標記@PreDestroy,從而在服務shutdown的時候會被觸發(fā)。

Service Consumer

Service Consumer這塊的實現(xiàn)相對就簡單一些,因為它只涉及到從Eureka Server獲取服務列表和更新服務列表。Service Consumer在啟動時會從Eureka Server獲取所有服務列表,并在本地緩存。需要注意的是,需要確保配置eureka.client.shouldFetchRegistry=true。由于在本地有一份Service Registries緩存,所以需要定期更新,定期更新頻率可以通過eureka.client.registryFetchIntervalSeconds配置。

小結(jié)

為什么要用eureka呢,因為分布式開發(fā)架構(gòu)中,任何單點的服務都不能保證不會中斷,因此需要服務發(fā)現(xiàn)機制,某個節(jié)點中斷后,服務消費者能及時感知到保證服務高可用。從eureka的設計與實現(xiàn)上來說還是容易理解的,SpringCloud將它集成在自己的子項目spring-cloud-netflix中,實現(xiàn)SpringCloud的服務發(fā)現(xiàn)功能。

注冊中心除了用eureka之外,還有zookeeper、consul、nacos等解決方案,他們實現(xiàn)原理不同,各自適用于不同的場景,可按需使用。

Eureka比ZooKeeper相比優(yōu)勢是什么

Zookeeper保證CP 當向注冊中心查詢服務列表時,我們可以容忍注冊中心返回的是幾分鐘以前的注冊信息,但不能接受服務直接down掉不可用。也就是說,服務注冊功能對可用性的要求要高于一致性。但是zk會出現(xiàn)這樣一種情況,當master節(jié)點因為網(wǎng)絡故障與其他節(jié)點失去聯(lián)系時,剩余節(jié)點會重新進行l(wèi)eader選舉。問題在于,選舉leader的時間太長,30 ~ 120s, 且選舉期間整個zk集群都是不可用的,這就導致在選舉期間注冊服務癱瘓。在云部署的環(huán)境下,因網(wǎng)絡問題使得zk集群失去master節(jié)點是較大概率會發(fā)生的事,雖然服務能夠最終恢復,但是漫長的選舉時間導致的注冊長期不可用是不能容忍的。

Eureka保證AP Eureka看明白了這一點,因此在設計時就優(yōu)先保證可用性。Eureka各個節(jié)點都是平等的,幾個節(jié)點掛掉不會影響正常節(jié)點的工作,剩余的節(jié)點依然可以提供注冊和查詢服務。而Eureka的客戶端在向某個Eureka注冊或時如果發(fā)現(xiàn)連接失敗,則會自動切換至其它節(jié)點,只要有一臺Eureka還在,就能保證注冊服務可用(保證可用性),只不過查到的信息可能不是最新的(不保證強一致性)。除此之外,Eureka還有一種自我保護機制,如果在15分鐘內(nèi)超過85%的節(jié)點都沒有正常的心跳,那么Eureka就認為客戶端與注冊中心出現(xiàn)了網(wǎng)絡故障。

eureka有哪些不足: eureka consumer本身有緩存,服務狀態(tài)更新滯后,最常見的狀況就是,服務下線了但是服務消費者還未及時感知,此時調(diào)用到已下線服務會導致請求失敗,只能依靠consumer端的容錯機制來保證。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容