【一起学源码-微服务】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%,那么就会开启保护模式,不会摘除任何实例(代码中是通过每分钟所有实例心跳总数和期望实例心跳总数对比)。

试想,如果没有自我保护机制,注册中心因为网络故障,收不到其他服务实例的续约 而误将这些服务实例都剔除了,是不是就出大问题了。

目录如下:

  1. evict()方法解读
  2. expectedNumberOfRenewsPerMin计算方式
  3. expectedNumberOfRenewsPerMin自动更新机制
  4. 注册中心Dashboard显示自我保护页面实现
  5. 自我保护机制bug汇总

技术亮点:

  1. 如何计算每一分钟内的内存中的计数呢?
    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;
}

这里我们先解读一下,上面注释已经说得很清晰了。

  1. 我们在代码中可以找到this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
  2. 这段的意思expectedNumberOfRenewsPerMin 代表每分钟期待的心跳时间,例如现在有100次心跳,然后乘以默认的心跳配比85%,这里就是nuberOfRenewsPerMinThreshold的含义了
  3. 如果上一分钟实际心跳次数小于这个值,那么就会进入自我保护模式

然后是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的设计是一个闪光点,看下重要的两个属性:

  1. lastBucket: 记录上一分钟总心跳次数
  2. currentBucket: 记录当前最近一分钟总心跳次数

首先我们看下increment()方法,这里看一下调用会发现在服务端处理续约renew()中的最后会调用此方法,使得currentBucket进行原子性的+1操作。

然后这里明有一个start()方法,这里面也是个时间调度任务,我们可以看下sampleInterval这个时间戳,在构造函数中被赋值,在AbstractInstanceRegistry的构造方法中被调用,默认时间为一分钟。

这里最重要的是lastBucket.set(currentBucket.getAndSet(0)); 每分钟调度一次,把当前一分钟总心跳时间赋值给上一分钟总心跳时间,然后将当前一分钟总心跳时间置为0.

expectedNumberOfRenewsPerMin计算方式

我们上一讲中已经介绍过expectedNumberOfRenewsPerMin的计算方式,因为这个属性很重要,所以这里再深入研究一下。

首先我们要理解这个属性的含义:期待的一分钟注册中心接收到的总心跳时间,接着看看哪几个步骤会更新:

  1. EurekaServer初始的时候会计算
    openForTraffic() 方法的入口会有计算
  2. 服务注册调用register()方法是会更新
  3. 服务下线调用cancel()方法时会更新
  4. 服务剔除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汇总

  1. 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 分支此问题已经修复。如下图:

  1. 同理 服务注册、服务下线 都是将

注册:expectedNumberOfRenewsPerMin+2
下线:expectedNumberOfRenewsPerMin-2

master分支也给予修复,图片如下:
服务注册:

服务下线:

  1. 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 和公众号:壹枝花算不算浪漫,如若转载请标明来源!

感兴趣的小伙伴可关注个人公众号:壹枝花算不算浪漫

原文地址:https://www.cnblogs.com/wang-meng/p/12131465.html

时间: 2024-10-09 10:53:40

【一起学源码-微服务】Nexflix Eureka 源码十一:EurekaServer自我保护机制竟然有这么多Bug?的相关文章

【一起学源码-微服务】Ribbon 源码四:进一步探究Ribbon的IRule和IPing

前言 前情回顾 上一讲深入的讲解了Ribbon的初始化过程及Ribbon与Eureka的整合代码,与Eureka整合的类就是DiscoveryEnableNIWSServerList,同时在DynamicServerListLoadBalancer中会调用PollingServerListUpdater 进行定时更新Eureka注册表信息到BaseLoadBalancer中,默认30s调度一次. 本讲目录 我们知道Ribbon主要是由3个组件组成的: ILoadBalancer IRule IP

【一起学源码-微服务】Nexflix Eureka 源码十三:Eureka源码解读完结撒花篇~!

前言 想说的话 [一起学源码-微服务-Netflix Eureka]专栏到这里就已经全部结束了. 实话实说,从最开始Eureka Server和Eureka Client初始化的流程还是一脸闷逼,到现在Eureka各种操作都了然于心了. 本专栏从12.17开始写,一直到今天12.30(文章在平台是延后发布的),这将近半个月的时间确实收获很多.每天都会保持一定的时间学习,只要肯下功夫,没有学不会的东西. 2020年将继续保持学习的节奏,自己定的目标是把spring cloud几个重要的组件都学一遍

