2023-09-17  阅读(3)
原文作者:carl.zhao 原文地址: https://carlzone.blog.csdn.net/article/details/112298750

下面是官方提供的 Eureka 架构图:

202309172251325181.png

1、Eureka Server 为什么要服务过期

正常情况下,应用实例下线时候会主动向 Eureka-Server 发起下线请求,也就是我们之前分析的 – 8、Eureka 源码解析 之 Eureka Client 服务下线。但实际情况下,应用实例可能异常崩溃,又或者是网络异常等原因,导致下线请求无法被成功提交。

这种情况之后,需要 Eureka Client 定时向 Eureka Server 发送续约配合 Eureka Client 通过定时任务清理超时的租约解决上述异常。

2、EvictionTask

EvictionTask 是清理租约过期任务,下面是它的调用时序图:

202309172251331002.png
当 Eureka Server 启动的时候就会调用实现了 Java Servlet 规范 ServletContextListener 监听器的 EurekaBootStrap 到调用初始化注册服务 AbstractInstanceRegistry。然后会启用以下几个定时任务:

  • MeasuredRate,统计定时任务,在 AbstractInstanceRegistry 的构建器创建 MeasuredRate 对象的时候传入 1000 * 60 * 1,然后在这里调用它的 start 方法里面有一个定时任务,每隔 60 秒也就是每隔 1 分钟执行一次。这个定时任务里面有 2 个 AtomicLong 类型的参数。一个是 AtomicLong currentBucket 每进行一次续约的时候就会调用它 + 1,另一个是AtomicLong lastBucketMeasuredRate任务每分钟进行执行的时候就会把AtomicLong currentBucket 里面的值设置到AtomicLong lastBucket当中去,然后把AtomicLong currentBucket值清空。然后通过获取 AtomicLong lastBucket 的值就能够得到最近一分钟续约的次数( 最近一分钟续约的次数这个在后面要讲的 Eureka Server 自我保护机制当中会使用到)
  • 另外一个就是定时调用 EvictionTask 任务,过期注册表中超时续约的应用。默认 60 秒执行一次

3、服务过期

下面我们来分析一下服务过期的逻辑:

EvictionTask

        class EvictionTask extends TimerTask {
    
            private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);
    
            @Override
            public void run() {
                try {
                    long compensationTimeMs = getCompensationTimeMs();
                    logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
                    evict(compensationTimeMs);
                } catch (Throwable e) {
                    logger.error("Could not run the evict task", e);
                }
            }
    
            /**
             * compute a compensation time defined as the actual time this task was executed since the prev iteration,
             * vs the configured amount of time for execution. This is useful for cases where changes in time (due to
             * clock skew or gc for example) causes the actual eviction task to execute later than the desired time
             * according to the configured cycle.
             */
            long getCompensationTimeMs() {
                long currNanos = getCurrentTimeNano();
                long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
                if (lastNanos == 0l) {
                    return 0l;
                }
    
                long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
                long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
                return compensationTime <= 0l ? 0l : compensationTime;
            }
    
            long getCurrentTimeNano() {  // for testing
                return System.nanoTime();
            }
    
        }

compute a compensation time defined as the actual time this task was executed since the prev iteration,
vs the configured amount of time for execution. This is useful for cases where changes in time (due to
clock skew or gc for example) causes the actual eviction task to execute later than the desired time
according to the configured cycle.

首先它会调用 getCompensationTimeMs 方法计算一个补偿时间,由于 JVM GC ,又或是时间偏移( clock skew ) 等原因,定时器执行实际比预期会略有延迟。

传入计算获取到的补偿时间,调用 evict 方法过期超时续约的应用。

AbstractInstanceRegistry#evict

        public void evict(long additionalLeaseMs) {
            logger.debug("Running the evict task");
    
            if (!isLeaseExpirationEnabled()) {
                logger.debug("DS: lease expiration is currently disabled.");
                return;
            }
    
            // We collect first all expired items, to evict them in random order. For large eviction sets,
            // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it,
            // the impact should be evenly distributed across all applications.
            List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
            for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
                Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
                if (leaseMap != null) {
                    for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
                        Lease<InstanceInfo> lease = leaseEntry.getValue();
                        if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                            expiredLeases.add(lease);
                        }
                    }
                }
            }
    
            // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for
            // triggering self-preservation. Without that we would wipe out full registry.
            int registrySize = (int) getLocalRegistrySize();
            int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
            int evictionLimit = registrySize - registrySizeThreshold;
    
            int toEvict = Math.min(expiredLeases.size(), evictionLimit);
            if (toEvict > 0) {
                logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit);
    
                Random random = new Random(System.currentTimeMillis());
                for (int i = 0; i < toEvict; i++) {
                    // Pick a random item (Knuth shuffle algorithm)
                    int next = i + random.nextInt(expiredLeases.size() - i);
                    Collections.swap(expiredLeases, i, next);
                    Lease<InstanceInfo> lease = expiredLeases.get(i);
    
                    String appName = lease.getHolder().getAppName();
                    String id = lease.getHolder().getId();
                    EXPIRED.increment();
                    logger.warn("DS: Registry: expired lease for {}/{}", appName, id);
                    internalCancel(appName, id, false);
                }
            }
        }
  • 首先会判断 Eureka Server 是否开启的自我保护机制,如果开启了自我保护机制( 下一篇博客会讲 )
  • 首先遍历注册表注册的应用列表,找到所有续约过期的应用实例列表。过期判断条件是:当前时间 > 最后一次续约时间 + 90 秒 + 补偿时间。
  • 然后调用 getLocalRegistrySize 获取当前注册列表中应用实例的个数
  • 接着通过下面的公式获取一个期望过期的个数 evictionLimit。evictionLimit = 注册列表所有应用实例的个数 - 注册列表所有应用实例的个数 * 获得续订的最小百分比(默认 0.85)。也就是:期望过期的个数 = 应用实例 * 0.15。如果有20 个服务应用那么这个值就是 3.
  • 然后取第一步获取到续约过期的应用实例个数与期望过期的应用实例个数的最小值
  • 如果这个最小值大于 0,就随机过期续约过期的应用实例列表里面的应用实例并且 调用内部取消逻辑(之前服务下线分析过)

如果 Eureka Client 默认会每 30 秒发送心跳到 Eureka Server。如果 Eureka Server 在进行服务过期判断的时候也就是: 最后一次续约时间 + 90 秒 + 补偿时间小于系统当前时间 就会以服务进行过期。

有以下两点需要注意一下:

  • Eureka Client 在进行续约的时候是把 lastUpdateTimestamp 设置成系统的当前时间 + 90 秒。然后 Eureka Server 在进行服务过期判断的时候是: 最后一次续约时间 + 90 秒 + 补偿时间小于系统当前时间 就会以服务进行过期。最后在 Eureka Client 拉取注册表是每 30 秒进行拉取。所以一个服务不可用时其它服务需要感知到这个服务不可用就需要:90 秒 + 90 秒 + 30 秒 = 210秒,也就是 3 分半 才能感知到
  • Eureka Server 在服务过期的时候会根据:应用实例 * 0.15 与 心跳过期应用个数随机顺序将它们剔除出去。因为对于需要大量剔除服务应用时,如果我们不这样做,我们可能会在 Eureka 自我保护开始之前清除整个应用程序。通过随机化,影响应该均匀分布在所有应用程序中。

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文