eureka--踢出实例原理

1. 为什么要踢出注册实例

一个实例注册到eureka-server中。如果在规定的时间内没法发送心跳(续租)信息。服务器有权把它赶出。这类比于你租房,如果在规定的时间内你没有交房租,那么房东有权把你赶出。

但是:在网络世界踢出实例比现实世界有点复杂:原因是网络分区

 网络分区:检测网络失败是很困难的,我们得到其他节点状态的信息就是通过网络来得到,延迟跟网络失败也无从区分。

2.eureka 如何解决网络分区

  1. eukeka server默认开启了自我保护机制,当然可以关闭
  2. 如果开启自我保护机制。则eureka踢出实例时有一个最大的踢出个数(不能把所有的实例都踢出,剩下的要保护起来,不能在踢出了)。

    int expiredLeasesSize= 过期实例数;
    int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());//需要保护的最小实例数
    int toEvict = Math.min(expiredLeases.size(), evictionLimit);//server端决定本次踢出的实例数
    
  3. server端踢出时有一定的技巧,那就是随机的踢出toEvit大小的实例。如果不随机踢出,那么会造成整个应用下线。不可提供服务。这样影响应该均匀的分布到所有的应用中。

3. eukeak 踢出源码实现

3.1 eureka 通过一个Timer定时器来定时的踢出过期的实例
private Timer evictionTimer = new Timer("Eureka-EvictionTimer", true);

 //定时任务的初始化方法
 protected void postInit() {
       //...其他代码
       evictionTaskRef.set(new EvictionTask());
       evictionTimer.schedule(evictionTaskRef.get(),
               serverConfig.getEvictionIntervalTimerInMs(),
               serverConfig.getEvictionIntervalTimerInMs());
   }

一个定时器,必须有个任务。我们首先看下踢出过期实例的任务:EvictionTask

2.1.1 EvictionTask源码分析

重点看下。补偿时间的实现。
为什么定时任务执行时需要注意补偿时间?
假如一个定时任务在10:00开始,每隔1秒执行一次任务,但是由于full gc (Stop the word) 或者别的原因,造成下次任务实际执行的时间是:10:02。因此任务执行有延迟。整整延迟了1秒,这l秒就是上面提到的补偿时间。

补偿时间计算算法实现:

  1. 一个变量存放上次定时任务执行的具体时间点,如:
    lastExecutionNanosRef

  2. 计算从上次任务执行以来到本地任务执行的时间差值:

     long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
    
  3. 计算补偿时间:两次任务的时间间隔差- 定时任务的时间间隔差

     long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
    
  4. compensationTime<=0 ? 0:compensationTime

    源码:

    /**
    * 长时间没有续租时,进行回收的定时任务
    *
    * eviction:回收
    */
    /* visible for testing */
    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.
        *
        * 计算一个补偿时间作为下一个任务实际执行的时间。为什么这么做:是由于gc或者时钟便宜造成的时间改变。
        * 造成时间任务执行的时间比期望的时间晚点。
        *
        *
        *
        */
       long getCompensationTimeMs() {
           /**当前时间的纳秒*/
           long currNanos = getCurrentTimeNano();
    
           /**以原子方式设置新值。并返回旧值*/
           long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
           if (lastNanos == 0l) {
               return 0l;
           }
    
           /**
            * elapsed 表示过去的时间
            * 自从上个定时任务执行以来。已经过了这么长时间
            */
           long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
    
           /**
            * 补偿时间 == 过去的时间 - 定时回收的时间间隔
            *
            * < 0 ;代表时间还没到
            * > = 0;表示时间已经过了。需要补偿这部分时间差值
            */
           long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
    
    
           return compensationTime <= 0l ? 0l : compensationTime;
       }
    
       long getCurrentTimeNano() {  // for testing
           return System.nanoTime();
       }
    
    }
2.1.2 踢出源码分析
  1. 要解决的问题:网络分区
  2. 自我保护机制
  3. full gc等引起的补偿时间
  4. 洗牌算法 shuffle 参考:
    https://blog.csdn.net/ai_xiangjuan/article/details/80210899
/**
    * 踢出过期实例
    * @param additionalLeaseMs
    */
   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.

       /**
        * 收集所有的过期项到一个集合中,然后随机的踢出它们。
        * 对于一个较大的踢出集合项,如果我们不这么做(随机的踢出),在进入自我保护之前,我们可能移除某个app的整个实例
        * 这样影响应该均匀的分布到所有的应用程序中
        */
       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)
               //Knuth shuffle algorithm 参考:https://blog.csdn.net/ai_xiangjuan/article/details/80210899
               //随机的挑选一个
               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);
           }
       }
   }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值