摘要: 原創出處 http://www.iocoder.cn/Eureka/instance-registry-register/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 Eureka 1.8.X 版本
- 1. 概述
- 2. Eureka-Client 發起註冊
- 3. Eureka-Server 接收註冊
- 藍框部分,爲本文重點。
- 非藍框部分,Eureka-Server 集羣間複製註冊的應用實例信息,不在本文內容範疇。
- 請支持正版。下載盜版,等於主動編寫低級 BUG 。
- 程序猿DD —— 《Spring Cloud微服務實戰》
- 周立 —— 《Spring Cloud與Docker微服務架構實戰》
- 兩書齊買,京東包郵。
- 配置
eureka.registration.enabled = true
,Eureka-Client 向 Eureka-Server 發起註冊應用實例的開關。 - InstanceInfo 在 Eureka-Client 和 Eureka-Server 數據不一致。
com.netflix.discovery.InstanceInfoReplicator
,應用實例信息複製器。調用
InstanceInfoReplicator#start(...)
方法,開啓應用實例信息複製器。實現代碼如下:// InstanceInfoReplicator.javaclass InstanceInfoReplicator implements Runnable {private static final Logger logger = LoggerFactory.getLogger(InstanceInfoReplicator.class);private final DiscoveryClient discoveryClient;/*** 應用實例信息*/private final InstanceInfo instanceInfo;/*** 定時執行頻率,單位:秒*/private final int replicationIntervalSeconds;/*** 定時執行器*/private final ScheduledExecutorService scheduler;/*** 定時執行任務的 Future*/private final AtomicReference<Future> scheduledPeriodicRef;/*** 是否開啓調度*/private final AtomicBoolean started;private final RateLimiter rateLimiter; // 限流相關,跳過private final int burstSize; // 限流相關,跳過private final int allowedRatePerMinute; // 限流相關,跳過InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {this.discoveryClient = discoveryClient;this.instanceInfo = instanceInfo;this.scheduler = Executors.newScheduledThreadPool(1,new ThreadFactoryBuilder().setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d").setDaemon(true).build());this.scheduledPeriodicRef = new AtomicReference<Future>();this.started = new AtomicBoolean(false);this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);this.replicationIntervalSeconds = replicationIntervalSeconds;this.burstSize = burstSize;this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", allowedRatePerMinute);}public void start(int initialDelayMs) {if (started.compareAndSet(false, true)) {// 設置 應用實例信息 數據不一致instanceInfo.setIsDirty(); // for initial register// 提交任務,並設置該任務的 FutureFuture next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);scheduledPeriodicRef.set(next);}}// ... 省略無關方法}// InstanceInfo.javaprivate volatile boolean isInstanceInfoDirty = false;private volatile Long lastDirtyTimestamp;public synchronized void setIsDirty() {isInstanceInfoDirty = true;lastDirtyTimestamp = System.currentTimeMillis();}- 執行
instanceInfo.setIsDirty()
代碼塊,因爲 InstanceInfo 剛被創建時,在 Eureka-Server 不存在,也會被註冊。 - 調用
ScheduledExecutorService#schedule(...)
方法,延遲initialDelayMs
毫秒執行一次任務。爲什麼此處設置scheduledPeriodicRef
?在InstanceInfoReplicator#onDemandUpdate()
方法會看到具體用途。
- 執行
定時檢查 InstanceInfo 的狀態(
status
) 屬性是否發生變化。若是,發起註冊。實現代碼如下:
// InstanceInfoReplicator.javapublic void run() {try {// 刷新 應用實例信息discoveryClient.refreshInstanceInfo();// 判斷 應用實例信息 是否數據不一致Long dirtyTimestamp = instanceInfo.isDirtyWithTime();if (dirtyTimestamp != null) {// 發起註冊discoveryClient.register();// 設置 應用實例信息 數據一致instanceInfo.unsetIsDirty(dirtyTimestamp);}} catch (Throwable t) {logger.warn("There was a problem with the instance info replicator", t);} finally {// 提交任務,並設置該任務的 FutureFuture next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);scheduledPeriodicRef.set(next);}}// InstanceInfo.javapublic synchronized long setIsDirtyWithTime() {setIsDirty();return lastDirtyTimestamp;}public synchronized void unsetIsDirty(long unsetDirtyTimestamp) {if (lastDirtyTimestamp <= unsetDirtyTimestamp) {isInstanceInfoDirty = false;} else {}}- 調用
DiscoveryClient#refreshInstanceInfo()
方法,刷新應用實例信息。此處可能導致應用實例信息數據不一致,在「2.2」刷新應用實例信息 詳細解析。 - 調用
DiscoveryClient#register()
方法,Eureka-Client 向 Eureka-Server 註冊應用實例。 - 調用
ScheduledExecutorService#schedule(...)
方法,再次延遲執行任務,並設置scheduledPeriodicRef
。通過這樣的方式,不斷循環定時執行任務。
- 調用
com.netflix.appinfo.ApplicationInfoManager.StatusChangeListener
內部類,監聽應用實例信息狀態的變更。調用
ApplicationInfoManager#registerStatusChangeListener(...)
方法,註冊應用實例狀態變更監聽器。實現代碼如下:public class ApplicationInfoManager {/*** 狀態變更監聽器*/protected final Map<String, StatusChangeListener> listeners;public void registerStatusChangeListener(StatusChangeListener listener) {listeners.put(listener.getId(), listener);}}業務裏,調用
ApplicationInfoManager#setInstanceStatus(...)
方法,設置應用實例信息的狀態,從而通知InstanceInfoReplicator#onDemandUpdate()
方法的調用。實現代碼如下:// ApplicationInfoManager.javapublic synchronized void setInstanceStatus(InstanceStatus status) {InstanceStatus next = instanceStatusMapper.map(status);if (next == null) {return;}InstanceStatus prev = instanceInfo.setStatus(next);if (prev != null) {for (StatusChangeListener listener : listeners.values()) {try {listener.notify(new StatusChangeEvent(prev, next));} catch (Exception e) {logger.warn("failed to notify listener: {}", listener.getId(), e);}}}}// InstanceInfo.javapublic synchronized InstanceStatus setStatus(InstanceStatus status) {if (this.status != status) {InstanceStatus prev = this.status;this.status = status;// 設置 應用實例信息 數據一致setIsDirty();return prev;}return null;}InstanceInfoReplicator#onDemandUpdate()
,實現代碼如下:// InstanceInfoReplicator.javapublic boolean onDemandUpdate() {if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) { // 限流相關,跳過scheduler.submit(new Runnable() {public void run() {logger.debug("Executing on-demand update of local InstanceInfo");// 取消任務Future latestPeriodic = scheduledPeriodicRef.get();if (latestPeriodic != null && !latestPeriodic.isDone()) {logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");latestPeriodic.cancel(false);}// 再次調用InstanceInfoReplicator.this.run();}});return true;} else {logger.warn("Ignoring onDemand update due to rate limiter");return false;}}- 調用
Future#cancel(false)
方法,取消定時任務,避免無用的註冊。 - 調用
InstanceInfoReplicator#run()
方法,發起註冊。
- 調用
調用
ApplicationInfoManager#refreshDataCenterInfoIfRequired()
方法,刷新數據中心相關信息,實現代碼如下:// ApplicationInfoManager.javapublic void refreshDataCenterInfoIfRequired() {// hostnameString existingAddress = instanceInfo.getHostName();String newAddress;if (config instanceof RefreshableInstanceConfig) {// Refresh data center info, and return up to date addressnewAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(true);} else {newAddress = config.getHostName(true);}// ipString newIp = config.getIpAddress();if (newAddress != null && !newAddress.equals(existingAddress)) {logger.warn("The address changed from : {} => {}", existingAddress, newAddress);// :( in the legacy code here the builder is acting as a mutator.// This is hard to fix as this same instanceInfo instance is referenced elsewhere.// We will most likely re-write the client at sometime so not fixing for now.InstanceInfo.Builder builder = new InstanceInfo.Builder(instanceInfo);builder.setHostName(newAddress) // hostname.setIPAddr(newIp) // ip.setDataCenterInfo(config.getDataCenterInfo()); // dataCenterInfoinstanceInfo.setIsDirty();}}public abstract class AbstractInstanceConfig implements EurekaInstanceConfig {private static final Pair<String, String> hostInfo = getHostInfo();public String getHostName(boolean refresh) {return hostInfo.second();}public String getIpAddress() {return hostInfo.first();}private static Pair<String, String> getHostInfo() {Pair<String, String> pair;try {InetAddress localHost = InetAddress.getLocalHost();pair = new Pair<String, String>(localHost.getHostAddress(), localHost.getHostName());} catch (UnknownHostException e) {logger.error("Cannot get host info", e);pair = new Pair<String, String>("", "");}return pair;}}- 關注應用實例信息的
hostName
、ipAddr
、dataCenterInfo
屬性的變化。 - 一般情況下,我們使用的是非 RefreshableInstanceConfig 實現的配置類( 一般是 MyDataCenterInstanceConfig ),因爲
AbstractInstanceConfig.hostInfo
是靜態屬性,即使本機修改了 IP 等信息,Eureka-Client 進程也不會感知到。TODO[0022]:看下springcloud 的實現
- 關注應用實例信息的
調用
ApplicationInfoManager#refreshLeaseInfoIfRequired()
方法,刷新租約相關信息,實現代碼如下:
public void refreshLeaseInfoIfRequired() {LeaseInfo leaseInfo = instanceInfo.getLeaseInfo();if (leaseInfo == null) {return;}int currentLeaseDuration = config.getLeaseExpirationDurationInSeconds();int currentLeaseRenewal = config.getLeaseRenewalIntervalInSeconds();if (leaseInfo.getDurationInSecs() != currentLeaseDuration // 租約過期時間 改變|| leaseInfo.getRenewalIntervalInSecs() != currentLeaseRenewal) { // 租約續約頻率 改變LeaseInfo newLeaseInfo = LeaseInfo.Builder.newBuilder().setRenewalIntervalInSecs(currentLeaseRenewal).setDurationInSecs(currentLeaseDuration).build();instanceInfo.setLeaseInfo(newLeaseInfo);instanceInfo.setIsDirty();}}- 關注應用實例信息的
renewalIntervalInSecs
、durationInSecs
屬性的變化。
- 關注應用實例信息的
調用
HealthCheckHandler#getStatus()
方法,健康檢查。這裏先暫時跳過,我們在TODO[0004]:健康檢查 詳細解析。
- 調用
AbstractJerseyEurekaHttpClient#register(...)
方法,POST
請求 Eureka-Server 的apps/${APP_NAME}
接口,參數爲 InstanceInfo ,實現註冊實例信息的註冊。 - 請求頭
isReplication
參數,和 Eureka-Server 集羣複製相關,暫時跳過。 調用
PeerAwareInstanceRegistryImpl#register(...)
方法,註冊應用實例信息。實現代碼如下:public void register(final InstanceInfo info, final boolean isReplication) {// 租約過期時間int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {leaseDuration = info.getLeaseInfo().getDurationInSecs();}// 註冊應用實例信息super.register(info, leaseDuration, isReplication);// Eureka-Server 複製replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);}- 調用父類
AbstractInstanceRegistry#register(...)
方法,註冊應用實例信息。
- 調用父類
holder
屬性,租約的持有者。在 Eureka-Server 裏,暫時只有 InstanceInfo 使用。registrationTimestamp
屬性,註冊( 創建 )租約時間戳。在構造方法裏可以看租約對象的創建時間戳即爲註冊租約時間戳。serviceUpTimestamp
屬性,開始服務時間戳。註冊應用實例信息會使用到它如下兩個方法,實現代碼如下:public void serviceUp() {if (serviceUpTimestamp == 0) { // 第一次有效serviceUpTimestamp = System.currentTimeMillis();}}public void setServiceUpTimestamp(long serviceUpTimestamp) {this.serviceUpTimestamp = serviceUpTimestamp;}lastUpdatedTimestamp
屬性,最後更新租約時間戳。每次續租時,更新該時間戳。註冊應用實例信息會使用到它如下方法,實現代碼如下:public void setLastUpdatedTimestamp() {this.lastUpdatedTimestamp = System.currentTimeMillis();}duration
屬性,租約持續時間,單位:毫秒。當租約過久未續租,即當前時間 -lastUpdatedTimestamp
>duration
時,租約過期。evictionTimestamp
屬性,租約過期時間戳。- 第 3 行 :添加到應用實例覆蓋狀態映射,在 《Eureka 源碼解析 —— Eureka-Server 集羣同步》 詳細解析。
- 第 6 至 7 行 :增加註冊次數到監控。配合 Netflix Servo 實現監控信息採集。
第 5 至 16 行 :獲得應用實例信息對應的租約。
registry
實現代碼如下:/*** 租約映射* key1 :應用名 {@link InstanceInfo#appName}* key2 :應用實例信息編號 {@link InstanceInfo#instanceId}* value :租約*/private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();第 17 至 30 行 :當租約已存在,判斷 Server 已存在的 InstanceInfo 的
lastDirtyTimestamp
是否大於( 不包括等於 ) Client 請求的 InstanceInfo ,若是,使用 Server 的 InstanceInfo 進行替代。- 第 31 至 44 行 :增加
numberOfRenewsPerMinThreshold
、expectedNumberOfRenewsPerMin
,自我保護機制相關,在 《Eureka 源碼解析 —— 應用實例註冊發現(四)之自我保護機制》 有詳細解析。 - 第 45 至 52 行 :創建租約,並添加到租約映射(
registry
)。 第 53 至 58 行 :添加到最近註冊的調試隊列(
recentRegisteredQueue
),用於 Eureka-Server 運維界面的顯示,無實際業務邏輯使用。實現代碼如下:/*** 最近註冊的調試隊列* key :添加時的時間戳* value :字符串 = 應用名(應用實例信息編號)*/private final CircularQueue<Pair<Long, String>> recentRegisteredQueue;/*** 循環隊列** @param <E> 泛型*/private class CircularQueue<E> extends ConcurrentLinkedQueue<E> {/*** 隊列大小*/private int size = 0;public CircularQueue(int size) {this.size = size;}public boolean add(E e) {this.makeSpaceIfNotAvailable();return super.add(e);}/*** 保證空間足夠** 當空間不夠時,移除首元素*/private void makeSpaceIfNotAvailable() {if (this.size() == size) {this.remove();}}public boolean offer(E e) {this.makeSpaceIfNotAvailable();return super.offer(e);}}第 59 至 68 行 :添加到應用實例覆蓋狀態映射,在 《Eureka 源碼解析 —— Eureka-Server 集羣同步》 詳細解析。
- 第 69 至 73 行 :設置應用實例的覆蓋狀態(
overridestatus
),避免註冊應用實例後,丟失覆蓋狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。 - 第 75 至 78 行 : 獲得應用實例最終狀態,並設置應用實例的狀態。在《應用實例註冊發現 (八)之覆蓋狀態》詳細解析。
- 第 80 至 84 行 :設置租約的開始服務的時間戳( 只有第一次有效 )。
第 85 至 88 行 :設置應用實例信息的操作類型爲添加,並添加到最近租約變更記錄隊列(
recentlyChangedQueue
)。recentlyChangedQueue
用於註冊信息的增量獲取,在《應用實例註冊發現 (七)之增量獲取》詳細解析。實現代碼如下:/*** 最近租約變更記錄隊列*/private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<RecentlyChangedItem>();第 89 至 90 行 :設置租約的最後更新時間戳。
- 第 91 至 92 行 :設置響應緩存( ResponseCache )過期,在《Eureka 源碼解析 —— 應用實例註冊發現 (六)之全量獲取》詳細解析。
- 第 96 至 97 行 :釋放鎖。
1. 概述
本文主要分享 Eureka-Client 向 Eureka-Server 註冊應用實例的過程。
FROM 《深度剖析服務發現組件Netflix Eureka》 二次編輯
推薦 Spring Cloud 書籍:
2. Eureka-Client 發起註冊
Eureka-Client 向 Eureka-Server 發起註冊應用實例需要符合如下條件:
每次 InstanceInfo 發生屬性變化時,標記 isInstanceInfoDirty
屬性爲 true
,表示 InstanceInfo 在 Eureka-Client 和 Eureka-Server 數據不一致,需要註冊。另外,InstanceInfo 剛被創建時,在 Eureka-Server 不存在,也會被註冊。
當符合條件時,InstanceInfo 不會立即向 Eureka-Server 註冊,而是後臺線程定時註冊。
當 InstanceInfo 的狀態( status
) 屬性發生變化時,並且配置 eureka.shouldOnDemandUpdateStatusChange = true
時,立即向 Eureka-Server 註冊。因爲狀態屬性非常重要,一般情況下建議開啓,當然默認情況也是開啓的。
Let’s Go。讓我們看看代碼的實現。
2.1 應用實例信息複製器
|
2.2 刷新應用實例信息
調用 DiscoveryClient#refreshInstanceInfo()
方法,刷新應用實例信息。此處可能導致應用實例信息數據不一致,實現代碼如下:
|
2.3 發起註冊應用實例
調用 DiscoveryClient#register()
方法,Eureka-Client 向 Eureka-Server 註冊應用實例,實現代碼如下:
|
3. Eureka-Server 接收註冊
3.1 接收註冊請求
com.netflix.eureka.resources.ApplicationResource
,處理單個應用的請求操作的 Resource ( Controller )。
註冊應用實例信息的請求,映射 ApplicationResource#addInstance()
方法,實現代碼如下:
|
3.2 Lease
在看具體的註冊應用實例信息的邏輯之前,我們先來看下 com.netflix.eureka.lease.Lease
,租約。實現代碼如下:
|
3.3 註冊應用實例信息
調用 AbstractInstanceRegistry#register(...)
方法,註冊應用實例信息,實現代碼如下:
|