zoukankan      html  css  js  c++  java
  • 【一起学源码-微服务】Nexflix Eureka 源码十一:EurekaServer自我保护机制竟然有这么多Bug?

    前言

    前情回顾

    上一讲主要讲了服务下线,已经注册中心自动感知宕机的服务。
    其实上一讲已经包含了很多EurekaServer自我保护的代码,其中还发现了1.7.x(1.9.x)包含的一些bug,但这些问题在master分支都已修复了。

    服务下线会将服务实例从注册表中删除,然后放入到recentQueue中,下次其他EurekaClient来进行注册表抓取的时候就能感知到对应的哪些服务下线了。

    自动感知服务实例宕机不会调用下线的逻辑,所以我们还抛出了一个问题,一个client宕机,其他的client需要多久才能感知到?通过源码我们知道 至少要180s 才能被注册中心给摘除,也就是最快180s才能被其他服务感知,因为这里还涉及读写缓存和只读缓存不一致的情况。

    本讲目录

    本讲主要讲解注册中心一个独有的功能,如果使用Eureka作为注册中心的小伙伴可能都看过注册中心Dashboard上会有这么一段文字:

    image.png

    那注册中心为何要做这种自我保护呢?这里要跟注册中心的设计思想相关联了,我们知道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看看具体代码实现:

    image.png

    这里主要是看: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 分支此问题已经修复。如下图:
    image.png

    image.png

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

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

    image.png

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

    服务下线:
    image.png

    1. evict()方法为何不更新expectedNumberOfRenewsPerMin 按常理来说这里也应该进行 -2操作的,实际上并没有更新,于是看了下master分支源码仍然没有更新,于是早上我便在netflix eureka git
      上提了一个isssue:(我蹩脚的英语大家就不要吐槽了,哈哈哈)

    地址为:Where to update the "expectedNumberOfClientsSendingRenews" when we evict a instance?
    疑问:
    image.png

    搜索了github 发现也有人在2017年就遇到了这个问题,从最后一个回答来看这个问题依然没有解决:

    Eureka seems to do not recalculate numberOfRenewsPerMinThreshold during evicting expired leases

    image.png

    翻译如下:
    image.png

    总结

    一张图代为总结一下:

    08_注册中心自我保护机制原理流程图.png

    申明

    本文章首发自本人博客:https://www.cnblogs.com/wang-meng 和公众号:壹枝花算不算浪漫,如若转载请标明来源!

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

    22.jpg

  • 相关阅读:
    According to TLD or attribute directive in tag file, attribute end does not accept any expressions
    Several ports (8080, 8009) required by Tomcat v6.0 Server at localhost are already in use.
    sql注入漏洞
    Servlet—简单的管理系统
    ServletContext与网站计数器
    VS2010+ICE3.5运行官方demo报错----std::bad_alloc
    java 使用相对路径读取文件
    shell编程 if 注意事项
    Ubuntu12.04下eclipse提示框黑色背景色的修改方法
    解决Ubuntu环境变量错误导致无法正常登录
  • 原文地址:https://www.cnblogs.com/wang-meng/p/12131465.html
Copyright © 2011-2022 走看看