【一起学源码-微服务】Nexflix Eureka 源码十一:EurekaServer自我保护机制竟然有这么多Bug?
前言
前情回顾
上一讲主要讲了服务下线,已经注册中心自动感知宕机的服务。
其实上一讲已经包含了很多EurekaServer自我保护的代码,其中还发现了1.7.x(1.9.x)包含的一些bug,但这些问题在master分支都已修复了。
服务下线会将服务实例从注册表中删除,然后放入到recentQueue中,下次其他EurekaClient来进行注册表抓取的时候就能感知到对应的哪些服务下线了。
自动感知服务实例宕机不会调用下线的逻辑,所以我们还抛出了一个问题,一个client宕机,其他的client需要多久才能感知到?通过源码我们知道 至少要180s 才能被注册中心给摘除,也就是最快180s才能被其他服务感知,因为这里还涉及读写缓存和只读缓存不一致的情况。
本讲目录
本讲主要讲解注册中心一个独有的功能,如果使用Eureka作为注册中心的小伙伴可能都看过注册中心Dashboard上会有这么一段文字:
那注册中心为何要做这种自我保护呢?这里要跟注册中心的设计思想相关联了,我们知道Eureka是一个高可用的组件,符合CAP架构中的A、P,如果注册中心检测到很多服务实例宕机的时候,它不会将这些宕机的数据全都剔除,会做一个判断,如果宕机的服务实例大于所有实例数的15%,那么就会开启保护模式,不会摘除任何实例(代码中是通过每分钟所有实例心跳总数和期望实例心跳总数对比)。
试想,如果没有自我保护机制,注册中心因为网络故障,收不到其他服务实例的续约 而误将这些服务实例都剔除了,是不是就出大问题了。
目录如下:
evict()
方法解读expectedNumberOfRenewsPerMin
计算方式expectedNumberOfRenewsPerMin
自动更新机制- 注册中心
Dashboard
显示自我保护页面实现 - 自我保护机制bug汇总
技术亮点:
- 如何计算每一分钟内的内存中的计数呢?
MeassuredRate
计算每一分钟内的心跳的次数,保存上一分钟心跳次数和当前分钟的心跳次数 后面我们会看一下这个类似怎么实现的
说明
原创不易,如若转载 请标明来源:一枝花算不算浪漫
源码分析
evict()
方法解读
接着上一讲的内容,上一讲其实已经讲到了evict()
的使用,我们再来说下如何一步步调入进来的:
EurekaBootStrap.initEurekaServerContext()
中调用registry.openForTraffic()
, 然后进入PeerAwareInstanceRegistryImpl.openForTraffic()
方法,其中有调用super.postInit()
这里面直接进入到 AbstractInstanceRegistry.postInit()
方法,这里其实就是一个定时调度任务,默认一分钟执行一次,这里会执行EvictionTask
,在这个task里面会有一个run()
方法,最后就是执行到了evict()
方法了。
这里再来看下evict()
方法代码:
public void evict(long additionalLeaseMs) {
logger.debug("Running the evict task");
// 是否允许主动删除宕机节点数据,这里判断是否进入自我保护机制,如果是自我保护了则不允许摘除服务
if (!isLeaseExpirationEnabled()) {
logger.debug("DS: lease expiration is currently disabled.");
return;
}
// 省略服务摘除等等操作...
}
接着进入PeerAwareInstanceRegistryImpl.isLeaseExpirationEnabled()
:
public boolean isLeaseExpirationEnabled() {
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
return true;
}
// 这行代码触发自我保护机制,期望的一分钟要有多少次心跳发送过来,所有服务实例一分钟得发送多少次心跳
// getNumOfRenewsInLastMin 上一分钟所有服务实例一共发送过来多少心跳,10次
// 如果上一分钟 的心跳次数太少了(20次)< 我期望的100次,此时会返回false
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
这里我们先解读一下,上面注释已经说得很清晰了。
我们在代码中可以找到
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
这段的意思
expectedNumberOfRenewsPerMin
代表每分钟期待的心跳时间,例如现在有100次心跳,然后乘以默认的心跳配比85%,这里就是nuberOfRenewsPerMinThreshold的含义了如果上一分钟实际心跳次数小于这个值,那么就会进入自我保护模式
然后是getNumOfRenewsInLastMin()
:
private final MeasuredRate renewsLastMin;
public long getNumOfRenewsInLastMin() {
return renewsLastMin.getCount();
}
public class MeasuredRate {
private static final Logger logger = LoggerFactory.getLogger(MeasuredRate.class);
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;
/**
* @param sampleInterval in milliseconds
*/
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.
// renewsLastMin 为1分钟
// 每分钟调度一次,将当前的88次总心跳设置到lastBucket中去,然后将当前的currentBucket 设置为0 秒啊!
lastBucket.set(currentBucket.getAndSet(0));
} catch (Throwable e) {
logger.error("Cannot reset the Measured Rate", e);
}
}
}, sampleInterval, sampleInterval);
isActive = true;
}
}
public synchronized void stop() {
if (isActive) {
timer.cancel();
isActive = false;
}
}
/**
* Returns the count in the last sample interval.
*/
public long getCount() {
return lastBucket.get();
}
/**
* Increments the count in the current sample interval.
*/
public void increment() {
// 心跳次数+1 例如说1分钟所有服务实例共发起了88次心跳
currentBucket.incrementAndGet();
}
}
最上面我们说过,MeasuredRate
的设计是一个闪光点,看下重要的两个属性:
lastBucket
: 记录上一分钟总心跳次数currentBucket
: 记录当前最近一分钟总心跳次数
首先我们看下increment()
方法,这里看一下调用会发现在服务端处理续约renew()
中的最后会调用此方法,使得currentBucket
进行原子性的+1操作。
然后这里明有一个start()
方法,这里面也是个时间调度任务,我们可以看下sampleInterval
这个时间戳,在构造函数中被赋值,在AbstractInstanceRegistry
的构造方法中被调用,默认时间为一分钟。
这里最重要的是lastBucket.set(currentBucket.getAndSet(0));
每分钟调度一次,把当前一分钟总心跳时间赋值给上一分钟总心跳时间,然后将当前一分钟总心跳时间置为0.
expectedNumberOfRenewsPerMin
计算方式
我们上一讲中已经介绍过expectedNumberOfRenewsPerMin
的计算方式,因为这个属性很重要,所以这里再深入研究一下。
首先我们要理解这个属性的含义:期待的一分钟注册中心接收到的总心跳时间,接着看看哪几个步骤会更新:
- EurekaServer初始的时候会计算
在openForTraffic()
方法的入口会有计算 - 服务注册调用
register()
方法是会更新 - 服务下线调用
cancel()
方法时会更新 - 服务剔除
evict()
也应该调用,可惜是代码中并未找到调用的地方?这里其实是个bug,我们可以看后面自我保护机制Bug汇总
中提到更多详细内容。此问题至今未修复,我们先继续往后看。
expectedNumberOfRenewsPerMin
自动更新机制
Server端初始化上下文的时候,15分钟跑的一次定时任务:
scheduleRenewalThresholdUpdateTask
入口是:EurekaBootStrap.initEurekaServerContext()
方法,然后执行serverContext.initialize()
方法,里面的registry.init()
执行PeerAwareInstanceRegistryImpl.init()
中会执行scheduleRenewalThresholdUpdateTask()
,这个调度任务默认是每15分钟执行一次的,来看下源代码:
private void updateRenewalThreshold() {
try {
// count为注册表中服务实例的个数
// 将自己作为eureka client,从其他eureka server拉取注册表
// 合并到自己本地去 将从别的eureka server拉取到的服务实例的数量作为count
Applications apps = eurekaClient.getApplications();
int count = 0;
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
if (this.isRegisterable(instance)) {
++count;
}
}
}
synchronized (lock) {
// Update threshold only if the threshold is greater than the
// current expected threshold of if the self preservation is disabled.
// 这里也是存在bug的,master分支已经修复
// 一分钟服务实例心跳个数(其他eureka server拉取的服务实例个数 * 2) > 自己本身一分钟所有服务实例实际心跳次数 * 0.85(阈值)
// 这里主要是跟其他的eureka server去做一下同步
if ((count * 2) > (serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold)
|| (!this.isSelfPreservationModeEnabled())) {
this.expectedNumberOfRenewsPerMin = count * 2;
this.numberOfRenewsPerMinThreshold = (int) ((count * 2) * serverConfig.getRenewalPercentThreshold());
}
}
logger.info("Current renewal threshold is : {}", numberOfRenewsPerMinThreshold);
} catch (Throwable e) {
logger.error("Cannot update renewal threshold", e);
}
}
这里需要注意一点,为何上面说eurekaClient.getApplications()
是从别的注册中心获取注册表实例信息,因为一个eurekaServer对于其他注册中心来说也是一个eurekaClient。
这里注释已经写得很清晰了,就不再多啰嗦了。
注册中心Dashboard
显示自我保护页面实现
还是自己先找到对应jsp看看具体代码实现:
这里主要是看:registry.isBelowRenewThresold()
逻辑。
PeerAwareInstanceRegistryImpl.isBelowRenewThresold()
:
public int isBelowRenewThresold() {
if ((getNumOfRenewsInLastMin() <= numberOfRenewsPerMinThreshold)
&&
((this.startupTime > 0) && (System.currentTimeMillis() > this.startupTime + (serverConfig.getWaitTimeInMsWhenSyncEmpty())))) {
return 1;
} else {
return 0;
}
}
这里的意思就是 上一分钟服务实例实际总心跳个数 <= 一分钟期望的总心跳实例 * 85%,而且判断 Eureka-Server 是否允许被 Eureka-Client 获取注册信息。如果都满足的话就会返回1,当前警告信息就会在dashbord上显示自我保护的提示了。
这里面注意一下配置:
#getWaitTimeInMsWhenSyncEmpty()
:Eureka-Server 启动时,从远程 Eureka-Server 读取不到注册信息时,多长时间不允许 Eureka-Client 访问,默认是5分钟
自我保护机制bug汇总
- expectedNumberOfRenewsPerMin计算方式
this.expectedNumberOfRenewsPerMin = count * 2;
// numberOfRenewsPerMinThreshold = count * 2 * 0.85 = 34 期望一分钟 20个服务实例,得有34个心跳
this.numberOfRenewsPerMinThreshold =
(int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
这里为何要使用count * 2?count是注册表中所有的注册实例的数量,因为作者以为用户不会修改默认续约时间(30s), 所以理想的认为这里应该乘以2就是一分钟得心跳总数了。
好在看了master 分支此问题已经修复。如下图:
- 同理 服务注册、服务下线 都是将
注册:expectedNumberOfRenewsPerMin+2
下线:expectedNumberOfRenewsPerMin-2
master分支也给予修复,图片如下:
服务注册:
服务下线:
evict()
方法为何不更新expectedNumberOfRenewsPerMin
按常理来说这里也应该进行 -2操作的,实际上并没有更新,于是看了下master分支源码仍然没有更新,于是早上我便在netflix eureka
git
上提了一个isssue:(我蹩脚的英语大家就不要吐槽了,哈哈哈)
地址为:Where to update the "expectedNumberOfClientsSendingRenews" when we evict a instance?
疑问:
搜索了github 发现也有人在2017年就遇到了这个问题,从最后一个回答来看这个问题依然没有解决:
Eureka seems to do not recalculate numberOfRenewsPerMinThreshold during evicting expired leases
翻译如下:
总结
一张图代为总结一下:
申明
本文章首发自本人博客:https://www.cnblogs.com/wang-meng 和公众号:壹枝花算不算浪漫,如若转载请标明来源!
感兴趣的小伙伴可关注个人公众号:壹枝花算不算浪漫
【一起学源码-微服务】Nexflix Eureka 源码十一:EurekaServer自我保护机制竟然有这么多Bug?的更多相关文章
- 【一起学源码-微服务】Eureka+Ribbon+Feign阶段性总结
前言 想说的话 这里已经梳理完Eureka.Ribbon.Feign三大组件的基本原理了,今天做一个总结,里面会有一个比较详细的调用关系流程图. 说明 原创不易,如若转载 请标明来源! 博客地址:一枝 ...
- 【一起学源码-微服务】Ribbon源码五:Ribbon源码解读汇总篇~
前言 想说的话 [一起学源码-微服务-Ribbon]专栏到这里就已经全部结束了,共更新四篇文章. Ribbon比较小巧,这里是直接 读的spring cloud 内嵌封装的版本,里面的各种config ...
- 【一起学源码-微服务】Feign 源码一:源码初探,通过Demo Debug Feign源码
前言 前情回顾 上一讲深入的讲解了Ribbon的初始化过程及Ribbon与Eureka的整合代码,与Eureka整合的类就是DiscoveryEnableNIWSServerList,同时在Dynam ...
- 【一起学源码-微服务】Ribbon 源码三:Ribbon与Eureka整合原理分析
前言 前情回顾 上一篇讲了Ribbon的初始化过程,从LoadBalancerAutoConfiguration 到RibbonAutoConfiguration 再到RibbonClientConf ...
- 【一起学源码-微服务】Ribbon 源码一:Ribbon概念理解及Demo调试
前言 前情回顾 前面文章已经梳理清楚了Eureka相关的概念及源码,接下来开始研究下Ribbon的实现原理. 我们都知道Ribbon在spring cloud中担当负载均衡的角色, 当两个Eureka ...
- 【一起学源码-微服务】Ribbon 源码二:通过Debug找出Ribbon初始化流程及ILoadBalancer原理分析
前言 前情回顾 上一讲讲了Ribbon的基础知识,通过一个简单的demo看了下Ribbon的负载均衡,我们在RestTemplate上加了@LoadBalanced注解后,就能够自动的负载均衡了. 本 ...
- 【一起学源码-微服务】Ribbon 源码四:进一步探究Ribbon的IRule和IPing
前言 前情回顾 上一讲深入的讲解了Ribbon的初始化过程及Ribbon与Eureka的整合代码,与Eureka整合的类就是DiscoveryEnableNIWSServerList,同时在Dynam ...
- 【一起学源码-微服务】Feign 源码三:Feign结合Ribbon实现负载均衡的原理分析
前言 前情回顾 上一讲我们已经知道了Feign的工作原理其实是在项目启动的时候,通过JDK动态代理为每个FeignClinent生成一个动态代理. 动态代理的数据结构是:ReflectiveFeign ...
- 【一起学源码-微服务】Feign 源码二:Feign动态代理构造过程
前言 前情回顾 上一讲主要看了@EnableFeignClients中的registerBeanDefinitions()方法,这里面主要是 将EnableFeignClients注解对应的配置属性注 ...
随机推荐
- Gym - 101480K_K - Kernel Knights (DFS)
题意:有两队骑士各n人,每位骑士会挑战对方队伍的某一个位骑士. (可能相同) 要求找以一个区间s: 集合S中的骑士不会互相挑战. 每个集合外的骑士必定会被集合S内的某个骑士挑战. 题解:讲真被题目绕懵 ...
- Pytorch的网络结构可视化(tensorboardX)(详细)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/xiaoxifei/article/det ...
- 1x1卷积
你可能会想为什么有人会用1x1卷积,因为它关注的不是一块像素,而是一个像素,图1 图1 我们看看传统的卷积,它基本上是运行在一个小块图像上的小分类器,但仅仅是个线性分类器.图2 图2 如果你在中间加一 ...
- es6 set简析
1.数据结构Set类似于数组,但是成员的值都是唯一的,没有重复的值. var s = new Set(); [,,,,,,].map(x => s.add(x)) for (i of s) {d ...
- @codeforces - 418D@ Big Problems for Organizers
目录 @description@ @solution@ @accepted code@ @details@ @description@ n 个点连成一棵树,经过每条边需要花费 1 个单位时间. 现给出 ...
- MyBatis-使用XML或注解的简单实例
一.导入jar包 <dependency> <groupId>junit</groupId> <artifactId>junit</artifac ...
- Error: Cannot find module 'webpack/bin/config-yargs' 报错原因, webpack@4.X踩的坑~
1 .使用webpack@4.32.2时, 当我通过package.json的script去执行webpack-dev-server时, 报以下错误: Error: Cannot find modu ...
- 搭建服务器上的GIT并实现自动同步到站点目录(www)
https://blog.csdn.net/baidu_30000217/article/details/51327289 前言:当我们想要实现几个小伙伴合作开发同一个项目,或者建立一个资源分享平台的 ...
- Vue 路由的嵌套使用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Python--day30--tcp协议(建立链接三次握手,断掉链接四次挥手)和UDP协议
TCP协议: tcp是可靠的,面向连接的.建立全双工通信. 建立链接的三次握手 链接一旦建立一定是全双工工通信,必然是双方通信. UDP协议: TCP协议和UDP协议的对比: QQ使用的是UDP,因为 ...