文章目录
前言
服务注册到
eurekaServer
上之后,会定时30s
发送心跳请求来进行续约,如果服务宕机了或者其他情况导致没有主动向EurekaServer
发送服务下线请求,那么EurekaServer
在后台会启动一个定时后,默认60s
扫描一次注册表,看看哪些服务实例规定时间没有发送续约请求,将这些故障实例从注册表剔除。
一、服务故障剔除源码解析
1.1、自动剔除定时任务启动
之前将
EurekaServer
启动的时候,在EurekaBootStrap
的initEurekaServerContext
方法中会调用registry
的openForTraffic
方法中,在openForTraffic
方法中会调用super.postInit()
;方法也就是AbstractInstanceRegistry#postInit
方法会启动定时任务
1.1.1、AbstractInstanceRegistry#postInit
这个方法中干了两件事:
- 调用renewsLastMin.start启动一个定时任务,用于统计每分钟的服务续约的renew统计
- 启动一个定时任务,用于做服务剔除的,默认60s执行一次
protected void postInit() {
renewsLastMin.start();
if (evictionTaskRef.get() != null) {
evictionTaskRef.get().cancel();
}
evictionTaskRef.set(new EvictionTask());
//60s执行一次
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
}
1.2、服务剔除流程
1.2.1、EvictionTask#run
这里会调用
evict
方法进行剔除操作
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);
}
}
1.2.2、EvictionTask#evict
- 这里首先会判断是否触发了自我保护机制,触发机制如下 比如说每一个新的实例注册到
Eureka Server
,Eureka
都会记录下来当前注册了多少个实例,再就是默认客户端每30s
进行服务续约一次,一分钟1
个实例就是2
次服务续约,它都会记录下来,按照这个规则,如果我有10
个实例注册到Eureka Server
上,那么按照正常流程下,每分钟就会有20
个服务续约请求,当然这是最好的情况下,不好的情况下服务续约请求可能达不到20
次,这个时候它就有一个最小阈值,也就是85%
,这里就是需要最少有15
次服务续约,当低于这个阈值的时候,就会自动开启自我保护机制,这次服务剔除就不会再进行。- 遍历注册表,判断续约有没有到期,如果到期则进行剔除,这里剔除会走我们上文说到的服务下线的逻辑,并且不进行节点间的同步,这个因为可能存在网络分区,可能实例只是和当前节点的心跳断了。
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
//自我保护机制,是否启用服务故障剔除
/**
* 自我保护机制触发流程, 比如说每一个新的实例注册到Eureka Server ,Eureka 都会记录下来当前注册了多少个实例,再就是默认客户端每30s进行服务续约一次,
* 一分钟1个实例就是2次服务续约,它都会记录下来,按照这个规则,如果我有10个实例注册到了Eureka Server 上,那么按照正常流程下,每分钟就会有20个服务续约请求,当然这是最好的情况下
* ,不好的情况下服务续约请求可能达不到20次,这个时候它就有一个最小阈值,也就是85% ,这里就是需要最少有15 次服务续约,当低于这个阈值的时候,就会自动开启自我保护机制,这次服务剔除就不会再进行
*/
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<>();
// 遍历注册表,遍历每一个实例信息,调用每个实例租约的lease.isExpired()方法判断有没有过期,additionalLeaseMs是上面计算的补偿时间
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()) {
//当前时间 距离服务上次续约时间超过 90+90 s,超过180s认为过期了。
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();
// 85%
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
// 计算剔除范围,就是不超过注册表所有实例的15%
int evictionLimit = registrySize - registrySizeThreshold;
// 剔除数量不超过注册表实例的15%
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);
}
}
}