“不积跬步,无以至千里。”
eureka专题的第10篇文章,来聊聊eureka所谓的自我保护机制。
提起eureka的自我保护机制,相信人人都能说两句,大概的意思,就是eureka在一段时间内,如果过期的服务实例过多,超多了一个比例,eureka server就会认为自己作为注册中心一定是网络发生了故障,导致接收不到客户端的心跳,这个时候就会进入一个保护模式,就不会再摘除任何服务实例了,然后静静地等待自己的网络恢复,巴拉巴拉… …
大概就是这么个意思,这个保护机制,有些地方说生产环境建议开启,看个人情况吧。
直接来看AbstractInstanceRegistry
的evict
方法
上来就会在isLeaseExpirationEnabled()
方法里判断一下,判断上一分钟的心跳次数,是否小于我期望的一分钟的心跳次数,如果小于,那么压根儿就不让清理任何服务实例,就直接return了
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
... ...
}
首先会看一下isSelfPreservationModeEnabled()
这个配置,如果是false,也不会开启保护模式,不过默认是ture的,所以如果我们想禁用保护模式,把这个参数设置为false即可
@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;
}
@Override
public boolean shouldEnableSelfPreservation() {
return configInstance.getBooleanProperty(
namespace + "enableSelfPreservation", true).get();
}
关键的一行代码来了:
numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold
numberOfRenewsPerMinThreshold
,就是期望的心跳数,记住这个参数,服务注册,下线,故障摘除,都会重新计算这个值
就是说,首先这个期望的心跳数大于0,如果不大于0,直接返回false,不摘除(没实例摘啥)
然后getNumOfRenewsInLastMin()
获取上一分钟实际的心跳数,这个后面分析,如果不大于期望的心跳次数,返回false,不摘除
ok,分析完了这一块代码逻辑,那么这个所谓的“期望心跳数”是在哪里初始化的呢??
答案是:Eureka Server初始化的时候。
还记得上篇文章说的registry.openForTraffic(applicationInfoManager, registryCount)
这个方法吗?之前我们说摘除服务实例的逻辑说过,其实计算期望的心跳数的逻辑也在这个方法中。
我把关键的一些代码贴出来,
// Copy registry from neighboring eureka node
int registryCount = registry.syncUp();
registry.openForTraffic(applicationInfoManager, registryCount);
首先 registry.syncUp()
,这是从相邻的其他server节点拷贝注册表,然后返回的registryCount
就是服务实例的个数,作为了openForTraffic
方法的参数之一
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// Renewals happen every 30 seconds and for a minute it should be a factor of 2.
this.expectedNumberOfClientsSendingRenews = count;
updateRenewsPerMinThreshold();
... ...
}
这里把registryCount
赋值给了expectedNumberOfClientsSendingRenews
这个变量,然后调用了关键的计算期望心跳的逻辑 updateRenewsPerMinThreshold()
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
这个numberOfRenewsPerMinThreshold
就是前面说的期望心跳数,我们看看是怎么计算出来的
首先,expectedNumberOfClientsSendingRenews 是注册的实例数,比方说有10个
这个serverConfig.getExpectedClientRenewalIntervalSeconds()
是eureka client发送心跳续约的时间间隔,默认是30s,用60/30,得到2,也就是说一个client每秒发送两次心跳,如果你配置的续约周期是60,那么这个结果就是1
最后这个serverConfig.getRenewalPercentThreshold()
是续约实例的比例,默认是0.85
所以说10 * 2 * 0.85,最后计算得出,10个服务实例,默认一分钟的期望心跳次数是17次!!!
如果小于这个值,就会开启保护模式,不再下线任何服务实例。
服务注册,下线,故障摘除,都会重新调用这个方法计算这个期望心跳次数的值
比如,随便找一下服务注册的代码
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();
}
}
把expectedNumberOfClientsSendingRenews
加1,然后调用updateRenewsPerMinThreshold()
方法
如果没猜错,服务下线就是-1了
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
// Since the client wants to cancel it, reduce the number of clients to send renews.
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
updateRenewsPerMinThreshold();
}
}
结果跟我们想的完全一样
故障实例摘除,也会调用internalCancel()
方法,也是走上面的逻辑,重新计算期望心跳次数的值
最后,我们来看看它怎么计算实际的心跳次数的,getNumOfRenewsInLastMin()
,这里面用了一个MeasutredRate
的机制
private final MeasuredRate renewsLastMin;
... ...
@com.netflix.servo.annotations.Monitor(name = "numOfRenewsInLastMin",
description = "Number of total heartbeats received in the last minute", type = DataSourceType.GAUGE)
@Override
public long getNumOfRenewsInLastMin() {
return renewsLastMin.getCount();
}
private final AtomicLong lastBucket = new AtomicLong(0);
... ...
public long getCount() {
return lastBucket.get();
}
最终是调用了MeasutredRate
的一个getCount()
方法,从一个AtomicLong
中拿到了收集的一分钟的实例心跳次数
那么这个lastBucket是怎么统计的?
你想想这个所谓的统计心跳跟什么有关,当然是注册表,那么这个MeasutredRate
是在哪里被构建和初始化的呢?
答案呼之欲出:注册表初始化的时候
来到AbstractInstanceRegistry
这个老生常谈的类看一看吧,就在注册表初始化的时候
protected AbstractInstanceRegistry(EurekaServerConfig serverConfig, EurekaClientConfig clientConfig, ServerCodecs serverCodecs) {
this.serverConfig = serverConfig;
this.clientConfig = clientConfig;
this.serverCodecs = serverCodecs;
this.recentCanceledQueue = new CircularQueue<Pair<Long, String>>(1000);
this.recentRegisteredQueue = new CircularQueue<Pair<Long, String>>(1000);
this.renewsLastMin = new MeasuredRate(1000 * 60 * 1);
this.deltaRetentionTimer.schedule(getDeltaRetentionTask(),
serverConfig.getDeltaRetentionTimerIntervalInMs(),
serverConfig.getDeltaRetentionTimerIntervalInMs());
}
this.renewsLastMin = new MeasuredRate(1000 * 60 * 1)
,这里new了一个对象,传了一个1分钟的参数进去,这个参数就是控制其内部的调度任务运行的周期,sampleInterval,1分钟哥们!!!
public MeasuredRate(long sampleInterval) {
this.sampleInterval = sampleInterval;
this.timer = new Timer("Eureka-MeasureRateTimer", true);
this.isActive = false;
}
public synchronized void start() {
if (!isActive) {
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
// Zero out the current bucket.
lastBucket.set(currentBucket.getAndSet(0));
} catch (Throwable e) {
logger.error("Cannot reset the Measured Rate", e);
}
}
}, sampleInterval, sampleInterval);
isActive = true;
}
}
lastBucket.set(currentBucket.getAndSet(0))
这行代码,是整个MeasuredRate
机制的精髓所在,调度任务1分钟执行一次,每次将currentBucket
这个当前这一分钟的eureka集群客户端所有心跳次数赋值给lastBucket
,然后清空,归零,归零之后就重新统计了,然后evict任务需要获取客户端实际心跳跟期望心跳比对的时候,调用lastBucket.get()
即可拿到,后面的事情我们都知道了,如果实际心跳次数小于期望心跳,就会开启保护模式。
而每次客户端调用renew()
方法来续约的时候,这个currentBucket
就会+1
renewsLastMin.increment();
public void increment() {
currentBucket.incrementAndGet();
}
整个自我保护机制到这个程度,已经相当的清晰明了了。