假如说,20个服务实例,结果在1分钟之内,只有8个服务实例保持了心跳 ,这时eureka server是应该将剩余的12个没有心跳的服务实例都摘除吗?
eureka server这时会考虑是否自己网络故障了,而服务是没问题的。只不过eureka server自己的机器所在的网络故障了,导致那些服务的心跳发送不过来。就导致eureka server本地一直没有更新心跳。
这里eureka server会进入一个自我保护机制,停止摘除服务实例。
上文我们看到注册表registry的evict()方法,EvictionTask,定时调度的任务,60s来一次,会判断一下服务实例是否故障了,如果故障了,一直没有心跳,就会将服务实例给摘除。
但是在判断服务实例是否故障之前,evict()还会对当前服务器的状态进行判断
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
public boolean isLeaseExpirationEnabled() {
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
return true;
}
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
当isLeaseExpirationEnabled返回false的时候,eureka server启动自我保护机制,停止摘除任何服务实例。
那么isLeaseExpirationEnabled中是如何判断服务端出现问题,启动自我保护机制的呢?
自我保护机制完整流程:
(1)eureka server初始化的时候,就会初始化一个值numberOfRenewsPerMinThreshold(一分钟期望心跳次数阈值)
int registryCount = registry.syncUp();
在eureka server的启动类EurekaBootStrap中,首先通过syncUp()方法从相邻的eureka server中拉取注册表。如果发现eurekaClient中服务实例数量为0,则等待30秒后再查看一次(根据配置参数这样最多重复五次,这里并不会重复去拉取注册表,而只是等待,然后查看。拉取注册表的行为在其他流程中)。
如果发现拉取到的服务实例在本地还没注册,则在本地进行注册。
registry.openForTraffic(applicationInfoManager, registryCount);
这个方法就会根据当前服务实例的数量完成numberOfRenewsPerMinThreshold的初始化
this.expectedNumberOfRenewsPerMin = count * 2;
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
期望心跳次数 = 服务实例 * 2
一分钟期望心跳次数阈值 = 期望心跳次数 * serverConfig.getRenewalPercentThreshold()(默认为0.85)
(2)每分钟期望的心跳次数与服务实例是相关的,如果服务实例上线,则期望心跳次数会+2,如果服务实例下线,期望心跳次数会-2。理论上来说,当服务实例故障下线时,期望心跳次数也会 -2,但是源码中并没有这个体现,但是这里摘除服务实例时,并没有去改变期望心跳次数。
(3)启动类EurekaBootStrap中,有一行代码
serverContext.initialize();
在这里,会启动一个定时调度任务
private void scheduleRenewalThresholdUpdateTask() {
timer.schedule(new TimerTask() {
@Override
public void run() {
updateRenewalThreshold();
}
}, serverConfig.getRenewalThresholdUpdateIntervalMs(),
serverConfig.getRenewalThresholdUpdateIntervalMs());
}
延迟15分钟(默认)启动,然后每隔15分钟(默认)运行一次
private void updateRenewalThreshold() {
try {
Applications apps = eurekaClient.getApplications();
int count = 0;
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
if (this.isRegisterable(instance)) {
++count;
}
}
}
synchronized (lock) {
// Update threshold only if the threshold is greater than the
// current expected threshold of if the self preservation is disabled.
if ((count * 2) > (serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold)
|| (!this.isSelfPreservationModeEnabled())) {
this.expectedNumberOfRenewsPerMin = count * 2;
this.numberOfRenewsPerMinThreshold = (int) ((count * 2) * serverConfig.getRenewalPercentThreshold());
}
}
logger.info("Current renewal threshold is : {}", numberOfRenewsPerMinThreshold);
} catch (Throwable e) {
logger.error("Cannot update renewal threshold", e);
}
}
从(2)中我们可以看到如果服务实例故障被摘除,期望心跳次数的阈值并不会改变。如果不去定时的改变期望心跳次数的阈值,那么随着服务实例故障被摘除的数量变多,心跳次数就会低于期望心跳次数的阈值,导致自我保护机制触发,之后就不能摘除心跳超时的服务实例了。
当然更新期望心跳次数阈值也是有条件的,如果server当前处于自我保护状态下,就不能随意改变,因为这样会导致server退出自我保护机制。所以更新总值和临界值的前提是当前Server不处于自我保护状态,也就是上一分钟的续约总数的比例超过85%。
(4)实际心跳次数的计算
在renew方法中,每次有心跳过来,都会去更新MeasuredRate,将其内部的currentBucket使用CAS + 1。在MeasuredRate内部也有一个定时调度任务,每隔一分钟,就会将currentBucket的值存储到lastBucket中,并将currentBucket置0。
lastBucket就是用于判断的实际心跳次数。
(5)自我保护机制的触发
public boolean isLeaseExpirationEnabled() {
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
return true;
}
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
当代码中配置了启动自我保护机制,或者当前心跳次数小于每分钟期望心跳次数的阈值,则触发自我保护机制,eureka server认为自己网络故障,不再摘除任何服务实例。