摘要: 原創出處 http://www.iocoder.cn/Eureka/instance-registry-renew/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 Eureka 1.8.X 版本
- 1. 概述
- 2. Eureka-Client 發起續租
- 3. Eureka-Server 接收續租
- 藍框部分,爲本文重點。
- 非藍框部分,Eureka-Server 集羣間複製註冊的應用實例信息,不在本文內容範疇。
- 請支持正版。下載盜版,等於主動編寫低級 BUG 。
- 程序猿DD —— 《Spring Cloud微服務實戰》
- 周立 —— 《Spring Cloud與Docker微服務架構實戰》
scheduler
,定時任務服務,用於定時觸發心跳( 續租 )。細心如你,會發現任務提交的方式是ScheduledExecutorService#schedule(...)
方法,只延遲執行一次心跳,說好的固定頻率執行心跳呢!!!答案在 「2.3 TimedSupervisorTask」 揭曉。heartbeatExecutor
,心跳任務執行線程池。爲什麼有scheduler
的情況下,還有heartbeatExecutor
???答案也在 「2.3 TimedSupervisorTask」 揭曉。- HeartbeatThread,心跳線程,在「2.2 TimedSupervisorTask」 詳細解析。
調用
#renew
方法,執行續租邏輯。實現代碼如下:// DiscoveryClient.javaboolean renew() {EurekaHttpResponse<InstanceInfo> httpResponse;try {httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());if (httpResponse.getStatusCode() == 404) {REREGISTER_COUNTER.increment();logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());long timestamp = instanceInfo.setIsDirtyWithTime();// 發起註冊boolean success = register();if (success) {instanceInfo.unsetIsDirty(timestamp);}return success;}return httpResponse.getStatusCode() == 200;} catch (Throwable e) {logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);return false;}}調用
AbstractJerseyEurekaHttpClient#sendHeartBeat(...)
方法,發起續租請求,實現代碼如下:// AbstractJerseyEurekaHttpClient.javapublic EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {String urlPath = "apps/" + appName + '/' + id;ClientResponse response = null;try {WebResource webResource = jerseyClient.resource(serviceUrl).path(urlPath).queryParam("status", info.getStatus().toString()).queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());if (overriddenStatus != null) {webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());}Builder requestBuilder = webResource.getRequestBuilder();addExtraHeaders(requestBuilder);response = requestBuilder.put(ClientResponse.class);EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));if (response.hasEntity()) {eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));}return eurekaResponseBuilder.build();} finally {if (logger.isDebugEnabled()) {logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());}if (response != null) {response.close();}}}- PUT 請求 Eureka-Server 的
apps/${APP_NAME}/${INSTANCE_INFO_ID}
接口,參數爲status
、lastDirtyTimestamp
、overriddenstatus
,實現續租。
- PUT 請求 Eureka-Server 的
調用
AbstractJerseyEurekaHttpClient#register(...)
方法,當 Eureka-Server 不存在租約時,重新發起註冊,在《Eureka 源碼解析 —— 應用實例註冊發現 (一)之註冊》有詳細解析。
scheduler
,定時任務服務,用於定時【發起】子任務。executor
,執行子任務線程池,用於【提交】子任務執行。task
,子任務。timeoutMillis
,子任務執行超時時間,單位:毫秒。delay
,當前子任務執行頻率,單位:毫秒。值等於timeout
參數。maxDelay
,最大子任務執行頻率,單位:毫秒。值等於timeout * expBackOffBound
參數。scheduler
初始化延遲執行 TimedSupervisorTask 。- TimedSupervisorTask 執行時,提交
task
到executor
執行任務。- 當
task
執行正常,TimedSupervisorTask 再次提交自己到scheduler
延遲timeoutMillis
執行。 - 當
task
執行超時,重新計算延遲時間( 不允許超過maxDelay
),再次提交自己到scheduler
延遲執行。
- 當
- 第 5 至 6 行 :提交子任務
task
到執行子任務線程池executor
。 - 第 9 至 10 行 :等待子任務
task
執行完成或執行超時。 - 第 11 至 12 行 :子任務
task
執行完成,設置下一次執行延遲delay
。 - 第 19 至 22 行 :子任務
task
執行超時,重新計算下一次執行延遲delay
。計算公式爲Math.min(maxDelay, currentDelay * 2)
。如果多次超時,超時時間不斷乘以 2 ,不允許超過最大延遲時間(maxDelay
)。 - 第 41 至 44 行 :強制取消未完成的子任務。
- 第 46 至 49 行 :調度下一次 TimedSupervisorTask 。
第 8 至 9 行 :調用
PeerAwareInstanceRegistryImpl#renew(...)
方法,續租。實現代碼如下:// PeerAwareInstanceRegistryImpl.javapublic boolean renew(final String appName, final String id, final boolean isReplication) {if (super.renew(appName, id, isReplication)) { // 續租// Eureka-Server 複製replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);return true;}return false;}- 調用父類
AbstractInstanceRegistry#renew(...)
方法,註冊應用實例信息。
- 調用父類
第 11 至 16 行 :續租失敗,返回 404 響應。當 Eureka-Client 收到 404 響應後,會重新發起 InstanceInfo 的註冊。
第 18 至 30 行 :比較請求的
lastDirtyTimestamp
和 Server 的 InstanceInfo 的lastDirtyTimestamp
屬性差異,需要配置eureka.syncWhenTimestampDiffers = true
( 默認開啓 )。第 23 行 :調用
#validateDirtyTimestamp(...)
方法,比較lastDirtyTimestamp
的差異。實現代碼如下:// InstanceResource.java1: private Response validateDirtyTimestamp(Long lastDirtyTimestamp, boolean isReplication) {2: // 獲取 InstanceInfo3: InstanceInfo appInfo = registry.getInstanceByAppAndId(app.getName(), id, false);4: if (appInfo != null) {5: if ((lastDirtyTimestamp != null) && (!lastDirtyTimestamp.equals(appInfo.getLastDirtyTimestamp()))) {6: Object[] args = {id, appInfo.getLastDirtyTimestamp(), lastDirtyTimestamp, isReplication};7: // 請求 的 較大8: if (lastDirtyTimestamp > appInfo.getLastDirtyTimestamp()) {9: logger.debug("Time to sync, since the last dirty timestamp differs -"10: + " ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}", args);11: return Response.status(Status.NOT_FOUND).build();12: // Server 的 較大13: } else if (appInfo.getLastDirtyTimestamp() > lastDirtyTimestamp) {14: // In the case of replication, send the current instance info in the registry for the15: // replicating node to sync itself with this one.16: if (isReplication) {17: logger.debug(18: "Time to sync, since the last dirty timestamp differs -"19: + " ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}",20: args);21: return Response.status(Status.CONFLICT).entity(appInfo).build();22: } else {23: return Response.ok().build();24: }25: }26: }27:28: }29: return Response.ok().build();30: }- 第 7 至 11 行 :請求的
lastDirtyTimestamp
較大,意味着請求方( 可能是 Eureka-Client ,也可能是 Eureka-Server 集羣內的其他 Server )存在 InstanceInfo 和 Eureka-Server 的 InstanceInfo 的數據不一致,返回 404 響應。請求方收到 404 響應後重新發起註冊。 - 第 16 至 21 行 :《Eureka 源碼解析 —— Eureka-Server 集羣同步》 有詳細解析。
- 第 22 至 24 行 :Server 的
lastDirtyTimestamp
較大,並且請求方爲 Eureka-Client,續租成功,返回 200 成功響應。 - 第 29 行 :
lastDirtyTimestamp
一致,返回 200 成功響應。
- 第 24 至 30 行 :《Eureka 源碼解析 —— Eureka-Server 集羣同步》 有詳細解析。
- 第 7 至 11 行 :請求的
第 31 至 33 行 :續租成功,返回 200 成功響應。
- 第 2 至 3 行 :增加續租次數到監控。配合 Netflix Servo 實現監控信息採集。
- 第 4 至 9 行 :獲得租約( Lease )。
- 第 10 至 14 行 :租約不存在,返回續租失敗(
false
)。 - 第 19 至 21 行 :獲得應用實例的最終狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
- 第 22 至 27 行 :應用實例的最終狀態爲
UNKNOWN
,無法續約,返回false
。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。 - 第 28 至 37 行 :應用實例的狀態與最終狀態不相等,使用最終狀態覆蓋應用實例的狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
第 40 至 41 行 :新增續租每分鐘次數(
renewsLastMin
)。com.netflix.eureka.util.MeasuredRate
,速度測量類,實現代碼如下:// AbstractInstanceRegistry.java/*** 續租每分鐘次數*/private final MeasuredRate renewsLastMin;// MeasuredRate.javapublic class MeasuredRate {/*** 上一個間隔次數*/private final AtomicLong lastBucket = new AtomicLong(0);/*** 當前間隔次數*/private final AtomicLong currentBucket = new AtomicLong(0);/*** 間隔*/private final long sampleInterval;/*** 定時器*/private final Timer timer;private volatile boolean isActive;public MeasuredRate(long sampleInterval) {this.sampleInterval = sampleInterval;this.timer = new Timer("Eureka-MeasureRateTimer", true);this.isActive = false;}public synchronized void start() {if (!isActive) {timer.schedule(new TimerTask() {public void run() {try {// Zero out the current bucket.lastBucket.set(currentBucket.getAndSet(0));} catch (Throwable e) {logger.error("Cannot reset the Measured Rate", e);}}}, sampleInterval, sampleInterval);isActive = true;}}public synchronized void stop() {if (isActive) {timer.cancel();isActive = false;}}/*** Returns the count in the last sample interval.*/public long getCount() {return lastBucket.get();}/*** Increments the count in the current sample interval.*/public void increment() {currentBucket.incrementAndGet();}}timer
,定時器,負責每個sampleInterval
間隔重置當前次數(currentBucket
),並將原當前次數設置到上一個次數(lastBucket
)。#increment()
方法,返回當前次數(currentBucket
)。#getCount()
方法,返回上一個次數(lastBucket
)。renewsLastMin
有如下用途:- 配合 Netflix Servo 實現監控信息採集續租每分鐘次數。
- Eureka-Server 運維界面的顯示續租每分鐘次數。
- 自我保護機制,在 《Eureka 源碼解析 —— 應用實例註冊發現 (四)之自我保護機制》 詳細解析。
第 42 至 43 行 :調用
Lease#renew()
方法,設置租約最後更新時間( 續租 ),實現代碼如下:
public void renew() {lastUpdateTimestamp = System.currentTimeMillis() + duration;}- x
第 44 行 :返回續租成功(
true
)。
- 整個過程修改的租約的過期時間,即使併發請求,也不會對數據的一致性產生不一致的影響,因此像註冊操作一樣加鎖。
1. 概述
本文主要分享 Eureka-Client 向 Eureka-Server 續租應用實例的過程。
FROM 《深度剖析服務發現組件Netflix Eureka》 二次編輯
推薦 Spring Cloud 書籍:
2. Eureka-Client 發起續租
Eureka-Client 向 Eureka-Server 發起註冊應用實例成功後獲得租約 ( Lease )。
Eureka-Client 固定間隔向 Eureka-Server 發起續租( renew ),避免租約過期。
默認情況下,租約有效期爲 90 秒,續租頻率爲 30 秒。兩者比例爲 1 : 3 ,保證在網絡異常等情況下,有三次重試的機會。
2.1 初始化定時任務
Eureka-Client 在初始化過程中,創建心跳線程,固定間隔向 Eureka-Server 發起續租( renew )。實現代碼如下:
|
2.2 HeartbeatThread
com.netflix.discovery.DiscoveryClient.HeartbeatThread
,心跳線程,實現執行 Eureka-Client 向 Eureka-Server 發起續租( renew )請求。實現代碼如下:
|
2.3 TimedSupervisorTask
com.netflix.discovery.TimedSupervisorTask
,監管定時任務的任務。
A supervisor task that schedules subtasks while enforce a timeout.
創建 TimedSupervisorTask 代碼如下:
|
實現代碼如下:
|
3. Eureka-Server 接收續租
3.1 接收續租請求
com.netflix.eureka.resources.InstanceResource
,處理單個應用實例信息的請求操作的 Resource ( Controller )。
續租應用實例信息的請求,映射 InstanceResource#renewLease()
方法,實現代碼如下:
|
3.2 續租應用實例信息
調用 AbstractInstanceRegistry#renew(...)
方法,續租應用實例信息,實現代碼如下:
|