Eureka重點原理解析
前言
帶著問題學習,事半功倍。本文將對如下幾個問題進行總結說明:
1、EurekaServer端服務註冊的流程和設計模式
2、Eureka服務續約的bug
3、EurekaClient的啟動流程
4、client啟動後是往一個server註冊還是多個server遍歷註冊
5、EurekaServer的三級快取
6、一個EurekaClient宕機後,其他EurekaClient最晚多長時間後才會不再往這個宕機的服務發起請求?
Eureka在Spring Cloud組件全家桶中,處於很核心的位置,從去年格林尼治版的更新說明中就能知道,更新日誌截圖如下,netflix的其他組件均進入維護狀態,不再添加新特性,但Eureka不包括在內。個人觀點,一方面Eureka的功能實現相對比較複雜,不好隨便改動,再就是位置關鍵,改動後影響範圍廣。
下面進入正文。註:Spring Cloud版本Hoxton SR1,eureka-core 1.9.13
正文
一、EurekaServer端服務註冊的流程和設計模式
服務端的入口類如下所示,不帶s的類中是對單個服務/實例的操作,帶s的是集合操作。服務註冊入口在ApplicationResource中。
服務註冊方法是ApplicationResource#addInstance,可以看到經過一些必要的判斷後調用了註冊方法,注意因為該請求是從客戶端發起的,isReplication為空,所以register方法的第二個參數是false。
1 registry.register(info, "true".equals(isReplication)); 2 return Response.status(204).build(); // 204 to be backwards compatible 3 }
追蹤進入PeerAwareInstanceRegistryImpl類的register方法,如下,該方法先調用了父類的註冊方法,然後調的往其他服務擴散註冊資訊的方法replicateToPeers。
1 @Override 2 public void register(final InstanceInfo info, final boolean isReplication) { 3 int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS; 4 if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) { 5 leaseDuration = info.getLeaseInfo().getDurationInSecs(); 6 } 7 super.register(info, leaseDuration, isReplication); 8 replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); 9 }
繼續跟進到父類AbstractInstanceRegistry,在父類的register方法中完成了對真實服務列表ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry的維護 (方法太長就不貼出來了)。
至此完成了服務註冊,共涉及到三個類:ApplicationResource、PeerAwareInstanceRegistryImpl、AbstractInstanceRegistry。前兩個類的register方法都是做了一些自己的事情外加調用父類的register,一個典型的責任鏈模式應用,一個類只負責自己的事情,然後調用上一層的方法,如果需加一個功能,只需要在對應位置加一層繼承關係即可,對原有功能無侵入。
二、Eureka服務續約的bug
打開Lease租債器類,看到renew方法和isExpired方法:
1 public void renew() { 2 lastUpdateTimestamp = System.currentTimeMillis() + duration; 3 4 }
1 public boolean isExpired(long additionalLeaseMs) { 2 return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs)); 3 }
續約方法每次調用都將最後修改時間變為當前時間+有效期(默認90s),而判斷是否失效的方法比較的是當前時間和最後修改時間+有效期,這就導致有效期加了兩次,即一個服務過了兩倍的有效期時間之後才會被服務端判定為到期。其實這個事情在isExpired方法的注釋中可以看到說明:
1 /** 2 * Checks if the lease of a given {@link com.netflix.appinfo.InstanceInfo} has expired or not. 3 * 4 * Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to +duration more than 5 * what it should be, the expiry will actually be 2 * duration. This is a minor bug and should only affect 6 * instances that ungracefully shutdown. Due to possible wide ranging impact to existing usage, this will 7 * not be fixed. 8 * 9 * @param additionalLeaseMs any additional lease time to add to the lease evaluation in ms. 10 */
三、EurekaClient的啟動流程
Eureka客戶端只需要引入依賴加上配置,便可以自動實現服務註冊。客戶端的功能主要包括三部分:啟動時的服務註冊、服務定時續約、服務列錶快取定時更新。
客戶端啟動的邏輯都在DiscoveryClient的構造方法中,com.netflix.discovery.DiscoveryClient#DiscoveryClient,方法程式碼太長,就不粘貼程式碼了,只描述下流程:
初始化兩個ThreadPoolExecutor:服務續約和更新快取;
從server拉取註冊資訊com.netflix.discovery.DiscoveryClient#fetchRegistry;
com.netflix.discovery.DiscoveryClient#register服務註冊;
啟動定時器。
四、client啟動後是往一個server註冊還是多個server遍歷註冊
客戶端在執行register方法註冊服務時,採用裝飾器模式對httpClient進行處理,其中有一個是RetryableEurekaHttpClient,在該類的execute方法中對客戶端配置文件中配置的serviceUrl進行了遍歷,如果第一個註冊請求處理成功了,則不再重試,否則遍歷serviceUrl重試。具體可見com.netflix.discovery.shared.transport.decorator.RetryableEurekaHttpClient#execute方法。
五、EurekaServer的三級快取
eureka的服務端為了提高服務列表維護和讀取的一致性與可用性,對服務列表的查看設置了三級快取,入口為com.netflix.eureka.resources.ApplicationsResource#getContainers。在該方法中調用了
ResponseCacheImpl#getGZIP方法獲取快取,如下:
1 public byte[] getGZIP(Key key) { 2 Value payload = getValue(key, shouldUseReadOnlyResponseCache); 3 if (payload == null) { 4 return null; 5 } 6 return payload.getGzipped(); 7 }
繼續跟進getValue方法:
1 Value getValue(final Key key, boolean useReadOnlyCache) { 2 Value payload = null; 3 try { 4 if (useReadOnlyCache) { 5 final Value currentPayload = readOnlyCacheMap.get(key); 6 if (currentPayload != null) { 7 payload = currentPayload; 8 } else { 9 payload = readWriteCacheMap.get(key); 10 readOnlyCacheMap.put(key, payload); 11 } 12 } else { 13 payload = readWriteCacheMap.get(key); 14 } 15 } catch (Throwable t) { 16 logger.error("Cannot get value for key : {}", key, t); 17 } 18 return payload; 19 }
可以看到,這裡有兩個map:readOnlyCacheMap(只讀快取)、readWriteCacheMap(讀寫快取),再加上AbstractInstanceRegistry#registry真實數據,總共三級map快取。
它們使用的規則如下:只讀快取每隔30s定時從讀寫快取中更新最新數據,讀寫快取與真實數據是同步的,它的存在是為了減少對真實數據的讀取。額外要注意,在eureka server中,讀取操作用的寫鎖,而註冊修改下線操作用的讀鎖。
通過三級快取,Eureka在並發吞吐量的基礎上做到了最大程度的數據一致性。這種設計思路值得學習。
六、一個EurekaClient宕機後,其他EurekaClient最晚多長時間後才會不再往這個宕機的服務發起請求?
先說下上面三級快取場景可能產生的延遲:如果在服務端的真實服務列表中,一個服務已經被剔除了,此時最多過多長時間其他客戶端才能得知到此消息?
客戶端每隔30s去服務端拉取一次快取 + 服務端只讀快取每30s同步一次讀寫快取的數據,即最長需要60s後客戶端才能得到最新的服務端列表數據。
再來看宕機的情況,即如果一個服務宕機,其他服務最多會經過多長時間才不會再往這個服務發送請求?
先看服務端,因為有上面第二項說的bug存在,默認服務端經過90s*2才會剔除該宕機服務,該剔除的定時器每60s執行一次,再加上上面說的客戶端更新快取的60s延遲,再加上ribbon的60s快取,所以總計是:
90*2 + 60 + 60 + 60 = 360s,即最長可能需要6分鐘。
小結
Eureka的定時器真TM多。。。