摘要: 原創出處 http://www.iocoder.cn/Eureka/instance-registry-register/ 「芋道原始碼」歡迎轉載,保留摘要,謝謝!
本文主要基於 Eureka 1.8.X 版本
???關注**微信公眾號:【芋道原始碼】**有福利:
- RocketMQ / MyCAT / Sharding-JDBC 所有原始碼分析文章列表
- RocketMQ / MyCAT / Sharding-JDBC 中文註釋原始碼 GitHub 地址
- 您對於原始碼的疑問每條留言都將得到認真回覆。甚至不知道如何讀原始碼也可以請教噢。
- 新的原始碼解析文章實時收到通知。每週更新一篇左右。
- 認真的原始碼交流微信群。
1. 概述
本文主要分享 Eureka-Client 向 Eureka-Server 註冊應用例項的過程。
FROM 《深度剖析服務發現元件Netflix Eureka》 二次編輯
- 藍框部分,為本文重點。
- 非藍框部分,Eureka-Server 叢集間複製註冊的應用例項資訊,不在本文內容範疇。
推薦 Spring Cloud 書籍:
- 請支援正版。下載盜版,等於主動編寫低階 BUG 。
- 程式猿DD —— 《Spring Cloud微服務實戰》
- 周立 —— 《Spring Cloud與Docker微服務架構實戰》
- 兩書齊買,京東包郵。
推薦 Spring Cloud 視訊:
2. Eureka-Client 發起註冊
Eureka-Client 向 Eureka-Server 發起註冊應用例項需要符合如下條件:
- 配置
eureka.registration.enabled = true
,Eureka-Client 向 Eureka-Server 發起註冊應用例項的開關。 - InstanceInfo 在 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 應用例項資訊複製器
// DiscoveryClient.java
public class DiscoveryClient implements EurekaClient {
/**
* 應用例項狀態變更監聽器
*/
private ApplicationInfoManager.StatusChangeListener statusChangeListener;
/**
* 應用例項資訊複製器
*/
private InstanceInfoReplicator instanceInfoReplicator;
private void initScheduledTasks() {
// ... 省略無關程式碼
if (clientConfig.shouldRegisterWithEureka()) {
// ... 省略無關程式碼
// 建立 應用例項資訊複製器
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
// 建立 應用例項狀態變更監聽器
statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
@Override
public String getId() {
return "statusChangeListener";
}
@Override
public void notify(StatusChangeEvent statusChangeEvent) {
if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
// log at warn level if DOWN was involved
logger.warn("Saw local status change event {}", statusChangeEvent);
} else {
logger.info("Saw local status change event {}", statusChangeEvent);
}
instanceInfoReplicator.onDemandUpdate();
}
};
// 註冊 應用例項狀態變更監聽器
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 開啟 應用例項資訊複製器
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
}
}
}
複製程式碼
-
com.netflix.discovery.InstanceInfoReplicator
,應用例項資訊複製器。-
呼叫
InstanceInfoReplicator#start(...)
方法,開啟應用例項資訊複製器。實現程式碼如下:// InstanceInfoReplicator.java class 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 // 提交任務,並設定該任務的 Future Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } } // ... 省略無關方法 } // InstanceInfo.java private 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.java @Override public 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 { // 提交任務,並設定該任務的 Future Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } } // InstanceInfo.java public 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.java public 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.java public synchronized InstanceStatus setStatus(InstanceStatus status) { if (this.status != status) { InstanceStatus prev = this.status; this.status = status; // 設定 應用例項資訊 資料一致 setIsDirty(); return prev; } return null; } 複製程式碼
-
InstanceInfoReplicator#onDemandUpdate()
,實現程式碼如下:// InstanceInfoReplicator.java public boolean onDemandUpdate() { if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) { // 限流相關,跳過 scheduler.submit(new Runnable() { @Override 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()
方法,發起註冊。
- 呼叫
-
2.2 重新整理應用例項資訊
呼叫 DiscoveryClient#refreshInstanceInfo()
方法,重新整理應用例項資訊。此處可能導致應用例項資訊資料不一致,實現程式碼如下:
void refreshInstanceInfo() {
// 重新整理 資料中心資訊
applicationInfoManager.refreshDataCenterInfoIfRequired();
// 重新整理 租約資訊
applicationInfoManager.refreshLeaseInfoIfRequired();
// 健康檢查
InstanceStatus status;
try {
status = getHealthCheckHandler().getStatus(instanceInfo.getStatus());
} catch (Exception e) {
logger.warn("Exception from healthcheckHandler.getStatus, setting status to DOWN", e);
status = InstanceStatus.DOWN;
}
if (null != status) {
applicationInfoManager.setInstanceStatus(status);
}
}
複製程式碼
-
呼叫
ApplicationInfoManager#refreshDataCenterInfoIfRequired()
方法,重新整理資料中心相關資訊,實現程式碼如下:// ApplicationInfoManager.java public void refreshDataCenterInfoIfRequired() { // hostname String existingAddress = instanceInfo.getHostName(); String newAddress; if (config instanceof RefreshableInstanceConfig) { // Refresh data center info, and return up to date address newAddress = ((RefreshableInstanceConfig) config).resolveDefaultAddress(true); } else { newAddress = config.getHostName(true); } // ip String 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()); // dataCenterInfo instanceInfo.setIsDirty(); } } public abstract class AbstractInstanceConfig implements EurekaInstanceConfig { private static final Pair<String, String> hostInfo = getHostInfo(); @Override public String getHostName(boolean refresh) { return hostInfo.second(); } @Override 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]:健康檢查 詳細解析。
2.3 發起註冊應用例項
呼叫 DiscoveryClient#register()
方法,Eureka-Client 向 Eureka-Server 註冊應用例項,實現程式碼如下:
// DiscoveryClient.java
boolean register() throws Throwable {
logger.info(PREFIX + appPathIdentifier + ": registering service...");
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}
// AbstractJerseyEurekaHttpClient.java
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
複製程式碼
- 呼叫
AbstractJerseyEurekaHttpClient#register(...)
方法,POST
請求 Eureka-Server 的apps/${APP_NAME}
介面,引數為 InstanceInfo ,實現註冊例項資訊的註冊。
3. Eureka-Server 接收註冊
3.1 接收註冊請求
com.netflix.eureka.resources.ApplicationResource
,處理單個應用的請求操作的 Resource ( Controller )。
註冊應用例項資訊的請求,對映 ApplicationResource#addInstance()
方法,實現程式碼如下:
@Produces({"application/xml", "application/json"})
public class ApplicationResource {
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
// 校驗引數是否合法
logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
// validate that the instanceinfo contains all the necessary required fields
if (isBlank(info.getId())) {
return Response.status(400).entity("Missing instanceId").build();
} else if (isBlank(info.getHostName())) {
return Response.status(400).entity("Missing hostname").build();
} else if (isBlank(info.getIPAddr())) {
return Response.status(400).entity("Missing ip address").build();
} else if (isBlank(info.getAppName())) {
return Response.status(400).entity("Missing appName").build();
} else if (!appName.equals(info.getAppName())) {
return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
} else if (info.getDataCenterInfo() == null) {
return Response.status(400).entity("Missing dataCenterInfo").build();
} else if (info.getDataCenterInfo().getName() == null) {
return Response.status(400).entity("Missing dataCenterInfo Name").build();
}
// AWS 相關,跳過
// handle cases where clients may be registering with bad DataCenterInfo with missing data
DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
if (dataCenterInfo instanceof UniqueIdentifier) {
String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
if (isBlank(dataCenterInfoId)) {
boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
if (experimental) {
String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
return Response.status(400).entity(entity).build();
} else if (dataCenterInfo instanceof AmazonInfo) {
AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
if (effectiveId == null) {
amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
}
} else {
logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
}
}
}
// 註冊應用例項資訊
registry.register(info, "true".equals(isReplication));
// 返回 204 成功
return Response.status(204).build(); // 204 to be backwards compatible
}
}
複製程式碼
-
請求頭
isReplication
引數,和 Eureka-Server 叢集複製相關,暫時跳過。 -
呼叫
PeerAwareInstanceRegistryImpl#register(...)
方法,註冊應用例項資訊。實現程式碼如下:@Override 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(...)
方法,註冊應用例項資訊。
- 呼叫父類
3.2 Lease
在看具體的註冊應用例項資訊的邏輯之前,我們先來看下 com.netflix.eureka.lease.Lease
,租約。實現程式碼如下:
public class Lease<T> {
/**
* 實體
*/
private T holder;
/**
* 註冊時間戳
*/
private long registrationTimestamp;
/**
* 開始服務時間戳
*/
private long serviceUpTimestamp;
/**
* 取消註冊時間戳
*/
private long evictionTimestamp;
/**
* 最後更新時間戳
*/
// Make it volatile so that the expiration task would see this quicker
private volatile long lastUpdateTimestamp;
/**
* 租約持續時長,單位:毫秒
*/
private long duration;
public Lease(T r, int durationInSecs) {
holder = r;
registrationTimestamp = System.currentTimeMillis();
lastUpdateTimestamp = registrationTimestamp;
duration = (durationInSecs * 1000);
}
}
複製程式碼
-
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.3 註冊應用例項資訊
呼叫 AbstractInstanceRegistry#register(...)
方法,註冊應用例項資訊,實現程式碼如下:
1: public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
2: try {
3: // 獲取讀鎖
4: read.lock();
5: Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
6: // 增加 註冊次數 到 監控
7: REGISTER.increment(isReplication);
8: // 獲得 應用例項資訊 對應的 租約
9: if (gMap == null) {
10: final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
11: gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap); // 新增 應用
12: if (gMap == null) { // 新增 應用 成功
13: gMap = gNewMap;
14: }
15: }
16: Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
17: // Retain the last dirty timestamp without overwriting it, if there is already a lease
18: if (existingLease != null && (existingLease.getHolder() != null)) { // 已存在時,使用資料不一致的時間大的應用註冊資訊為有效的
19: Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp(); // Server 註冊的 InstanceInfo
20: Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp(); // Client 請求的 InstanceInfo
21: logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
22:
23: // this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
24: // InstanceInfo instead of the server local copy.
25: if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
26: logger.warn("There is an existing lease and the existing lease`s dirty timestamp {} is greater" +
27: " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
28: logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
29: registrant = existingLease.getHolder();
30: }
31: } else {
32: // The lease does not exist and hence it is a new registration
33: // 【自我保護機制】增加 `numberOfRenewsPerMinThreshold` 、`expectedNumberOfRenewsPerMin`
34: synchronized (lock) {
35: if (this.expectedNumberOfRenewsPerMin > 0) {
36: // Since the client wants to cancel it, reduce the threshold
37: // (1
38: // for 30 seconds, 2 for a minute)
39: this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
40: this.numberOfRenewsPerMinThreshold =
41: (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
42: }
43: }
44: logger.debug("No previous lease information found; it is new registration");
45: }
46: // 建立 租約
47: Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
48: if (existingLease != null) { // 若租約已存在,設定 租約的開始服務的時間戳
49: lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
50: }
51: // 新增到 租約對映
52: gMap.put(registrant.getId(), lease);
53: // 新增到 最近註冊的除錯佇列
54: synchronized (recentRegisteredQueue) {
55: recentRegisteredQueue.add(new Pair<Long, String>(
56: System.currentTimeMillis(),
57: registrant.getAppName() + "(" + registrant.getId() + ")"));
58: }
59: // 新增到 應用例項覆蓋狀態對映(Eureka-Server 初始化使用)
60: // This is where the initial state transfer of overridden status happens
61: if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
62: logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
63: + "overrides", registrant.getOverriddenStatus(), registrant.getId());
64: if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
65: logger.info("Not found overridden id {} and hence adding it", registrant.getId());
66: overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
67: }
68: }
69: InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
70: if (overriddenStatusFromMap != null) {
71: logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
72: registrant.setOverriddenStatus(overriddenStatusFromMap);
73: }
74:
75: // 獲得應用例項最終狀態,並設定應用例項的狀態
76: // Set the status based on the overridden status rules
77: InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
78: registrant.setStatusWithoutDirty(overriddenInstanceStatus);
79:
80: // 設定 租約的開始服務的時間戳(只有第一次有效)
81: // If the lease is registered with UP status, set lease service up timestamp
82: if (InstanceStatus.UP.equals(registrant.getStatus())) {
83: lease.serviceUp();
84: }
85: // 設定 應用例項資訊的操作型別 為 新增
86: registrant.setActionType(ActionType.ADDED);
87: // 新增到 最近租約變更記錄佇列
88: recentlyChangedQueue.add(new RecentlyChangedItem(lease));
89: // 設定 租約的最後更新時間戳
90: registrant.setLastUpdatedTimestamp();
91: // 設定 響應快取 過期
92: invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
93: logger.info("Registered instance {}/{} with status {} (replication={})",
94: registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
95: } finally {
96: // 釋放鎖
97: read.unlock();
98: }
99: }
複製程式碼
-
第 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; } @Override 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 行 :釋放鎖。
666. 彩蛋
嘿嘿,蠻嗨的,比起前面幾篇寫配置相關的文章來說。
胖友,分享我的公眾號( 芋道原始碼 ) 給你的胖友可好?