Eureka自我保护
默认情况下,当EurekaServer在一定时间内(默认90秒)没有接收到某个客户端实例的心跳,EurekaServer将会注销该实例。
但是当网络分区故障发生时,客户端与EurekaServer之间无法正常通信,此时不应该注销客户端。
Eureka通过“自我保护机制”来解决这个问题:当EurekaServer短时间内丢失过多客户端时,这个节点就会进入自我保护模式。
在自我保护模式下,EurekaServer不会剔除任何客户端。当网络故障恢复后,该节点会自动退出自我保护模式
自我保护机制的实现是基于维护服务注册表的类AbstractInstanceRegistry
中的2个变量来维护的
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
// 每分钟最小续约阈值
protected volatile int numberOfRenewsPerMinThreshold;
// 预期中发送续约消息的客户端数量
protected volatile int expectedNumberOfClientsSendingRenews;
}
Eureka Server启动
Eureka Server启动的源码可以去看这篇文章:Spring Cloud 三:Eureka Server源码详细分析
public class EurekaServerBootstrap {
// 省略部分代码
protected void initEurekaServerContext() throws Exception {
// 集群同步
int registryCount = this.registry.syncUp();
// 自我保护初始化
// 服务定时自动剔除。
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
}
// 省略部分代码
}
在服务端启动、从其他集群同步完信息后执行了一个方法:openForTraffic
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// 初始化期望client发送过来的服务数量,即上面获取到的服务数量
this.expectedNumberOfClientsSendingRenews = count;
//计算自我保护的统计参数
updateRenewsPerMinThreshold();
this.startupTime = System.currentTimeMillis();
// 如果count=0,没有拉取到注册表信息,将此值设为true,表示其他peer来取空的实例信息,意味着,将不允许client从此server获取注册表信息。如果count>0,将此值设置为false,允许client来获取注册表。
if (count > 0) {
this.peerInstancesTransferEmptyOnStartup = false;
}
// 服务置为上线
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
// 开启剔除的定时任务
super.postInit();
}
预期中发送续约消息的客户端数量(expectedNumberOfClientsSendingRenews),等于syncUp()集群同步获取到的服务个数。用于下面updateRenewsPerMinThreshold()方法来计算每分钟最小续约阈值:
//每分钟最小续约阈值
//包含所有客户端,不分服务提供方和消费方
protected volatile int numberOfRenewsPerMinThreshold;
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
翻译如下:
每分钟最小续约阈值 = 预期中发送续约消息的客户端数量 * (60s / 每分钟客户端预期续约间隔) * 续约百分比阈值
- 每分钟最小续约阈值: Eureka Server 每分钟至少要收到这个数量的续约消息(心跳)。
- 预期中发送续约消息的客户端数量 :注册到 Eureka Server 的客户端的数量。
- 60s / 每分钟客户端预期续约间隔:一分钟内,一个客户端的续约次数。每分钟客户端预期续约间隔,默认是 30s。所以一分钟内,一个客户端的续约次数默认是2次。
- 续约百分比阈值:默认 85%。
自我保护定时任务
DefaultEurekaServerContext
类中有一个initialize
方法,这个方法在执行过程中会启动一个定时任务
public class DefaultEurekaServerContext implements EurekaServerContext {
@PostConstruct
@Override
public void initialize() {
logger.info("Initializing ...");
peerEurekaNodes.start();
try {
registry.init(peerEurekaNodes);
} catch (Exception e) {
throw new RuntimeException(e);
}
logger.info("Initialized");
}
}
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
@Override
public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
this.numberOfReplicationsLastMin.start();
this.peerEurekaNodes = peerEurekaNodes;
initializedResponseCache();
scheduleRenewalThresholdUpdateTask();
initRemoteRegionRegistry();
try {
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register the JMX monitor for the InstanceRegistry :", e);
}
}
}
scheduleRenewalThresholdUpdateTask
这个定时任务就是跟自我保护模式相关的了
private void scheduleRenewalThresholdUpdateTask() {
timer.schedule(new TimerTask() {
@Override
public void run() {
updateRenewalThreshold();
}
}, serverConfig.getRenewalThresholdUpdateIntervalMs(),
serverConfig.getRenewalThresholdUpdateIntervalMs());
}
其中getRenewalThresholdUpdateIntervalMs
默认值是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) {
// 仅当阈值大于当前的预期阈值或禁用自我保护时才更新阈值。
// 更新 预期中发送续约消息的客户端数量 expectedNumberOfClientsSendingRenews
// 更新 每分钟最小续约阈值numberOfRenewsPerMinThreshold
if ((count) > (serverConfig.getRenewalPercentThreshold() * expectedNumberOfClientsSendingRenews)
|| (!this.isSelfPreservationModeEnabled())) {
// 更新 预期中发送续约消息的客户端数量
this.expectedNumberOfClientsSendingRenews = count;
// 更新 每分钟最小续约阈值(numberOfRenewsPerMinThreshold)
updateRenewsPerMinThreshold();
}
}
}
}
服务注册
每当有一个实例注册上来时,预期中发送续约消息的客户端数量(expectedNumberOfClientsSendingRenews),和每分钟最小续约阈值(numberOfRenewsPerMinThreshold)都要更新。
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
// 省略部分代码...
if (this.expectedNumberOfClientsSendingRenews > 0) {
// 新注册上来的,所以原值+1
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
updateRenewsPerMinThreshold();
}
}
}
服务下线
每当有一个实例下线时,预期中发送续约消息的客户端数量(expectedNumberOfClientsSendingRenews),和每分钟最小续约阈值(numberOfRenewsPerMinThreshold)都要更新。
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
protected boolean internalCancel(String appName, String id, boolean isReplication) {
// 省略部分代码...
if (this.expectedNumberOfClientsSendingRenews > 0) {
// 一个服务下线了,所以要减去一个
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
updateRenewsPerMinThreshold();
}
}
}
开启自我保护模式
在服务端定时剔除过期服务时,会判断是否允许服务剔除,如果开启了自我保护或者当前服务续约个数 > 最小续约个数则允许服务剔除
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
public void evict(long additionalLeaseMs) {
// 是否允许服务剔除,如果允许服务剔除则走下面的服务剔除逻辑,如果不允许服务剔除则返回
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// 省略部分代码...
}
}
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
/**
* 是否允许服务剔除,没开自我保护,或者开启了自我保护,然而实际续约个数>最小续约个数,所以允许服务剔除。
*/
@Override
public boolean isLeaseExpirationEnabled() {
// 检查是否启用了自我保护
if (!isSelfPreservationModeEnabled()) {
return true;
}
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
}