摘要: 原創出處 http://www.iocoder.cn/Eureka/server-cluster/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 Eureka 1.8.X 版本
1. 概述
本文主要分享 Eureka-Server 集羣同步註冊信息。
Eureka-Server 集羣如下圖:
- Eureka-Server 集羣不區分主從節點或者 Primary & Secondary 節點,所有節點相同角色( 也就是沒有角色 ),完全對等。
- Eureka-Client 可以向任意 Eureka-Client 發起任意讀寫操作,Eureka-Server 將操作複製到另外的 Eureka-Server 以達到最終一致性。注意,Eureka-Server 是選擇了 AP 的組件。
Eureka-Server 可以使用直接配置所有節點的服務地址,或者基於 DNS 配置。推薦閱讀:《Spring Cloud構建微服務架構(六)高可用服務註冊中心》 。
本文主要類在 com.netflix.eureka.cluster
包下。
OK,讓我們開始愉快的遨遊在代碼的海洋。
推薦 Spring Cloud 書籍:
- 請支持正版。下載盜版,等於主動編寫低級 BUG 。
- 程序猿DD —— 《Spring Cloud微服務實戰》
- 周立 —— 《Spring Cloud與Docker微服務架構實戰》
- 兩書齊買,京東包郵。
ps :注意,本文提到的同步,準確來說是複製( Replication )。
2. 集羣節點初始化與更新
com.netflix.eureka.cluster.PeerEurekaNodes
,Eureka-Server 集羣節點集合 。構造方法如下 :
|
peerEurekaNodes
,peerEurekaNodeUrls
,taskExecutor
屬性,在構造方法中未設置和初始化,而是在PeerEurekaNodes#start()
方法,設置和初始化,下文我們會解析這個方法。- Eureka-Server 在初始化時,調用
EurekaBootStrap#getPeerEurekaNodes(...)
方法,創建 PeerEurekaNodes ,點擊 鏈接 查看該方法的實現。
2.1 集羣節點啓動
調用 PeerEurekaNodes#start()
方法,集羣節點啓動,主要完成兩個邏輯:
- 初始化集羣節點信息
- 初始化固定週期( 默認:10 分鐘,可配置 )更新集羣節點信息的任務
代碼如下:
|
- 第 15 行 && 第 21 行 :調用
#updatePeerEurekaNodes()
方法,更新集羣節點信息。
2.2 更新集羣節點信息
調用 #resolvePeerUrls()
方法,獲得 Eureka-Server 集羣服務地址數組,代碼如下:
|
- 第 2 至 5 行 :獲得 Eureka-Server 集羣服務地址數組。
EndpointUtils#getDiscoveryServiceUrls(...)
方法,邏輯與 《Eureka 源碼解析 —— EndPoint 與 解析器》「3.4 ConfigClusterResolver」 基本類似。EndpointUtils 正在逐步,猜測未來這裏會替換。 - 第 7 至 15 行 :移除自身節點,避免向自己同步。
調用 #updatePeerEurekaNodes()
方法,更新集羣節點信息,主要完成兩部分邏輯:
- 添加新增的集羣節點
- 關閉刪除的集羣節點
代碼如下:
|
- 第 7 至 9 行 :計算新增的集羣節點地址。
- 第 11 至 13 行 :計算刪除的集羣節點地址。
- 第 19 至 34 行 :關閉刪除的集羣節點。
第 36 至 43 行 :添加新增的集羣節點。調用
#createPeerEurekaNode(peerUrl)
方法,創建集羣節點,代碼如下:1: protected PeerEurekaNode createPeerEurekaNode(String peerEurekaNodeUrl) {2: HttpReplicationClient replicationClient = JerseyReplicationClient.createReplicationClient(serverConfig, serverCodecs, peerEurekaNodeUrl);3: String targetHost = hostFromUrl(peerEurekaNodeUrl);4: if (targetHost == null) {5: targetHost = "host";6: }7: return new PeerEurekaNode(registry, targetHost, peerEurekaNodeUrl, replicationClient, serverConfig);8: }- 第 2 行 :創建 Eureka-Server 集羣通信客戶端,在 《Eureka 源碼解析 —— 網絡通信》「4.2 JerseyReplicationClient」 有詳細解析。
- 第 7 行 :創建 PeerEurekaNode ,在 「2.3 PeerEurekaNode」 有詳細解析。
2.3 集羣節點
com.netflix.eureka.cluster.PeerEurekaNode
,單個集羣節點。
點擊 鏈接 查看構造方法
- 第 129 行 :創建 ReplicationTaskProcessor 。在 「4.1.2 同步操作任務處理器」 詳細解析
- 第 131 至 140 行 :創建批量任務分發器,在 《Eureka 源碼解析 —— 任務批處理》 有詳細解析。
- 第 142 至 151 行 :創建單任務分發器,用於 Eureka-Server 向亞馬遜 AWS 的 ASG (
Autoscaling Group
) 同步狀態。暫時跳過。
3. 獲取初始註冊信息
Eureka-Server 啓動時,調用 PeerAwareInstanceRegistryImpl#syncUp()
方法,從集羣的一個 Eureka-Server 節點獲取初始註冊信息,代碼如下:
|
- 第 7 至 15 行 :未獲取到註冊信息,
sleep
等待再次重試。 - 第 17 至 30 行 :獲取註冊信息,若獲取到,註冊到自身節點。
- 第 22 行 :判斷應用實例是否能夠註冊到自身節點。主要用於亞馬遜 AWS 環境下的判斷,若非部署在亞馬遜裏,都返回
true
。點擊 鏈接 查看實現。 - 第 23 行 :調用
#register()
方法,註冊應用實例到自身節點。在 《Eureka 源碼解析 —— 應用實例註冊發現(一)之註冊》 有詳細解析。
- 第 22 行 :判斷應用實例是否能夠註冊到自身節點。主要用於亞馬遜 AWS 環境下的判斷,若非部署在亞馬遜裏,都返回
若調用 #syncUp()
方法,未獲取到應用實例,則 Eureka-Server 會有一段時間( 默認:5 分鐘,可配 )不允許被 Eureka-Client 獲取註冊信息,避免影響 Eureka-Client 。
標記 Eureka-Server 啓動時,未獲取到應用實例,代碼如下:
// PeerAwareInstanceRegistryImpl.javaprivate boolean peerInstancesTransferEmptyOnStartup = true;public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {// ... 省略其他代碼if (count > 0) {this.peerInstancesTransferEmptyOnStartup = false;}// ... 省略其他代碼}
判斷 Eureka-Server 是否允許被 Eureka-Client 獲取註冊信息,代碼如下:
// PeerAwareInstanceRegistryImpl.javapublic boolean shouldAllowAccess(boolean remoteRegionRequired) {if (this.peerInstancesTransferEmptyOnStartup) {// 設置啓動時間this.startupTime = System.currentTimeMillis();if (!(System.currentTimeMillis() > this.startupTime + serverConfig.getWaitTimeInMsWhenSyncEmpty())) {return false;}}// ... 省略其他代碼return true;}
4. 同步註冊信息
Eureka-Server 集羣同步註冊信息如下圖:
- Eureka-Server 接收到 Eureka-Client 的 Register、Heartbeat、Cancel、StatusUpdate、DeleteStatusOverride 操作,固定間隔( 默認值 :500 毫秒,可配 )向 Eureka-Server 集羣內其他節點同步( 準實時,非實時 )。
4.1 同步操作類型
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.Action
,同步操作類型,代碼如下:
|
- Register ,在 《Eureka 源碼解析 —— 應用實例註冊發現(一)之註冊》 有詳細解析
- Heartbeat ,在 《Eureka 源碼解析 —— 應用實例註冊發現(二)之續租》 有詳細解析
- Cancel ,在 《Eureka 源碼解析 —— 應用實例註冊發現(三)之下線》 有詳細解析
- StatusUpdate ,在 《Eureka 源碼解析 —— 應用實例註冊發現(八)之覆蓋狀態》 有詳細解析
- DeleteStatusOverride ,在 《Eureka 源碼解析 —— 應用實例註冊發現(八)之覆蓋狀態》 有詳細解析
4.2 發起 Eureka-Server 同步操作
Eureka-Server 在完成 Eureka-Client 發起的上述操作在自身節點的執行後,向集羣內其他 Eureka-Server 發起同步操作。以 Register 操作舉例子,代碼如下:
|
- 最後一行,調用
#replicateToPeers(...)
方法,傳遞對應的同步操作類型,發起同步操作。
#replicateToPeers(...)
方法,代碼如下:
|
- 第 10 至 14 行 :Eureka-Server 在處理上述操作( Action ),無論來自 Eureka-Client 發起請求,還是 Eureka-Server 發起同步,調用的內部方法相同,通過
isReplication=true
參數,避免死循環同步。 - 第 16 至 22 行 :循環集羣內每個節點,調用
#replicateInstanceActionsToPeers(...)
方法,發起同步操作。
#replicateInstanceActionsToPeers(...)
方法,代碼如下:
|
- Cancel :調用
PeerEurekaNode#cancel(...)
方法,點擊 鏈接 查看實現。 - Heartbeat :調用
PeerEurekaNode#heartbeat(...)
方法,點擊 鏈接 查看實現。 - Register :調用
PeerEurekaNode#register(...)
方法,點擊 鏈接 查看實現。 - StatusUpdate :調用
PeerEurekaNode#statusUpdate(...)
方法,點擊 鏈接 查看實現。 - DeleteStatusOverride :調用
PeerEurekaNode#deleteStatusOverride(...)
方法,點擊 鏈接 查看實現。 上面的每個方法實現,我們都會看到類似這麼一段代碼 :
batchingDispatcher.process(taskId("${action}", appName, id), // idnew InstanceReplicationTask(targetHost, Action.Cancel, appName, id) {public EurekaHttpResponse<Void> execute() {return replicationClient.doString(...);}public void handleFailure(int statusCode, Object responseEntity) throws Throwable {// do Something...}}, // ReplicationTask 子類expiryTime)#task(...)
方法,生成同步操作任務編號。代碼如下:private static String taskId(String requestType, String appName, String id) {return requestType + '#' + appName + '/' + id;}- 相同應用實例的相同同步操作使用相同任務編號。在 《Eureka 源碼解析 —— 任務批處理》「2. 整體流程」 中,我們看到” 接收線程( Runner )合併任務,將相同任務編號的任務合併,只執行一次。 “,因此,相同應用實例的相同同步操作就能被合併,減少操作量。例如,Eureka-Server 同步某個應用實例的 Heartbeat 操作,接收同步的 Eureak-Server 掛了,一方面這個應用的這次操作會重試,另一方面,這個應用實例會發起新的 Heartbeat 操作,通過任務編號合併,接收同步的 Eureka-Server 恢復後,減少收到重複積壓的任務。
- InstanceReplicationTask ,同步操作任務,在 「4.1.1 同步操作任務」 詳細解析。
expiryTime
,任務過期時間。
4.1.1 同步操作任務
com.netflix.eureka.cluster.ReplicationTask
,同步任務抽象類- 點擊 鏈接 查看 ReplicationTask 代碼。
- 定義了
#getTaskName()
抽象方法。 - 定義了
#execute()
抽象方法,執行同步任務。 - 實現了
#handleSuccess()
方法,處理成功執行同步結果。 - 實現了
#handleFailure(...)
方法,處理失敗執行同步結果。
com.netflix.eureka.cluster.InstanceReplicationTask
,同步應用實例任務抽象類- 點擊 鏈接 查看 InstanceReplicationTask 代碼。
- 實現了父類
#getTaskName()
抽象方法。
com.netflix.eureka.cluster.AsgReplicationTask
,亞馬遜 AWS 使用,暫時跳過。
從上面 PeerEurekaNode#同步操作(...)
方法,全部實現了 InstanceReplicationTask 類的 #execute()
方法,部分重寫了 #handleFailure(...)
方法。
4.1.2 同步操作任務處理器
com.netflix.eureka.cluster.InstanceReplicationTask
,實現 TaskProcessor 接口,同步操作任務處理器。
- TaskProcessor ,在 《Eureka 源碼解析 —— 任務批處理》「10. 任務執行器【執行任務】」 有詳細解析。
- 點擊 鏈接 查看 InstanceReplicationTask 代碼。
ReplicationTaskProcessor#process(task)
,處理單任務,用於 Eureka-Server 向亞馬遜 AWS 的 ASG ( Autoscaling Group
) 同步狀態,暫時跳過,感興趣的同學可以點擊 鏈接 查看方法代碼。
ReplicationTaskProcessor#process(tasks)
,處理批量任務,用於 Eureka-Server 集羣註冊信息的同步操作任務,通過調用被同步的 Eureka-Server 的 peerreplication/batch/
接口,一次性將批量( 多個 )的同步操作任務發起請求,代碼如下:
|
- 第 4 行 :創建批量提交同步操作任務的請求對象( ReplicationList ) 。比較易懂,咱就不囉嗦貼代碼了。
- 第 7 行 :調用
JerseyReplicationClient#submitBatchUpdates(...)
方法,請求peerreplication/batch/
接口,一次性將批量( 多個 )的同步操作任務發起請求。JerseyReplicationClient#submitBatchUpdates(...)
方法,點擊 鏈接 查看方法。- ReplicationListResponse ,點擊 鏈接 查看類。
- ReplicationInstanceResponse ,點擊 鏈接 查看類。
- 第 9 至 31 行 :處理批量提交同步操作任務的響應,在 「4.4 處理 Eureka-Server 同步結果」 詳細解析。
4.3 接收 Eureka-Server 同步操作
com.netflix.eureka.resources.PeerReplicationResource
,同步操作任務 Resource ( Controller )。
peerreplication/batch/
接口,映射 PeerReplicationResource#batchReplication(...)
方法,代碼如下:
|
- 第 7 至 15 行 :逐個處理單個同步操作任務,並將處理結果( ReplicationInstanceResponse ) 添加到 ReplicationListResponse 。
- 第 23 至 50 行 :處理單個同步操作任務,返回處理結果( ReplicationInstanceResponse )。
- 第 24 至 25 行 :創建 ApplicationResource , InstanceResource 。我們看到,實際該方法是把單個同步操作任務提交到其他 Resource ( Controller ) 處理,Eureka-Server 收到 Eureka-Client 請求響應的 Resource ( Controller ) 是相同的邏輯。
- Register :點擊 鏈接 查看
#handleRegister(...)
方法。 - Heartbeat :點擊 鏈接 查看
#handleHeartbeat(...)
方法。 - Cancel :點擊 鏈接 查看
#handleCancel(...)
方法。 - StatusUpdate :點擊 鏈接 查看
#handleStatusUpdate(...)
方法。 - DeleteStatusOverride :點擊 鏈接 查看
#handleDeleteStatusOverride(...)
方法。
4.4 處理 Eureka-Server 同步結果
�� 想想就有小激動,終於寫到這裏了。
接 ReplicationTaskProcessor#process(tasks)
方法,處理批量提交同步操作任務的響應,代碼如下:
|
- 第 10 行 ,調用
#isSuccess(...)
方法,判斷請求是否成功,響應狀態碼是否在 [200, 300) 範圍內。 - 第 11 至 13 行 :狀態碼 503 ,目前 Eureka-Server 返回 503 的原因是被限流。在 《Eureka 源碼解析 —— 基於令牌桶算法的 RateLimiter》 詳細解析。該情況爲瞬時錯誤,會重試該同步操作任務,在 《Eureka 源碼解析 —— 任務批處理》「3. 任務處理器」 有詳細解析。
- 第 14 至 18 行 :非預期狀態碼,目前 Eureka-Server 在代碼上看下來,不會返回這樣的狀態碼。該情況爲永久錯誤,會重試該同步操作任務,在 《Eureka 源碼解析 —— 任務批處理》「3. 任務處理器」 有詳細解析。
- 第 20 行 :請求成功,調用
#handleBatchResponse(...)
方法,逐個處理每個 ReplicationTask 和 ReplicationInstanceResponse 。這裏有一點要注意下,請求成功指的是整個請求成功,實際每個 ReplicationInstanceResponse 可能返回的狀態碼不在 [200, 300) 範圍內。該方法下文詳細解析。 第 23 至 25 行 :請求發生網絡異常,例如網絡超時,打印網絡異常日誌。目前日誌的打印爲部分採樣,條件爲網絡發生異常每間隔 10 秒打印一條,避免網絡發生異常打印超級大量的日誌。該情況爲永久錯誤,會重試該同步操作任務,在 《Eureka 源碼解析 —— 任務批處理》「3. 任務處理器」 有詳細解析。
第 26 至 29 行 :非預期異常,目前 Eureka-Server 在代碼上看下來,不會拋出這樣的異常。該情況爲永久錯誤,會重試該同步操作任務,在 《Eureka 源碼解析 —— 任務批處理》「3. 任務處理器」 有詳細解析。
#handleBatchResponse(...)
方法,代碼如下:
|
ReplicationTask#handleSuccess()
方法,無任務同步操作任務重寫,是個空方法,代碼如下:// ReplicationTask.javapublic void handleSuccess() {}ReplicationTask#handleFailure()
方法,有兩個同步操作任務重寫:Cancel :當 Eureka-Server 不存在下線的應用實例時,返回 404 狀態碼,此時打印錯誤日誌,代碼如下:
// PeerEurekaNode#cancel(...)public void handleFailure(int statusCode, Object responseEntity) throws Throwable {super.handleFailure(statusCode, responseEntity);if (statusCode == 404) {logger.warn("{}: missing entry.", getTaskName());}}- x
Heartbeat :情況較爲複雜,我們換一行繼續說,避免排版有問題,影響閱讀。
噔噔噔恰,本文的重要頭戲來啦!Last But Very Importment !!!
Eureka-Server 是允許同一時刻允許在任意節點被 Eureka-Client 發起寫入相關的操作,網絡是不可靠的資源,Eureka-Client 可能向一個 Eureka-Server 註冊成功,但是網絡波動,導致 Eureka-Client 誤以爲失敗,此時恰好 Eureka-Client 變更了應用實例的狀態,重試向另一個 Eureka-Server 註冊,那麼兩個 Eureka-Server 對該應用實例的狀態產生衝突。
再例如…… 我們不要繼續舉例子,網絡波動真的很複雜。我們來看看 Eureka 是怎麼處理的。
應用實例( InstanceInfo ) 的 lastDirtyTimestamp
屬性,使用時間戳,表示應用實例的版本號,當請求方( 不僅僅是 Eureka-Client ,也可能是同步註冊操作的 Eureka-Server ) 向 Eureka-Server 發起註冊時,若 Eureka-Server 已存在擁有更大 lastDirtyTimestamp
該實例( 相同應用並且相同應用實例編號被認爲是相同實例 ),則請求方註冊的應用實例( InstanceInfo ) 無法覆蓋註冊此 Eureka-Server 的該實例( 見 AbstractInstanceRegistry#register(...)
方法 )。例如我們上面舉的例子,第一個 Eureka-Server 向 第二個 Eureka-Server 同步註冊應用實例時,不會註冊覆蓋,反倒是第二個 Eureka-Server 同步註冊應用到第一個 Eureka-Server ,註冊覆蓋成功,因爲 lastDirtyTimestamp
( 應用實例狀態變更時,可以設置 lastDirtyTimestamp
爲當前時間,見 ApplicationInfoManager#setInstanceStatus(status)
方法 )。
但是光靠註冊請求判斷 lastDirtyTimestamp
顯然是不夠的,因爲網絡異常情況下時,同步操作任務多次執行失敗到達過期時間後,此時在 Eureka-Server 集羣同步起到最終一致性最最最關鍵性出現了:Heartbeat 。因爲 Heartbeat 會週期性的執行,通過它一方面可以判斷 Eureka-Server 是否存在心跳對應的應用實例,另外一方面可以比較應用實例的 lastDirtyTimestamp
。當滿足下面任意條件,Eureka-Server 返回 404 狀態碼:
- 1)Eureka-Server 應用實例不存在,點擊 鏈接 查看觸發條件代碼位置。
- 2)Eureka-Server 應用實例狀態爲
UNKNOWN
,點擊 鏈接 查看觸發條件代碼位置。爲什麼會是UNKNOWN
,在 《Eureka 源碼解析 —— 應用實例註冊發現(八)之覆蓋狀態》「 4.3 續租場景」 有詳細解析。 - 3)請求的
lastDirtyTimestamp
更大,點擊 鏈接 查看觸發條件代碼位置。
請求方接收到 404 狀態碼返回後,認爲 Eureka-Server 應用實例實際是不存在的,重新發起應用實例的註冊。以本文的 Heartbeat 爲例子,代碼如下:
|
第 4 至 10 行 :接收到 404 狀態碼,調用
#register(...)
方法,向該被心跳同步操作失敗的 Eureka-Server 發起註冊本地的應用實例的請求。- 上述 3) ,會使用請求參數
overriddenStatus
存儲到 Eureka-Server 的應用實例覆蓋狀態集合(AbstractInstanceRegistry.overriddenInstanceStatusMap
),點擊 鏈接 查看觸發條件代碼位置。
- 上述 3) ,會使用請求參數
第 11 至 16 行 :恰好是 3) 反過來的情況,本地的應用實例的
lastDirtyTimestamp
小於 Eureka-Server 該應用實例的,此時 Eureka-Server 返回 409 狀態碼,點擊 鏈接 查看觸發條件代碼位置。調用#syncInstancesIfTimestampDiffers()
方法,覆蓋註冊本地應用實例,點擊 鏈接 查看方法。
OK,撒花!記住:Eureka 通過 Heartbeat 實現 Eureka-Server 集羣同步的最終一致性。