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多。。。