springCloud 是基於 springBoot 實現的一套微服務工具, 它組合了很多其他中間件、工具得以實現, 閱讀 spring 源碼找到閱讀入口, 在Resource/MATE-INF
下會有 application.factories
文件, 這裏標記了自動配置類, 然後查看整體源碼目錄結構, 已確定適合自己的閱讀方式]
eurekaServer
server node節點同步
EurekaServerBootstrap
類, EurekaServer 節點的啓動初始化與結束銷燬功能在這個類中實現
protected void initEurekaServerContext() throws Exception {
// For backward compatibility
JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
XStream.PRIORITY_VERY_HIGH);
XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
XStream.PRIORITY_VERY_HIGH);
if (isAws(this.applicationInfoManager.getInfo())) {
this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig,
this.eurekaClientConfig, this.registry, this.applicationInfoManager);
this.awsBinder.start();
}
EurekaServerContextHolder.initialize(this.serverContext);
log.info("Initialized server context");
// Copy registry from neighboring eureka node
//同步其他EurekaServer 節點的註冊信息
int registryCount = this.registry.syncUp();
//開啓當前節點的註冊通信信道默認是 1,
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
}
初始化 EurekaServer 上下文環境, 兼容兩種數據流 mxl 和 json, 都是最高優先級, (共四種優先級 最高 10000; 一般 20; 低 -10; 最低 -20)
這裏會通信到其他 EurekaServer 同步各個節點的註冊信息
int registryCount = this.registry.syncUp();
this.registry
是InstanceRegistry
的實例, 繼承自Eureka
的PeerAwareInstanceRegistryImpl(對等感知實例註冊)
,InstanceRegistry 沒有重寫syncUp()
方法, 此方法在父類的實現爲
//同步其它 EurekaServer 節點的實例註冊信息
public int syncUp() {
int count = 0;
//循環控制拷貝失敗次數,Eureka 中給定默認次數爲 5 次, springCloud 設置eureka.server.registry-sync-retries屬性配置修改重試次數
for(int i = 0; i < this.serverConfig.getRegistrySyncRetries() && count == 0; ++i) {
if (i > 0) {
try {
//第一次拷貝數據失敗後,進行後續重複拷貝前要等待一段時間, Eureka中給定默認時間 30s,springCloud 設置eureka.server.registry-sync-retry-wait-ms屬性配置修改等待時間
Thread.sleep(this.serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException var10) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
//eurekaClient獲取所有應用遍歷每個 eurekaServer, 再遍歷 eurekaServer 中的註冊應用實例,註冊到當前 EurekaServer 中
Applications apps = this.eurekaClient.getApplications();
Iterator var4 = apps.getRegisteredApplications().iterator();
while(var4.hasNext()) {
Application app = (Application)var4.next();
Iterator var6 = app.getInstances().iterator();
while(var6.hasNext()) {
InstanceInfo instance = (InstanceInfo)var6.next();
try {
//判斷應用實例是否可註冊
if (this.isRegisterable(instance)) {
this.register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
++count;
}
} catch (Throwable var9) {
logger.error("During DS init copy", var9);
}
}
}
}
return count;
}
應用註冊節點拷貝過程
- 啓動當前 EurekaServer 初始化時調用 syncUp, 開始同步註冊節點
- 讀取設定的失敗重複次數,開始同步
- 使用 EurekaClient 獲取全部註冊中心節點, 再遍歷註冊中心中的註冊節點, 遍歷每個註冊節點實例是否允許註冊到當前註冊中心,若允許註冊進來, 增加註冊的記錄數
- 如果第一次同步後, 註冊記錄數爲0, 表示沒有將其它註冊中心數據同步到本節點, 重新嘗試同步
- 第一次同步失敗後的其餘嘗試同步均需要等待一定時間
- 直到有數據同步成功或全部嘗試次數用盡退出同步
注意:
-
這裏的 EurekaServer 註冊數據同步是通過
EurekaClient
實現的 -
#將當前節點註冊到 eurekaServer eureka.client.register-with-eureka=true #獲取註冊中心的註冊信息 eureka.client.fetch-registry=true
-
上面兩個配置屬性一定是 true, 否則不能同步, 它是通過先將自己(eurekaServer)作爲客戶端註冊到註冊中心中,然後拉取註冊中心的數據到本地, 再將本地 eurekaClient 中獲取的註冊節點同步到當前註冊中心
this.eurekaClient.getApplications(); 調用的是 AtomicReference::get(), 返回的是 EurekaClient 中定義的EurekaApplications
server Node 節點註冊
先說 eureka 的節點通信使用 http, 那麼對於 註冊中心就應該會提供相應的接口; InstanceRegistry
是 springCloud 實現的實例註冊類, 實現的功能是 實例註冊, 實例續租, 實例刪除 等功能, 對於真正的註冊功能應該在 netflix
的實現中, InstanceRegistry
繼承了PeerAwareInstanceRegistryImpl
類
一路向上找吧!!! 累!
在 eureka-core.jar 中找到了 resource
接口定義, 源碼如下:
@Path("/{version}/apps")
@Produces({"application/xml", "application/json"})
public class ApplicationsResource {
/**
* Gets information about a particular {@link com.netflix.discovery.shared.Application}.
*
* @param version
* the version of the request.
* @param appId
* the unique application identifier (which is the name) of the
* application.
* @return information about a particular application.
*/
@Path("{appId}")
public ApplicationResource getApplicationResource(
@PathParam("version") String version,
@PathParam("appId") String appId) {
CurrentRequestVersion.set(Version.toEnum(version));
return new ApplicationResource(appId, serverConfig, registry);
}
}
@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) {
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
}
public class InstanceRegistry extends PeerAwareInstanceRegistryImpl
implements ApplicationContextAware {
//註冊
@Override
public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
handleRegistration(info, leaseDuration, isReplication);
super.register(info, leaseDuration, isReplication);
}
//註冊
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
super.register(info, isReplication);
}
//處理應用註冊
private void handleRegistration(InstanceInfo info, int leaseDuration,
boolean isReplication) {
log("register " + info.getAppName() + ", vip " + info.getVIPAddress()
+ ", leaseDuration " + leaseDuration + ", isReplication "
+ isReplication);
publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration,
isReplication));
}
}
@Singleton
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
@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);
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
}
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
/**
* Registers a new instance with a given duration.
*
* @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean)
*/
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
// this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
// InstanceInfo instead of the server local copy.
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
} else {
// The lease does not exist and hence it is a new registration
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
// Since the client wants to register it, increase the number of clients sending renews
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
updateRenewsPerMinThreshold();
}
}
logger.debug("No previous lease information found; it is new registration");
}
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
}
// This is where the initial state transfer of overridden status happens
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
}
- 上面的接口路徑是
/{version}/apps/
post
請求 - 由
ApplicationsResource
轉到ApplicationResource
調用方法addInstance()
, 大部分都是校驗, 有用的就registry.register(info, "true".equals(isReplication));
這一句 registry
是注入的InstanceRegistry
InstanceRegistry::register()
調用super.register()
到PeerAwareInstanceRegistryImpl
PeerAwareInstanceRegistryImpl::register
最後調用抽象類AbstractInstanceRegistry::register
找到了方法調用鏈:
ApplicationResource::addInstance —> InstanceRegistry::register —> PeerAwareInstanceRegistryImpl::register —> AbstractInstanceRegistry::register
//todo 未完待續 讀整體整體註冊過程 從 InstanceRegistry 到 AbstractInstanceRegistry(重點)
服務註冊
/**
* Registers a new instance with a given duration.
*
* @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean)
*/
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
//註冊器獲取實例註冊信息
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
// this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
// InstanceInfo instead of the server local copy.
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
} else {
// The lease does not exist and hence it is a new registration
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
// Since the client wants to register it, increase the number of clients sending renews
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
updateRenewsPerMinThreshold();
}
}
logger.debug("No previous lease information found; it is new registration");
}
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
}
// This is where the initial state transfer of overridden status happens
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
註冊信息容器存儲結構
eureka 註冊器(AbstractInstanceRegistry)中 維護了一個ConcurrentHashMap 作爲註冊器容器registry來管理實例註冊信息, 上圖爲註冊容器內部結構:
1.以appName爲key, ConcurrentHashMap類型的容器爲值存儲相同應用不同實例集合
2.內部ConcurrentHashMap集合再以 instanceId(實例id) 爲key, 每個實例應用的註冊信息爲值存儲, 註冊信息包括 應用名, id, 端口, 地址, 註冊時間, 續約時常…等等,
json結構:
{
"appName1":{
"instanceId1":{
"port": 8080,
"host": 192.168.0.1
},
"instanceId2":{
}
},
"appName2":{
"instanceId3":{
},
"instanceId14":{
}
},
"appName3":{
"instanceId5":{
},
"instanceId6":{
}
}
}
服務下架
服務下架是由Eureka的PeerAwareInstanceRegistryImpl
註冊器管理的, 源碼調用入口如下:
EurekaServerBootStrap
protected void initEurekaServerContext() throws Exception {
...
//在啓動定時器前同步了個節點註冊數據
int registryCount = this.registry.syncUp();
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
...
}
PeerAwareInstanceRegistryImpl
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
...
super.postInit();
}
AbstractInstanceRegistry
protected void postInit() {
renewsLastMin.start();
if (evictionTaskRef.get() != null) {
evictionTaskRef.get().cancel();
}
//設置定時任務, 任務內會定時清理過期的註冊節點
evictionTaskRef.set(new EvictionTask());
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),//設置定時時間, springcloud配置默認是60s
serverConfig.getEvictionIntervalTimerInMs());
}
/**
* Evicts everything in the instance registry that has expired, if expiry is enabled.
*
* @see com.netflix.eureka.lease.LeaseManager#evict()
*/
@Override
public void evict() {
evict(0l);
}
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// We collect first all expired items, to evict them in random order. For large eviction sets,
// if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
// the impact should be evenly distributed across all applications.
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
if (leaseMap != null) {
for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
Lease<InstanceInfo> lease = leaseEntry.getValue();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
// triggering self-preservation. Without that we would wipe out full registry.
int registrySize = (int) getLocalRegistrySize();
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
int evictionLimit = registrySize - registrySizeThreshold;
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
// Pick a random item (Knuth shuffle algorithm)
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
internalCancel(appName, id, false);
}
}
}
AbstractInstanceRegistry.EvictionTask (內部類) 繼承TimerTask
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// 首先收集所有過期節點,以隨機順序刪除,
// 如果不這樣, 可能會將整個應用組直接刪除,
// 隨機處理, 可以將項目的影響均勻的分佈到各個模塊接待你.
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
if (leaseMap != null) {
for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
Lease<InstanceInfo> lease = leaseEntry.getValue();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
// triggering self-preservation. Without that we would wipe out full registry.
//爲了補償GC暫停或本地時間漂移,我們需要使用當前註冊表大小作爲,觸發自我保護。如果沒有這些,我們將抹去完整的註冊表。
int registrySize = (int) getLocalRegistrySize();
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
int evictionLimit = registrySize - registrySizeThreshold;
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
// Pick a random item (Knuth shuffle algorithm)
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
EXPIRED.increment();
logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
internalCancel(appName, id, false);
}
}
}
AbstractInstanceRegistry
/**
* {@link #cancel(String, String, boolean)} method is overridden by {@link PeerAwareInstanceRegistry}, so each
* cancel request is replicated to the peers. This is however not desired for expires which would be counted
* in the remote peers as valid cancellations, so self preservation mode would not kick-in.
*/
/**
* 對於取消請求需要同步到其他註冊中心節點, 但對於超時的取消, 不需要, 因爲對於超時, 其他節點做的處理與當前節點是一樣的
*
*/
protected boolean internalCancel(String appName, String id, boolean isReplication) {
try {
read.lock();
CANCEL.increment(isReplication);
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
if (gMap != null) {
leaseToCancel = gMap.remove(id);
}
synchronized (recentCanceledQueue) {
recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
}
InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
if (instanceStatus != null) {
logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
}
if (leaseToCancel == null) {
CANCEL_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
return false;
} else {
leaseToCancel.cancel();
InstanceInfo instanceInfo = leaseToCancel.getHolder();
String vip = null;
String svip = null;
if (instanceInfo != null) {
instanceInfo.setActionType(ActionType.DELETED);
recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
instanceInfo.setLastUpdatedTimestamp();
vip = instanceInfo.getVIPAddress();
svip = instanceInfo.getSecureVipAddress();
}
invalidateCache(appName, vip, svip);
logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
return true;
}
} finally {
read.unlock();
}
}
自我保護
eureka Server 在一定時間內沒有收到客戶端發來的心跳會認爲客戶端實現, 將註冊信息刪除, 但是在短時間內丟失大量的心跳, eureka Server 會觸發自我保護, 認爲實例都是健康的, 可能會由於網絡或其他原因問題沒有收到心跳, 所以就不會再刪除註冊的實例, 認爲所有實例都是健康的
Renews threshold 與 Renews (last min)
服務器端的續約閥值(Renews threshold)
this.expectedNumberOfRenewsPerMin = count * 2;
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
其中 , count 爲 服務器的數量 。 2 爲每 30 秒 1 個心跳 , 每分鐘 2 個心跳的固定頻率因。
歸納出的公式爲: 2M * renewalPercentThreshold , M 爲服務器的個數 , renewalPercentThreshold 默認爲 0.85(可以通過 eureka .server . renewal-percent-threshold
配置) , 計算結果只保留整數位 。
其實這就是個固定值 , 對於每個 Eureka Server 來說 , M 只能取 1 。 代碼達到的效果是:
1.expectedNumberOfRenewsPerMin 重置爲固定值 2;
2.numberOfRenewsPerMinThreshold 的值被設置爲 1;
客戶端的續約閥值(Renews threshold)
if (this.expectedNumberOfRenewsPerMin > 0) {
this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
}
注:上面貼出的 PeerAwareInstanceRegistryImpl 繼承自 AbstractInstanceRegistry 。
它們共享 expectedNumberOfRenewsPerMin (預期每分鐘續訂數)和 numberOfRenewsPerMinThreshold 屬性,
具體可自行翻閱源碼。
設有 N 個客戶端:服務器端先啓動 , expectedNumberOfRenewsPerMin 被重置爲固定值 2 。 接着客戶端依次啓動:
N = 1 –> (2 + 2) * renewalPercentThreshold
N = 2 –> (2 + 2 + 2) * renewalPercentThreshold
N = 3 –> (2 + 2 + 2 + 2) * renewalPercentThreshold
歸納出的公式爲: 2(N + 1) * renewalPercentThreshold , 計算結果只保留整數位 。
即 , 如果只有 1 個 Eureka Server 或者有多個 Eureka Server 但它們之間沒有相互註冊:
當 N = 0 時 , 只計算服務器端 。 Renews threshold = 1 。 由於沒有客戶端向服務器發送心跳 , Renews (last min) < Renews threshold , Eureka 自我保護模式被激活;
當 N ≠ 0 時 , 服務器端的計算結果被客戶端覆蓋 , 即只計算客戶端;
當 N = 2 時 , Renews threshold = 2(N + 1) * renewalPercentThreshold = 2 * 3 * 0.85 = 5 。 2 個客戶端以每 30 秒發送 1 個心跳 , 1 分鐘後總共向服務器發送 4 個心跳 , Renews (last min) < Renews threshold , Eureka 自我保護模式被激活;
所以如果 N < 3 , 在 1 分鐘後 , 服務器端收到的客戶端實例續約的總數總是小於期望的閥值 , 因此 Eureka 的自我保護模式自動被激活 。 首頁會出現警告信息:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
這種情況下 , 由於 Eureka Server 沒有對等的節點 , 同步不到服務註冊信息 , 默認需等待 5 分鐘(可以通過 eureka.server.wait-time-in-ms-when-sync-empty
配置) 。 即 5 分鐘之後你應該看到此警告信息 。
爲避免這種情況發生 , 你可以:
關閉自我保護模式( eureka.server.enable-self-preservation
設爲 false)
降低 renewalPercentThreshold 的比例( eureka.server.renewal-percent-threshold 設置爲 0.5 以下 , 比如 0.49)
部署多個 Eureka Server 並開啓其客戶端行爲( eureka.client.register-with-eureka 不要設爲 false , 默認爲 true)
如果是採取部署多個 Eureka Server 並開啓其客戶端行爲使其相互註冊 。 假設有 M 個 Eureka Server , 那麼 , 每個 Eureka Server 每分鐘可以額外收到 2 * (M – 1) 個心跳 。 例如:
當 M = 1 , N = 2 時 , Renews threshold = 2(N + 1) * renewalPercentThreshold = 2 * 3 * 0.85 = 5 , 2 個客戶端以每 30 秒發送 1 個心跳 , 1 分鐘後總共向服務器發送 4 個心跳 , Renews (last min) < Renews threshold ;
當 M = 2 , N = 2 時 , Renews threshold = 2(N + 1) * renewalPercentThreshold = 2 * 3 * 0.85 = 5 , 2 個客戶端以每 30 秒發送 1 個心跳 , 1 分鐘後總共向服務器發送 4 個心跳 , 另外還有 1 個 M 發來的 2 個心跳 , 總共是 6 個心跳 , Renews (last min) > Renews threshold ;
Eureka 的自我保護模式是有意義的 , 該模式被激活後 , 它不會從註冊列表中剔除因長時間沒收到心跳導致租期過期的服務 , 而是等待修復 , 直到心跳恢復正常之後 , 它自動退出自我保護模式 。 這種模式旨在避免因網絡分區故障導致服務不可用的問題 。 例如 , 兩個客戶端實例 C1 和 C2 的連通性是良好的 , 但是由於網絡故障 , C2 未能及時向 Eureka 發送心跳續約 , 這時候 Eureka 不能簡單的將 C2 從註冊表中剔除 。 因爲如果剔除了 , C1 就無法從 Eureka 服務器中獲取 C2 註冊的服務 , 但是這時候 C2 服務是可用的 。 所以 , Eureka 的自我保護模式最好還是開啓它 。