深入理解Eureka - Eureka Evict机制


Eureka剔除流程

Eureka Server实现了一个Evict机制,用于剔除过期的Client,以清空非正常Cancel的过期Client。避免消费者调用过期的服务。

Eureka Server初始化后,在后台初始化一个EvictionTask,定时执行剔除任务(默认60S)。

  1. 首先EvictionTask会判断是否进行剔除任务:
    1. 如果关闭了自我保护,则不进行剔除任务。
    2. 如果开启了自我保护,继续判断如果【上一分钟续约数】大于【每分钟续约数阀值】,则不进行剔除任务。
    3. 只有开启了自我保护,且【上一分钟续约数】小于【每分钟续约数阀值】,才进行下一步的剔除任务。
    4. 备注:当实际续约数小于设定的阀值时,就认为存在没按时续约的Client了。
  2. 循环registry的数据结构,判断是否过期,如果过期了,保存到过期实例列表里。
  3. 计算要剔除的数量Evict Number(最大的剔除数=注册实例总数-注册实例总数*自我保护续约百分比阀值)
  4. 循环Evict Number,通过洗牌算法公平的随机剔除。(假如过期的实例<最大剔除数,则全部剔除)
  5. 剔除实例时首先会从数据结构中找到实例,然后将其清除。
  6. 清除之后将清除时间添加到recentlyChangedQueue队列中。
  7. 最后清空Guava缓存。
  8. 注意一:这里没有同步给其他节点,因为每个节点都有剔除任务,可以达到最终一直。
  9. 注意二:为什么不全部剔除?因为这里有个自我保护机制,当大量过期时,Eureka Server认为是自己出了问题,所以为了避免误伤,启动自我保护算法,只清除一部分过期实例。

Eureka剔除时序图

首先EurekaBootstrap(ServletContextListener的实现)初始化后调用registry.openForTraffic():

		// Copy registry from neighboring eureka node
		int registryCount = this.registry.syncUp();
		this.registry.openForTraffic(this.applicationInfoManager, registryCount);

然后PeerAwareInstanceRegistrImpl调用其父类的的postInit:

    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;
        updateRenewsPerMinThreshold();
        logger.info("Got {} instances from neighboring DS node", count);
        logger.info("Renew threshold is: {}", numberOfRenewsPerMinThreshold);
        this.startupTime = System.currentTimeMillis();
        if (count > 0) {
            this.peerInstancesTransferEmptyOnStartup = false;
        }
        DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
        boolean isAws = Name.Amazon == selfName;
        if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
            logger.info("Priming AWS connections for all replicas..");
            primeAwsReplicas(applicationInfoManager);
        }
        logger.info("Changing status to UP");
        applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
        super.postInit();
    }

在AbstractInstanceRegistry的postInit()里实现剔除任务的初始化:

    protected void postInit() {
        renewsLastMin.start();
        if (evictionTaskRef.get() != null) {
            evictionTaskRef.get().cancel();
        }
        evictionTaskRef.set(new EvictionTask());
        evictionTimer.schedule(evictionTaskRef.get(),
                serverConfig.getEvictionIntervalTimerInMs(),
                serverConfig.getEvictionIntervalTimerInMs());
    }

EvictionTask是一个TimerTask,启动之后run方法直接调用evit()剔除方法:

        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);
            }
        }
    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);
            }
        }
    }

执行EvictionTask的第一步就是判断实例过期是否可用,这里有两个条件:

1、自我保护机制是否开启

2、上一分钟的续约数是否小于设定的最小续约数阀值

只有开启了自我保护机制,且【上一分钟续约数】< 【最小续约数阀值】才会进行剔除操作。否则不进行剔除操作。

    public boolean isLeaseExpirationEnabled() {
        if (!isSelfPreservationModeEnabled()) {
            // The self preservation mode is disabled, hence allowing the instances to expire.
            return true;
        }
        return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
    }

剔除操作时,首先计算过期的实例,并添加到过期实例列表里(expiredLeases)。

然后再次计算要剔除的数量,注意这里由于自我保护机制的原因,剔除的最大数=实例总数-实例总数*自我保护阀值因子。

计算完后,通过一个洗牌算法,进行公平的剔除。

最后清空Guava缓存。

关于实际剔除数量的一点思考

为什么不把所有过期实例剔除呢?

因为服务故障都有可能发生,而Eureka Server本身也是个服务,所以当大量服务过期是,它谦虚的认为是自己出错了,所以只剔除部分过期实例,保留【自我保护阀值*实例总数】个实例。如果全部剔除的话,有可能会误伤所有服务。Eureka Server还是比较谦虚的。

 

Spring Cloud实战项目Jbone地址

github地址:https://github.com/417511458/jbone

码云地址:https://gitee.com/majunwei2017/jbone

马军伟
关于作者 马军伟
写的不错,支持一下

先给自己定个小目标,日更一新。