乐趣区

Eureka自我保护机制源码解析

默认情况下,当 EurekaServer 在一定时间内(默认 90 秒)没有接收到某个客户端实例的心跳,EurekaServer 将会注销该实例。但是当网络分区故障发生时,客户端与 EurekaServer 之间无法正常通信,此时不应该注销客户端。Eureka 通过“自我保护机制”来解决这个问题:当 EurekaServer 短时间内丢失过多客户端时,这个节点就会进入自我保护模式。在自我保护模式下,EurekaServer 不会剔除任何客户端。当网络故障恢复后,该节点会自动退出自我保护模式

自我保护机制的实现是基于维护服务注册表的类 AbstractInstanceRegistry 中的 2 个变量来维护的

/**
* 期望最小每分钟续租次数
*/
protected volatile int numberOfRenewsPerMinThreshold;
/**
* 期望最大每分钟续租次数
*/
protected volatile int expectedNumberOfRenewsPerMin;
服务端初始化

服务端的启动文章可以看这篇文章:EurekaServer 自动装配及启动流程解析
在服务端启动、从其他集群同步完信息后执行了一个方法:openForTraffic

public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
   this.expectedNumberOfRenewsPerMin = count * 2;
   this.numberOfRenewsPerMinThreshold =
           (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
    }

期望每分钟最大续租次数为:当前服务端已经注册的客户端的数量乘 2,为啥呢,因为默认 Eureka 的续约是 30 秒
期望每分钟最小续租次数为:最大续租次数乘续租百分比,默认续租百分比是 0.85,也就是说当某个时间窗内如果存在超过百分之十五的客户端没有再续租的话则开启自我保护模式

自我保护模式的定时任务

DefaultEurekaServerContext类中有一个 initialize 方法,这个方法在执行过程中会启动一个定时任务

    @PostConstruct
    @Override
    public void initialize() {logger.info("Initializing ...");
        peerEurekaNodes.start();
        try {registry.init(peerEurekaNodes);
        } catch (Exception e) {throw new RuntimeException(e);
        }
        logger.info("Initialized");
    }

public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {this.numberOfReplicationsLastMin.start();
        this.peerEurekaNodes = peerEurekaNodes;
        initializedResponseCache();
        scheduleRenewalThresholdUpdateTask();
        initRemoteRegionRegistry();

        try {Monitors.registerObject(this);
        } catch (Throwable e) {logger.warn("Cannot register the JMX monitor for the InstanceRegistry :", e);
        }
    }

scheduleRenewalThresholdUpdateTask这个定时任务就是跟自我保护模式相关的了

    private void scheduleRenewalThresholdUpdateTask() {timer.schedule(new TimerTask() {
                           @Override
                           public void run() {updateRenewalThreshold();
                           }
                       }, serverConfig.getRenewalThresholdUpdateIntervalMs(),
                serverConfig.getRenewalThresholdUpdateIntervalMs());
    }

其中 getRenewalThresholdUpdateIntervalMs 默认值是 15 分钟

private void updateRenewalThreshold() {
   try {
       // 1. 计算应用实例数
       Applications apps = eurekaClient.getApplications();
       int count = 0;
       for (Application app : apps.getRegisteredApplications()) {for (InstanceInfo instance : app.getInstances()) {if (this.isRegisterable(instance)) {++count;}
           }
       }
       // 2. 计算 expectedNumberOfRenewsPerMin、numberOfRenewsPerMinThreshold 参数
       synchronized (lock) {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);
   }
}

分为 2 步,第一步就不说了,看第二步
当最大续租数量大于最小续租数量时或者没有开启自我保护模式时可以重新计算两个值,否则不能重新计算

客户端注册
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {synchronized (lock) {if (this.expectedNumberOfRenewsPerMin > 0) {
             this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
             this.numberOfRenewsPerMinThreshold =
                     (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
         }
     }

}

每当有一个实例注册上来时,两个参数都要重新计算,最大期望续租数量 + 2 同样是因为默认 1 分钟续租 2 次

客户端下线
public boolean cancel(final String appName, final String id,
                     final boolean isReplication) {synchronized (lock) {if (this.expectedNumberOfRenewsPerMin > 0) {
               this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2;
               this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
        }
   }
}

于注册的处理逻辑恰好相反

开启自我保护模式

之前在 Eureka 客户端续约及服务端过期租约清理源码解析一文的租约过期清理解析过程中省略了关于自我保护模式的判断,现在再看一下。这个判断在租约过期处理方法的开头:

    public void evict(long additionalLeaseMs) {logger.debug("Running the evict task");

        if (!isLeaseExpirationEnabled()) {logger.debug("DS: lease expiration is currently disabled.");
            return;
        }
        //....
   }

详细内容在 isLeaseExpirationEnabled

    public boolean isLeaseExpirationEnabled() {if (!isSelfPreservationModeEnabled()) {return true;}
        return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;}
    public boolean isSelfPreservationModeEnabled() {return serverConfig.shouldEnableSelfPreservation();
    }
    public long getNumOfRenewsInLastMin() {return renewsLastMin.getCount();
    }

第一个 if 是判断是否开启自我保护模式
最后的 return 则是如果当前最小续租次数大于 0,并且最近续约实例数量大于最小期待续租数量

退出移动版