【一起学源码-微服务】Nexflix Eureka 源码十:服务下线及实例摘除,一个client下线到底多久才会被其他实例感知?

前言 前情回顾 上一讲我们讲了 client端向server端发送心跳检查,也是默认每30钟发送一次,server端接收后会更新注册表的一个时间戳属性,然后一次心跳(续约)也就完成了. 本讲目录 这一篇有两个知识点及一个疑问,这个疑问是在工作中真真实实遇到过的. 例如我有服务A.服务B,A.B都注册在同一个注册中心,当B下线后,A多久能感知到B已经下线了呢? 不知道大家有没有这个困惑,这篇文章最后会对此问题答疑,如果能够看到文章的结尾,或许你就知道答案了,当然答案也会在结尾揭晓. 目录如下: C

springcloud微服务实战:Eureka+Zuul+Ribbon+Hystrix+SpringConfig

原文地址:http://blog.csdn.net/yp090416/article/details/78017552 springcloud微服务实战:Eureka+Zuul+Ribbon+Hystrix+SpringConfig 相信现在已经有很多小伙伴已经或者准备使用springcloud微服务了,接下来为大家搭建一个微服务框架,后期可以自己进行扩展.会提供一个小案例: 服务提供者和服务消费者 ,消费者会调用提供者的服务,新建的项目都是用springboot,附源码下载. coding仓库

springcloud微服务实战:Eureka+Zuul+Feign/Ribbon+Hystrix Turbine+SpringConfig+sleuth+zipkin

参考:springcloud微服务实战:Eureka+Zuul+Feign/Ribbon+Hystrix Turbine+SpringConfig+sleuth+zipkin 原创 2017年09月18日 11:46:28 标签: 微服务架构 / 微服务组件 / eureka / ribbon / zuul 26459 springcloud微服务实战:Eureka+Zuul+Feign/Ribbon+Hystrix Turbine+SpringConfig+sleuth+zipkin 相信现在

springCloud进阶(微服务架构&amp;Eureka)

springCloud进阶(微服务架构&Eureka) 1. 微服务集群 1.1 为什么要集群 为了提供并发量,有时同一个服务提供者可以部署多个(商品服务).这个客户端在调用时要根据一定的负责均衡策略完成负载调用. 1.2 服务提供者集群-同一种服务(服务名)部署多个 实际生产就是同一种服务多部署几台服务器,开发时就是用端口来区分. 1) 拷贝一份8001 2) 修改主类-改名 3) 改8002yml,端口 4) 服务提供者 1.3 服务消费者负载均衡调用 1)常见的负载均衡技术 Ribbon

Eureka微服务云架构源码分析

在看具体源码前,我们先回顾一下之前我们所实现的内容,从而找一个合适的切入口去分析.首先,服务注册中心.服务提供者.服务消费者这三个主要元素来说,后两者(也就是Eureka客户端)在整个运行机制中是大部分通信行为的主动发起者,而注册中心主要是处理请求的接收者.所以,我们可以从Eureka的客户端作为入口看看它是如何完成这些主动通信行为的. 我们在将一个普通的Spring Boot应用注册到Eureka Server中,或是从Eureka Server中获取服务列表时,主要就做了两件事: 在应用主类

springcloud 项目源码 微服务 分布式 Activiti6 工作流 vue.js html 跨域 前后分离

1.代码生成器: [正反双向](单表.主表.明细表.树形表,快速开发利器)freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本.处理类.service等完整模块2.多数据源:(支持同时连接无数个数据库,可以不同的模块连接不同数的据库)支持N个数据源3.阿里数据库连接池druid,安全权限框架 shiro(菜单权限和按钮权限), 缓存框架 ehcache4.代码编辑器,在线模版编辑,仿开发工具编辑器5.调用摄像头拍照 自定义裁剪编辑头像,头像图片色度调节6.we

Spring cloud 微服务架构 Eureka篇

1 服务发现 ## 关于服务发现 在微服务架构中,服务发现(Service Discovery)是关键原则之一.手动配置每个客户端或某种形式的约定是很难做的,并且很脆弱.Spring Cloud提供了多种服务发现的实现方式,例如:Eureka.Consul.Zookeeper. Spring Cloud支持得最好的是Eureka,其次是Consul,最次是Zookeeper. 2.创建一个Maven工程(microservice-discovery-eureka),并在pom.xml中加入如下内