Eureka源码12-深度解析Eureka的自我保护机制
欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
0. 环境
- eureka版本:1.10.11
- Spring Cloud : 2020.0.2
- Spring Boot :2.4.4
1.自我保护机制介绍
想必用过eureka 的同学都在eureka web
控制台见过这么一行大红字,我现在还能想起来第一次用eureka 出现这个的心里的那种感觉,什么东西,这么显眼,然后就很懵逼,立马搜了一下这个是什么鬼,就搜到说eureka自我保护机制触发
了,说是 “eureka 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,eureka 会将这些实例保护起来,让这些实例不会过期”(需要注意这句话的前半部分,我们这篇文章将用源码推翻它),其实就是某段时间内收到心跳数量低于期望心跳数量85%,就会触发这个自我保护机制,这个时候,服务实例将不会被过期
。
2.实现原理
-
一个核心原理:
实际收到的心跳 小于 我心里能接受的最小心跳数 ,这个时候就会触发eureka 自我保护机制
-
两个概念(都很好理解):
- 一个是
期望心跳数
,期望心跳数与服务实例多少是有关系的,比如说我注册表中有10个实例,然后每30s发送一次心跳(也就是续约),那么我这个每分钟期望心跳数就是 10 * 2 ,也就是我期望收到 20个心跳。 - 还有一个是
我心里能接受的最小心跳数
,网络环境这么复杂,你总不能让这些实例的心跳一个都不少吧,所以这里就出现了一个心里能接受的最小心跳数(心跳数阈值),它的计算方式就是期望心跳数 * 0.85
(这个0.85 是默认的,是不是与上面那个85% 就对起来了)
- 一个是
有了 期望心跳数
与 最小心跳数阈值
,我们现在还差的是啥?那就是统计收到的心跳数
:
比如说我专门有个计数器,然后以一个时间单位为一个窗口,就是统计每分钟收到的心跳次数(续约次数),其实光有收集当前分钟心跳数的计数器还不够,总不能拿当前分钟的心跳数与心里能接受的最小心跳数 做比较吧,那分钟开始的时候,肯定会触发这个自我保护机制(毕竟分钟开始的时候,没收到多少心跳)
这个时候一个计数器就不够了,还得需要一个,一个用于统计当前分钟收到的心跳数量,一个用于存储上一分钟收到的心跳数
,然后拿上一分钟收到的心跳数 与 心里能接受的最小心跳数 做比较,然后小于心里能接受的最小心跳数,就触发自我保护机制
。
有了2个心跳计数器,还不够,还缺一个定时器,然后每分钟执行一次将 当前分钟收集到的心跳数 给 存储上一分钟这个计数器上,然后将当前分钟计数器清0,表示开启新一轮的计数。
到这里,其实大体上就已经讲清楚了,但是还有几个点:
- 最开始的时候
期望心跳数
是怎么出来的? - 服务注册,服务下线 这个
期望心跳数
是怎样变化的?
现在解答一下:
- 最开始的时候,
eureka server 启动会去其他server节点上拉取注册表,如果拉到注册表的话,遍历注册表里面的实例信息,然后挨个 按照注册的方式 注册到本机的注册表上
,它会有一个返回一个注册多少实例count ,接着就是按照这个count *2
初始化的。 - 服务注册的时候,期望心跳数 +2 ,我多一个实例,一个实例每分钟默认发送2个心跳,这个时候就会+2 。服务下线的时候 期望心跳数 -2,然后心里能接受的最小心跳数 也会重新算一遍。
好了,以上就是eureka 自我保护机制的实现原理了,只不过我用自己的话将代码翻译了一遍,如果还是不能明白,可以看一下第4节的那张图。接下来我们就要看一下源码了
3.源码解析
3.1期望心跳数 与 我心里能接受的最小心跳数
先看一下关于 期望心跳数 与 我心里能接受的最小心跳数 变量的定义
在 eureka-core
项目的注册表抽象类AbstractInstanceRegistry
中定义了这两个变量:
// 服务端统计最近一分钟预期收到客户端实例心跳续租的请求数 我心里能接受的最小心跳数
protected volatile int numberOfRenewsPerMinThreshold;
// 服务端统计预期收到心跳续租的客户端实例数 期望心跳数
protected volatile int expectedNumberOfClientsSendingRenews;
接着看下初始化,服务注册,服务下线 期望心跳数 与 我心里能接受的最小心跳数
变动。
3.1.1 初始化
eureka server在启动的时候,会进行初始化,它会到其他server 节点上同步一下注册表,然后注册到自己本地的注册表中,这个时候会返回一个同步数量count,后面注册表中的这两个变量会根据这个count计算出来
// com.netflix.eureka.EurekaBootStrap#initEurekaServerContext
int registryCount = registry.syncUp();
registry.openForTraffic(applicationInfoManager, registryCount);
先是同步注册表,返回同步实例的数量。
@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;
// todo 更新预期每分钟收到心跳续租请求数
updateRenewsPerMinThreshold();
...
}
接着就是调用注册表的 openForTraffic
方法,初始化这两个变量,改变applicationInfoManager
为up状态,完成注册表 初始化之后的动作
这里期望心跳数 直接就是同步实例数量*2
,我心里能接受的最小心跳数 是期望心跳数 * 0.85(默认是0.85)
3.1.2 服务注册
在注册表抽象类AbstractInstanceRegistry
中的register
方法中
可以看到 期望心跳数 +2
,然后 我心里能接受的最小心跳数
重新算了一遍。
3.1.3 服务下线
这个是在注册表抽象类AbstractInstanceRegistry
的实现类PeerAwareInstanceRegistryImpl
中的cancel
方法下internalCancel()
做的处理:
它这里有个很大的问题就是这个+2 和-2 直接写死了
,如果我心跳间隔改变了,这个时候不是30s发一次心跳了,变成了1分钟一次,然后eureka 就会一直处于自我保护机制中
。
3.2 关于计数器
3.2.1 计数器初始化
eureka server 启动的时候,会执行一堆初始化,这个是在EurekaBootstrap
中的,之后就是创建注册表,在注册表抽象类AbstractInstanceRegistry
的构造方法中创建了这个计数器。
然后再初始化最后面调用了一下注册表的openForTraffic
方法,初始化期望心跳数
与 我心里能接受的最小心跳数
这两个变量,改变applicationInfoManager
为up状态,完成注册表 初始化之后的动作,就在注册表 初始化之后的动作里面启动了计数器。
先来看下 MeasuredRate
这类中关键的几个成员变量:
// 除了这里使用这个计数任务工具类
// 还有 PeerAwareInstanceRegistryImpl.numberOfReplicationsLastMin 字段也使用该工具类
// numberOfReplicationsLastMin:服务端统计最后一分钟同步复制给集群节点的操作数
public class MeasuredRate {
private static final Logger logger = LoggerFactory.getLogger(MeasuredRate.class);
// 最近一分钟(上一分钟)的计数
// getCount() 返回该计数
private final AtomicLong lastBucket = new AtomicLong(0);
// 当前一分钟正在统计的计数
private final AtomicLong currentBucket = new AtomicLong(0);
// 定时任务执行间隔
private final long sampleInterval;
private final Timer timer;
private volatile boolean isActive;
}
它的start()
方法其实就是启动这个定时任务
它这里是1分钟执行一次,就是sampleInterval
这个成员决定的,这个成员实在创建这个对象的传过来的,然后就是1分钟。
看下这个画红框的代码,其实就是将 当前分钟收集到的心跳数
给 存储上一分钟这个计数器 lastBucket
上,然后将当前分钟计数器 currentBucket清0
,表示开启新一轮的计数。
3.2.2 心跳计数
其实每收到一个心跳(续约),都会给currentBucket 这变量自增1,我们看一下代码
public boolean renew(String appName, String id, boolean isReplication) {
// todo renew计数,最近一分钟处理的心跳续租数+1,自我保护机制会用到
renewsLastMin.increment();
// todo 续约
leaseToRenew.renew();
return true;
}
}
public void increment() {
currentBucket.incrementAndGet();
}
3.3 关于自我保护模式的判断
public int isBelowRenewThresold() {
if ((getNumOfRenewsInLastMin() <= numberOfRenewsPerMinThreshold)
&&
((this.startupTime > 0) && (System.currentTimeMillis() > this.startupTime + (serverConfig.getWaitTimeInMsWhenSyncEmpty())))) {
return 1;
} else {
return 0;
}
}
public long getNumOfRenewsInLastMin() {
return renewsLastMin.getCount();
}
上面就是就是自我保护模式的判断了,很清楚。
3.4 eureka web中那行大红字
在resource 的header.jsp
中有这么几行
先是调用注册表的isBelowRenewThresold
方法判断是不是触发了自我保护机制:
要想显示那行触发自我保护机制的大红字,需要 系统启动了5分钟以上,而且触发了自我保护机制,其实还需要isSelfPreservationModeEnabled
这个参数是true,这样子才会显示这个触发自我保护的大红字
3.5 触发自我保护对过期下线的影响
在注册表 postInit方法中,其实还启动一个定时任务,扫描那些过期的实例,然后进行服务下线动作,这里主要是介绍触发自我保护对过期下线的影响,不会深入剖析服务扫描下线这一堆的东西
这个定时任务是1分钟执行一次的。
进入这个evict 方法中。
如果触发了自我保护机制就不往下走了,其实下面就是扫描注册表中所有的实例信息,然后看看过没过期,过期就走服务下线逻辑。我们来看下这个
isLeaseExpirationEnabled
方法的实现。
如果没有启用这个自我保护机制的话,直接通过,走下面服务扫描过期的逻辑。如果启用了,还用判断 收到的心跳数大于期望最小心跳数 ,这个时候才能继续往下走,如果是小于的话,就是触发自我保护机制了,就不会再往下走了,也就是不会再扫描下线过期的实例。
4. 流程图
5. 总结
本篇文章主要是介绍了eureka自我保护机制的实现原理,不管是语言描述,还是代码剖析,都已经很明了了。
我没有找到所谓的 15分钟内心跳失败比例小于85% 就是触发自我保护机制,在源码中,我只分析出 你上一分钟心跳数 低于 期望心跳数的85% 就会触发自我保护机制
,我不知道他们所谓的15分钟是哪里的,我只相信源码出真知,然后这里还发现计算期望心跳数 的硬编码问题
,如果你心跳间隔改变了,不是30s了,就很容易出现bug。
参考文章
eureka-0.10.11源码(注释) springcloud-source-study学习github地址 Eureka源码解析 SpringCloud技术栈系列文章 Eureka 源码解析
转载自:https://juejin.cn/post/7158261215133696031