原创不易,转载注明出处
系列文章目录
- 《SpringCloud Eureka Server源码解析(启动流程)》
- 《Eureka Server源码解析(服务注册流程)》
- 《Eureka Server源码解析(服务续约流程)》
- 《Eureka Server源码解析(服务主动下线流程)》
- 《深度解析Eureka的自我保护机制》
前言
本文主要是解析下Euraka Server 是怎样剔除那些“失联”服务实例的源码,一个服务注册到Eureka Server 上后,默认会30s 发送一次服务续约请求告诉Eureka Server 自己还活着,如果一个服务突然挂了,并没有主动向Eureka Server 发送服务下线请求,并没有从注册表删除该实例信息,其他服务拉取注册表的时候还能拉取到这个实例信息,并且使用的有问题,对于这种突然“失联”的服务实例,Eureka Server 是怎样做的呢? 服务故障剔除,Eureka Server 启动的时候,会启动一个定时任务,默认是每60s 扫描一次注册表,看看哪些服务实例长时间没有发送续约请求,就会将这些服务从注册表中剔除,默认是90s 没有续约的服务实例(但实际上90+90s ,在那段判断逻辑中,有段注释说该处是bug ,但是他并不改, 这里是我们使用Eureka 作为注册中心的时候需要注意的一个点)
1.源码解析
1.1 创建故障剔除任务
在Eureka Server 初始化启动完成后,会创建一个服务剔除定时任务,默认每个60s扫描一次注册表,我们来看看定时任务创建流程代码。
在EurekaBootStrap 的initEurekaServerContext 方法最后面有 registry.openForTraffic(applicationInfoManager, registryCount);
这行代码
它会改变Eureka Server 服务状态为UP,在它UP之后有一行,调用父类postInit方法。
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
super.postInit();
也就是注册表父类AbstractInstanceRegistry 的postInit方法
protected void postInit() {
renewsLastMin.start();
if (evictionTaskRef.get() != null) {
evictionTaskRef.get().cancel();
}
evictionTaskRef.set(new EvictionTask());
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
}
这个方法干了2件事情,1是启动了一个定时任务,用作每分钟的服务续约renew 统计,2是启动一个服务剔除定时任务,默认是60s执行一次。
public long getEvictionIntervalTimerInMs() {
return configInstance.getLongProperty(
namespace + "evictionIntervalTimerInMs", (60 * 1000)).get();
}
可以看到,如果你不指定的话,默认是60执行一次。
1.2 服务剔除流程
在1.1小节中,我们看了服务剔除定时任务的创建,执行的任务是EvictionTask ,接下来我们看下它是怎样服务故障剔除的。
private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);
@Override
public void run() {
try {
long compensationTimeMs = getCompensationTimeMs();
logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
evict(compensationTimeMs);
} catch (Throwable e) {
logger.error("Could not run the evict task", e);
}
}
getCompensationTimeMs 这个方法防止服务器时钟发生问题,做出的时间补偿,这里我们不需要过多的关注。
调用了evict 方法进行服务剔除,方法比较长,我们一部分一部分的看下
1.2.1 自我保护机制
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
是否启用服务故障剔除。
@Override
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 ,Eureka 都会记录下来当前注册了多少个实例,再就是默认客户端每30s进行服务续约一次,一分钟1个实例就是2次服务续约,它都会记录下来,按照这个规则,如果我有10个实例注册到了Eureka Server 上,那么按照正常流程下,每分钟就会有20个服务续约请求,当然这是最好的情况下,不好的情况下服务续约请求可能达不到20次,这个时候它就有一个最小阈值,也就是85% ,这里就是需要最少有15 次服务续约,当低于这个阈值的时候,就会自动开启自我保护机制,这次服务剔除就不会再进行。关于Eureka Server自我保护机制 详细计算规则请查看我的另一篇文章《深度解析Eureka的自我保护机制》
1.2.2 服务剔除
再接着往下
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);
}
}
}
}
可以看到这里直接遍历注册表,遍历每一个实例信息,调用每个实例租约的lease.isExpired(additionalLeaseMs) 方法判断有没有过期,其中additionalLeaseMs是上面计算的补偿时间。
规则就是:当前时间 大于 上次续约计算出来的时间 + duration(默认是90s)+ 补偿时间。
上次续约计算出来的时间 :
public void renew() {
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
说白了就是 当前时间 距离服务上次续约时间超过 90+90 s,超过180s认为过期了。
过期的实例信息会被塞到一个集合中。
// 注册表中所有实例信息
int registrySize = (int) getLocalRegistrySize();
// 85%
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
// 计算剔除范围, 就是不超过注册表所有实例的15%
int evictionLimit = registrySize - registrySizeThreshold;
// 剔除数量不超过注册表实例的15%
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
这段代码就是计算出来要剔除服务实例的数量,默认是不超过注册表所有实例数量的15% ,比如说我注册表中有100个实例,但是上面发现有20个实例过期了,但是根据这个规则,这次服务剔除,只能剔除15个。
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);
}
}
上面这段代码就是真正的服务剔除了,根据上面的规则,只能剔除15个服务实例,所以就遍历15次。服务剔除走的服务下线逻辑,并且不进行集群节点间的同步。
2.流程图
总结
Eureka Server 初始化完成后,会启动一个定时任务,定时扫描注册表中(默认是60s 扫描一次),专门清除那些长时间没有续约(发送心跳)的客户端实例,将实例信息从注册表中剔除(默认是2倍的duration ,一个duration默认是90s,180s没有发送心跳的服务实例会被从注册表中剔除,),但是需要注意的是,自我保护机制触发后不会剔除这些服务。