图解+源码讲解 Eureka Server 服务剔除逻辑

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第8天,点击查看活动详情

Eureka Server 服务剔除逻辑

取得成就时坚持不懈,要比遭到失败时顽强不屈更重要 —— 拉罗什夫科

相关文章
图解+源码讲解 Eureka Server 启动流程分析
图解+源码讲解 Eureka Client 启动流程分析
图解+源码讲解 Eureka Server 注册表缓存逻辑
图解+源码讲解 Eureka Client 拉取注册表流程
图解+源码讲解 Eureka Client 服务注册流程
图解+源码讲解 Eureka Client 心跳机制流程
图解+源码讲解 Eureka Client 下线流程分析

核心流程图

image.png

从哪里开始分析

    服务端初始化的时候进行的服务剔除逻辑初始化工作,在这个方法里面进行了具体的方法实现,其实仔细想想就知道服务端的剔除逻辑指定是在服务端初始化的时候进行的定义的

registry.openForTraffic(applicationInfoManager, registryCount);
复制代码

剔除核心思想

    仔细想想如何剔除呢,客户端在进行初始化的时候一定会向服务端进行注册,之后定时向服务端发送心跳,服务端进行心跳统计,如果没有发送心跳的实例就进行放入到有问题的队列,就是放入到没有发送心跳的队列中,进行随机实例剔除,但是如果实例过期的比较多的话那么就会进行数据对比,期待的心跳数量和实际发送的心跳数量进行对比,

剔除核心流程

计算期待的心跳次数

    根据传进来的实例个数进行心跳个数计算,这个实例个数是服务端从其他的服务节点拉取的值进行本地注册后计算出来的,通过 registry.syncUp() 这个方法计算出来的

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;
    // 也就是期待一分钟内 实例数量*2*0.85个心跳
    updateRenewsPerMinThreshold();
    /**
     * 每隔60s会运行一次定时调度的后台线程任务,EvictionTask,故障实例摘除任务
     */
    super.postInit();
}
复制代码

    更新计算心跳逻辑 updateRenewsPerMinThreshold(); 计算公式=实例数量*(60s/30s)*0.85

protected void updateRenewsPerMinThreshold() {
    // serverConfig.getExpectedClientRenewalIntervalSeconds() 默认 30s
    // 60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds() == 2、
    // serverConfig.getRenewalPercentThreshold())
    // 2 * 0.85 也就是期待一分钟内 实例数量*2*0.85个心跳
    this.numberOfRenewsPerMinThreshold = (int)
            (this.expectedNumberOfClientsSendingRenews * 
       (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds()) * 
             serverConfig.getRenewalPercentThreshold());
}
复制代码

创建驱动任务来计算心跳值

    创建一个驱动任务,进行调度,每60s进行一次调度

protected void postInit() {
    renewsLastMin.start();
    if (evictionTaskRef.get() != null) {
        evictionTaskRef.get().cancel();
    }
    evictionTaskRef.set(new EvictionTask());
    /**
     * 默认是60s执行一次
     */
    evictionTimer.schedule(evictionTaskRef.get(),
           serverConfig.getEvictionIntervalTimerInMs(),
           serverConfig.getEvictionIntervalTimerInMs());
}
复制代码

真正的剔除逻辑

是否开启自我保护机制

    isSelfPreservationModeEnabled() 默认是开启自我保护机制的,所以计算一下上一分钟的心跳次数和每分钟的心跳次数作对比,如果上一分钟的心跳次数大于你期待的那么就不进行自我保护机制往下面走

if (!isLeaseExpirationEnabled()) {
     return;
}

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

过滤出过期的实例

    创建一个过期实例列表,遍历注册表中的所有实例信息通过 lease.isExpired() 方法计算当前实例是否过期,如果过期的话那么就放入过期列表中进行后续的操作

// 创建一个过期实例列表
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
/**
 * 遍历注册表中所有的服务实例,然后调用Lease的isExpired()方法,
 * 来判断当前这个服务实例的租约是否过期了,是否失效了,服务实例故障了,
 * 如果是故障的服务实例,加入一个列表
 */
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);
            }
        }
    }
}
复制代码

计算过期实例个数限制

    获取本地注册表中的实例,计算最少发送心跳的个数,默认实例个数百分比是0.85,所以假如有40个实例的话,那么就是40 * 0.85 = 34个心跳次数,那么过期数量限制就是40 -34 = 6个

// 获取本地的注册表中的实例数量
int registrySize = (int) getLocalRegistrySize();// 假设只有40个实例信息
/**
 * serverConfig.getRenewalPercentThreshold() 默认是 0.85
   比如有40个实例那么就是
 * registrySizeThreshold = 0.85*40 = 34 个心跳次数
 */
// 计算最少发送心跳的的值
int registrySizeThreshold = (int) (registrySize * 
                                   serverConfig.getRenewalPercentThreshold());
// 计算过期数量限制 利用 40 -34 = 6 
int evictionLimit = registrySize - registrySizeThreshold;
复制代码

计算下线实例

    取出 evictionLimit 与过期的expiredLeases的size比大小,取出最小值,之后在过期的集合列表中随机下掉几个实例,并不是将所有的过期实例都摘除的,每次只将注册表中的15%的实例下掉,剩下的等下次任务到的时候进行实例剔除

/**
 * 取出 evictionLimit 与过期的expiredLeases的size比大小,取出最小值
 */
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
    /**
     * 随机下掉几个实例,不会一次性将所有故障的服务实例都摘除,
     * 每次最多讲注册表中15%的服务实例给摘除掉,所以一次没摘除所有的故障实例,
     * 下次EvictionTask再次执行的时候,会再次摘除,分批摘取机制
     * 在摘除的时候,是从故障实例中随机挑选本次可以摘除的数量的服务实例,
     * 来摘除,随机摘取机制,摘除服务实例的时候,
     * 其实就是调用下线的方法,internelCancel()方法,注册表、
     * recentChangeQueue、invalidate缓存
     */
    Random random = new Random(System.currentTimeMillis());
    for (int i = 0; i < toEvict; i++) {
        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();
        // 下线实例
        internalCancel(appName, id, false);
    }
}
复制代码

下线实例操作

    internalCancel 这个下线实例操作就是客户端的shutdown的操作,后续我们会在实例下线的时候进行梳理,因为这个操作还涉及到了下线后的服务集群同步的操作等等,后续都会拿出一篇文章进行讲解的

小结

  1. 计算每分钟期待的最少的心跳次数
  2. 根据上一分钟的心跳进行与每分钟期待的最少的心跳次数进行对比是否开启自我保护机制
  3. 如果没有开启自我保护那么就选(过期集合中的数量)和(注册表减去每分钟期待的最少的心跳次数)取一个最小值进行随机选择
  4. 将过滤出来的实例进行下线

猜你喜欢

转载自juejin.im/post/7084041146124468254