整體架構圖:
Eureka結構圖:
Eureka原理:
Eureka是Netflix開源的一款提供服務注冊和發(fā)現(xiàn)的產(chǎn)品牌芋,github地址為https://github.com/Netflix/eureka。注冊中心是分布式開發(fā)的核心組件之一离唐,而eureka是spring cloud推薦的注冊中心實現(xiàn)背伴,因此對于Java開發(fā)同學來說沸毁,還是有必要學習eureka的峰髓,特別是其架構及設計思想。
官方文檔定義是: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)中間層服務器的負載平衡和故障轉移搂誉,我們稱此服務為Eureka服務器徐紧。Eureka也有一個基于java的客戶端組件,Eureka客戶端炭懊,這使得與服務的交互更加容易并级,同時客戶端也有一個內置的負載平衡器,它執(zhí)行基本的循環(huán)負載均衡侮腹。
Eureka提供了完整的Service Registry和Service Discovery實現(xiàn)嘲碧,并且也經(jīng)受住了Netflix自己的生產(chǎn)環(huán)境考驗,相對使用起來會比較省心(同時Spring Cloud還有一套非常完善的開源代碼來整合Eureka父阻,所以使用起來非常方便)愈涩。
本文主要內容有:eureka基礎概念及架構、服務發(fā)現(xiàn)原理加矛、eureka server/client流程分析及優(yōu)缺點分析履婉,最后做個小結。由于本文側重于原理分析斟览,因此eureka(結合spring cloud)的使用就不再贅述了谐鼎,感興趣的小伙伴可以看下 程序猿DD 關于spring cloud的相關教程。
eureka基礎
eureka架構圖
- 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端通過appName
和instanceInfoId
來唯一區(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)一保存在內存的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的構造腕唧,二者的信息是基本一致的,因為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)是一致的喊废。結合服務發(fā)現(xiàn)的場景,實際上也并不需要節(jié)點間的狀態(tài)強一致栗弟。在一段時間內(比如30秒)污筷,節(jié)點A比節(jié)點B多一個服務實例或少一個服務實例,在業(yè)務上也是完全可以接受的(Service Consumer側一般也會實現(xiàn)錯誤重試和負載均衡機制)。所以按照CAP理論瓣蛀,Eureka的選擇就是放棄C陆蟆,選擇AP。
如果同步過程中惋增,出現(xiàn)了異常怎么辦呢叠殷,這時會根據(jù)異常信息做對應的處理,如果是讀取超時或者網(wǎng)絡連接異常诈皿,則稍后重試林束;如果其他異常則打印錯誤日志不再后續(xù)處理。
服務續(xù)約
Renew(服務續(xù)約)操作由Service Provider定期調用稽亏,類似于heartbeat壶冒。主要是用來告訴Eureka Server Service Provider還活著,避免服務被剔除掉截歉。renew接口實現(xiàn)方式和register基本一致:首先更新自身狀態(tài)胖腾,再同步到其它Peer,服務續(xù)約也就是把過期時間設置為當前時間加上duration的值瘪松。
注意:服務注冊如果InstanceInfo不存在則加入咸作,存在則更新;而服務預約只是進行更新凉逛,如果InstanceInfo不存在直接返回false性宏。
服務下線
Cancel(服務下線)一般在Service Provider shutdown的時候調用,用來把自身的服務從Eureka Server中刪除状飞,以防客戶端調用不存在的服務毫胜,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的可用性:比如自我保護時期不能進行服務剔除操作、過期操作是分批進行栗柒、服務剔除是隨機逐個剔除礁扮,剔除均勻分布在所有應用中,防止在同一時間內同一服務集群中的服務全部過期被剔除,以致大量剔除發(fā)生時太伊,在未進行自我保護前促使了程序的崩潰雇锡。
eureka server/client流程
服務信息拉取
Eureka consumer服務信息的拉取分為全量式拉取和增量式拉取,eureka consumer啟動時進行全量拉取僚焦,運行過程中由定時任務進行增量式拉取锰提,如果網(wǎng)絡出現(xiàn)異常,可能導致先拉取的數(shù)據(jù)被舊數(shù)據(jù)覆蓋(比如上一次拉取線程獲取結果較慢叠赐,數(shù)據(jù)已更新情況下使用返回結果再次更新欲账,導致數(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在啟動后會調用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上骗奖,這樣其他消費者才能進行服務調用,除了在啟動時之外,只要實例狀態(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把自己剔除州藕,從而避免客戶端調用已經(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
配置漫玄。
小結
為什么要用eureka呢,因為分布式開發(fā)架構中压彭,任何單點的服務都不能保證不會中斷睦优,因此需要服務發(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分鐘內超過85%的節(jié)點都沒有正常的心跳峡碉,那么Eureka就認為客戶端與注冊中心出現(xiàn)了網(wǎng)絡故障。
eureka有哪些不足: eureka consumer本身有緩存驮审,服務狀態(tài)更新滯后鲫寄,最常見的狀況就是吉执,服務下線了但是服務消費者還未及時感知,此時調用到已下線服務會導致請求失敗地来,只能依靠consumer端的容錯機制來保證鼠证。