寫在前面
咱們知道Eureka分爲兩部分,Eureka Server和Eureka Client。Eureka Server充當註冊中心的角色,Eureka Client相對於Eureka Server來講是客戶端,須要將自身信息註冊到註冊中心。本文主要介紹的就是在Eureka Client註冊到Eureka Server時RetryableClientQuarantineRefreshPercentage
參數的使用技巧。java
Eureka Client註冊過程分析
Eureka Client註冊到Eureka Server時,首先遇到第一個問題就是Eureka Client端要知道Server的地址,這個參數對應的是eureka.client.service-url.defaultZone
舉個例子,在Eureka Client的properties文件中配置以下:ide
eureka.client.service-url.defaultZone= http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka,http://localhost:8764/eureka
如上所示,Eureka Client配置對應的Eureka Server地址分別是876一、876二、876三、8764。這裏存在兩個問題:源碼分析
- Eureka Client會將自身信息分別註冊到這四個地址嗎?
- Eureka Clinent註冊機制是怎樣的?
源碼面前一目瞭然,帶着這兩個問題咱們經過源碼來解答這兩個問題。Eureka Client在啓動的時候註冊源碼以下:
RetryableEurekaHttpClient中的execut方法post
@Override protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) { List<EurekaEndpoint> candidateHosts = null; int endpointIdx = 0; for (int retry = 0; retry < numberOfRetries; retry++) { EurekaHttpClient currentHttpClient = delegate.get(); EurekaEndpoint currentEndpoint = null; if (currentHttpClient == null) { if (candidateHosts == null) { candidateHosts = getHostCandidates(); if (candidateHosts.isEmpty()) { throw new TransportException("There is no known eureka server; cluster server list is empty"); } } if (endpointIdx >= candidateHosts.size()) { throw new TransportException("Cannot execute request on any known server"); } currentEndpoint = candidateHosts.get(endpointIdx++); currentHttpClient = clientFactory.newClient(currentEndpoint); } try { EurekaHttpResponse<R> response = requestExecutor.execute(currentHttpClient); if (serverStatusEvaluator.accept(response.getStatusCode(), requestExecutor.getRequestType())) { delegate.set(currentHttpClient); if (retry > 0) { logger.info("Request execution succeeded on retry #{}", retry); } return response; } logger.warn("Request execution failure with status code {}; retrying on another server if available", response.getStatusCode()); } catch (Exception e) { logger.warn("Request execution failed with message: {}", e.getMessage()); // just log message as the underlying client should log the stacktrace } // Connection error or 5xx from the server that must be retried on another server delegate.compareAndSet(currentHttpClient, null); if (currentEndpoint != null) { quarantineSet.add(currentEndpoint); } } throw new TransportException("Retry limit reached; giving up on completing the request"); }
按照個人理解,代碼精簡後內容以下:lua
int endpointIdx = 0; //用來保存全部Eureka Server信息(876一、876二、876三、8764) List<EurekaEndpoint> candidateHosts = null; //numberOfRetries的值代碼寫死默認爲3次 for (int retry = 0; retry < numberOfRetries; retry++) { /** *首次進入循環時,獲取全量的Eureka Server信息(876一、876二、876三、8764) */ if (candidateHosts == null) { candidateHosts = getHostCandidates(); } /** *經過endpointIdx自增,依次獲取Eureka Server信息,而後發送 *註冊的Post請求. */ currentEndpoint = candidateHosts.get(endpointIdx++); currentHttpClient = clientFactory.newClient(currentEndpoint); try { /** *發送註冊的Post請求動做,注意若是成功,則跳出循環,若是失敗則 *根據endpointIdx依次獲取下一個Eureka Server. */ response = requestExecutor.execute(currentHttpClient); return respones; } catch (Exception e) { //向註冊中心(Eureka Server)發起註冊的post出現異常時,打印日誌... } //若是這次註冊動做失敗,將當前的信息保存到quarantineSet中(一個Set集合) if (currentEndpoint != null) { quarantineSet.add(currentEndpoint); } } //若是都失敗,則以異常形式拋出... throw new TransportException("Retry limit reached; giving up on completing the request");
上面代碼中還有一個方法很重要就是List<EurekaEndpoint> candidateHosts = getHostCandidates();
接下來看下getHostCandidates()
方法源碼url
private List<EurekaEndpoint> getHostCandidates() { List<EurekaEndpoint> candidateHosts = clusterResolver.getClusterEndpoints(); quarantineSet.retainAll(candidateHosts); // If enough hosts are bad, we have no choice but start over again int threshold = (int) (candidateHosts.size() * transportConfig.getRetryableClientQuarantineRefreshPercentage()); if (quarantineSet.isEmpty()) { // no-op } else if (quarantineSet.size() >= threshold) { logger.debug("Clearing quarantined list of size {}", quarantineSet.size()); quarantineSet.clear(); } else { List<EurekaEndpoint> remainingHosts = new ArrayList<>(candidateHosts.size()); for (EurekaEndpoint endpoint : candidateHosts) { if (!quarantineSet.contains(endpoint)) { remainingHosts.add(endpoint); } } candidateHosts = remainingHosts; } return candidateHosts; }
按照個人理解,將代碼精簡下,只包括關鍵邏輯,內容以下:spa
private List<EurekaEndpoint> getHostCandidates() { /** * 獲取全部defaultZone配置的註冊中心信息(Eureka Server), * 在本文例子中表明4個(876一、876二、876三、8764)Eureka Server */ List candidateHosts = clusterResolver.getClusterEndpoints(); /** * quarantineSet這個Set集合中保存的是不可用的Eureka Server * 此處是拿不可用的Eureka Server與全量的Eureka Server取交集 */ quarantineSet.retainAll(candidateHosts); /** * 根據RetryableClientQuarantineRefreshPercentage參數計算閾值 * 該閾值後續會和quarantineSet中保存的不可用的Eureka Server個數 * 做比較,從而判斷是否返回全量的Eureka Server仍是過濾掉不可用的 * Eureka Server。 */ int threshold = (int) ( candidateHosts.size() * transportConfig.getRetryableClientQuarantineRefreshPercentage() ); if (quarantineSet.isEmpty()) { /** * 首次進入的時候,此時quarantineSet爲空,直接返回全量的 * Eureka Server列表 */ } else if (quarantineSet.size() >= threshold) { /** * 將不可用的Eureka Server與threshold值相比較,若是不可 * 用的Eureka Server個數大於閾值,則將以前保存的Eureka * Server內容直接清空,並返回全量的Eureka Server列表。 */ quarantineSet.clear(); } else { /** * 經過quarantineSet集合保存不可用的Eureka Server來過濾 * 全量的EurekaServer,從而獲取這次Eureka Client要註冊要 * 註冊的Eureka Server實例地址。 */ List<EurekaEndpoint> remainingHosts = new ArrayList<>(candidateHosts.size()); for (EurekaEndpoint endpoint : candidateHosts) { if (!quarantineSet.contains(endpoint)) { remainingHosts.add(endpoint); } } candidateHosts = remainingHosts; } return candidateHosts; }
經過源碼分析,咱們如今初步知道,當Eureka Client向Eureka Server發起註冊請求的時候(根據defaultZone尋找Eureka Server列表),若是有一次請求註冊成功,那麼後續就不會在向其餘Eureka Server發起註冊請求。以本文爲例,註冊中心有四個(876一、876二、876三、8764)。若是8761對應的Eureka Server服務的狀態是UP,那麼Eureka Client向該註冊中心註冊成功後,不會再向(876二、876三、8764)對應的Eureka Server發起註冊請求(對應程序是在for循環中直接return respones)。debug
說到這裏又引出來另一個問題,若是8761這個Eureka Server是down掉的呢?
根據源碼咱們可知Eureka Client首次會向8761這個Server發起註冊請求,若是該Server的狀態是down,那麼它會將該Server保存到quarantineSet這個Set集合中,而後再次訪問8762這個Eureka Server,若是8762這個Server的狀態依舊是down,它也會把這個Server保存到quarantineSet這個Set集合中,而後繼續訪問8763這個Server,若是8763這個Server的狀態依舊是down,此時除了會將其保存到quarantineSet這個Set集合中以外,還會跳出本次循環。從而結束這次註冊過程。日誌
道這裏有人要問接下來會不會向8764這個Server發起註冊,答案是否認的,由於循環的次數默認是3次。因此即便8764這個Server的狀態是UP,它也不會接收到來自Eureka Client發起的註冊信息。code
Eureka Client向Eureka Server發起註冊信息的過程除了在Eureka Client啓動的時候觸發,還有另一種方式,就是後臺定時任務。
假設咱們上面描述的場景是在Eureka Client啓動的時候,由於在啓動的時候註冊這個過程所有失敗了,當後臺定時任務執行時,還會進入該註冊流程。注意此時quarantineSet的值爲3(876一、876二、8763以前註冊失敗的Eureka Server)。
因此當程序再次進入getHostCandidates()
方法時,if (quarantineSet.isEmpty())
這個方法是不知足的,接下來會走else if (quarantineSet.size() >= threshold)
這個判斷,若是這個判斷成立,那麼會將quarantineSet集合清空,同時返回全量的Eureka Server列表,若是這個判斷不成立,會拿quarantineSet集合中保存的內容去過濾Eureka Server的全量列表。以本文爲例:
quarantineSet
中保存的是(876一、876二、8763)三個Eureka Server- Eureka Server全量列表的內容是(876一、876二、876三、8764)四個Eureka Server過濾後返回的結果爲8764這個Eureka Server。
在本文的例子中876一、876二、8763這三個Eureka Server的狀態是down而8764這個Eureka Server的狀態是UP,咱們實際上是想走到最後的else分支,從而完成過濾操做,並最終獲得8764這個Server,遺憾的是它並不會走到這個分支,而是被上面的else if (quarantineSet.size() >= threshold)
這個分支所攔截,返回的依舊是全量的Eureka Server列表。這樣形成的後果就是Eureka Client依舊會依次向(876一、876二、8763)這三個down的Eureka Server發起註冊請求。
那麼問題的關鍵在哪裏呢?問題的關鍵就是threshold這個值的由來,由於此時quarantineSet.size()的值爲3,而3這個值大於threshold,從而致使,會將quarantineSet集合清空,返回全量的Server列表。
咱們知道threshold這個值是根據全量的Eureka Server列表乘以一個可配置的參數計算出來的,在本文的例子當中,個人properties文件中除了defaultZone以外並無配置這個參數,那麼也就是說這個參數是有默認值的,經過源碼咱們瞭解到,這個默認值是0.66。具體源碼以下:
final class PropertyBasedTransportConfigConstants { /** *省略部分源碼 */ static class Values { static final int SESSION_RECONNECT_INTERVAL = 20*60; //默認值爲0.66 static final double QUARANTINE_REFRESH_PERCENTAGE = 0.66; static final int DATA_STALENESS_TRHESHOLD = 5*60; static final int ASYNC_RESOLVER_REFRESH_INTERVAL = 5*60*1000; static final int ASYNC_RESOLVER_WARMUP_TIMEOUT = 5000; static final int ASYNC_EXECUTOR_THREADPOOL_SIZE = 5; } }
/** *@return the percentage of the full endpoints set above which the *quarantine set is cleared in the range [0, 1.0] */ double getRetryableClientQuarantineRefreshPercentage();
看到這裏就不難理解了,由於這個值是0.66而此時全量的Eureka Server值爲4。計算以後的值爲2,而因爲註冊的for循環爲3次,因此當第二次發起註冊流程的時候quarantineSet的值始終大於threshold。這樣就會致使一個問題,就是若是876一、876二、8763一直是down即便8764一直是好的,那麼Eureka Client也不會註冊成功。並且這個參數值的區間爲0到1.
既然經過源碼分析咱們找到了問題根源,其實對應的咱們也找到了解決這個問題的辦法,就是對應把這個參數值調大些。
這個值在properties中對應的寫法以下:
eureka.client.transport.retryableClientQuarantineRefreshPercentage = xxx
接下來咱們修改下properties文件,修改後的內容以下:
eureka.client.service-url.defaultZone= http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka,http://localhost:8764/eureka eureka.client.transport.retryableClientQuarantineRefreshPercentage=1
eureka.client.service-url.defaultZone= http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka,http://localhost:8764/eureka eureka.client.transport.retryableClientQuarantineRefreshPercentage=1
接下來按照這個配置再次回顧下上面的流程:
- Eureka Client啓動時進行註冊(876一、876二、8763的狀態是down),因此此時quarantineSet的值爲3.
- 接下來在定時任務中又觸發註冊事件,此時由於參數的值從0.66調整爲1。因此計算出的threshold的值爲4。而此時quarantineSet的值爲3。因此不會進入到
else if (quarantineSet.size() >= threshold)
分支,而是會進入最後的esle分支。 - 在else分支中會完成過濾功能,最終返回的list中的結果只有一個就是8764這個Eureka Server。
- Eureka Client向8764這個Eureka Server發起註冊請求,獲得成功相應,並返回。
遺留問題
說道這裏咱們感受好像是解決了這個問題,那麼問一個問題,這個參數值能夠設置的無限大嗎?
好比我將這個參數值設置爲10,雖然javaDoc中說明這個參數值的範圍在0-1之間,可是並無說明若是將這個參數調整大於1會出現什麼狀況。接下來按照上面的流程咱們分析下:
以前咱們分析的流程中的前提是876一、876二、8763這三臺Server的狀態是down而8764這個server的狀態是up,如今咱們修改下這個前提。
假設一開始876一、876二、876三、8764這四臺Eureka Server的狀態都是down。
Eureka Client啓動時進行註冊(876一、876二、8763的狀態是down),因此此時quarantineSet的值爲3.
- 接下來在定時任務中又觸發註冊事件,此時由於參數的值從0.66調整爲10。因此計算出的threshold的值爲40。而此時quarantineSet的值爲3。因此不會進入到
else if (quarantineSet.size() >= threshold)
分支,而是會進入最後的esle分支。 - 在else分支中會完成過濾功能,最終返回的list中的結果只有一個就是8764這個Eureka Server。
- Eureka Client向8764這個Eureka Server發起註冊請求,由於此時8764的狀態也是down致使註冊失敗,此時quarantineSet中的內容是(876一、876二、876三、8764)
- 當定時任務再次觸發時
if (quarantineSet.isEmpty())
這個分支不會進入,由於此時quarantineSet的值爲4else if (quarantineSet.size() >= threshold)
這分支也不會進入由於threshold的值爲40 - 最終會進入else分支,這個分支本來的含義是想經過quarantineSet來充當過濾器,從全量的Eureka Server中過濾掉以前狀態爲down的Eureka Server,可是因爲quarantineSet的值如今已是全量,致使過濾後的結果返回的是一個空的list。即便此時Eureka Server列表(876一、876二、876三、8764)任何一個Server的狀態變爲UP,該Eureka Client也不可能完成註冊事件。