续租, 下线等操作比较直观, 实际上也不复杂. 让我们自己想想它们大概会在服务端有什么操作.
renew: 更新 Lease 的
lastUpdateTimestamp
, 更新一下 InstanceInfo 的最新状态. 然后调用其他同伴节点的 renew 接口.
cancel: 把 lease 从 registry 中移除, 设置 lease 的 evictionTimestamp, 然后设置 InstanceInfo 为已删除. 然后把 lease 加入到
recentlyChangedQueue
中. 最后调用同伴节点的 cancel 接口.
-- 服务端确实做了差不多这些事情. 因此为了不影响其他重要事件, 这里不再继续深入.
自我保护机制
Eureka 选择做一个支持 AP 的系统(CAP 定理). 其实很好理解, 当发生大量应用实例不再 renew 时, 服务端认为发生了网络分区. Eureka 为了保证高可用, 不再踢掉已经到期的 lease, 从而让依赖于该服务端实例的 client 端仍然能正常进行服务发现(尽管存在服务实例确实挂掉的可能, 即牺牲了一致性).
在 Spring Cloud Eureka 的 Home 页面上面经常会见到这个警告, 就是启用了自我保护机制.
关于这个问题网上的解释也很多了, 下面这篇文章提供了几幅有用的图:
The Mystery of Eureka Self-Preservation
实现细节
这篇文章有所有你想要的. Eureka 源码解析 -- 应用实例注册发现 (四) 之自我保护机制
搬过来一些重点:
1. 触发条件
看代码
- // PeerAwareInstanceRegistryImpl.java
- @Override
- public boolean isLeaseExpirationEnabled() {
- if (!isSelfPreservationModeEnabled()) {
- // The self preservation mode is disabled, hence allowing the instances to expire.
- return true;
- }
- return numberOfRenewsPerMinThreshold> 0 && getNumOfRenewsInLastMin()> numberOfRenewsPerMinThreshold;
- }
当每分钟心跳次数( renewsLastMin ) 小于 numberOfRenewsPerMinThreshold 时, 并且开启自动保护模式开关( eureka.server.enableSelfPreservation = true ) 时, 触发自动保护机制, 不再自动过期租约.
其中:
2. 计算公式
- numberOfRenewsPerMinThreshold
- =
- expectedNumberOfRenewsPerMin
* 续租百分比(
eureka.server.renewalPercentThreshold
, 默认 0.85 )
expectedNumberOfRenewsPerMin
= 当前注册的应用实例数 x 2
为什么乘以 2:
默认情况下, 注册的应用实例每半分钟续租一次, 那么一分钟心跳两次, 因此 x 2 .
这里有硬编码的情况, 因此修改应用实例的续租频率会让计算不太准. 不过, 自我保护机制我比较怀疑它的重要性, 该调还得调.
3. 计算时机
有四个地方会重新计算 numberOfRenewsPerMinThreshold , expectedNumberOfRenewsPerMin.
Eureka-Server 启动时. 先从相邻节点同步 registry 中的信息, 得到已注册的实例数量, 然后套公式计算.
定时重新计算. 计算方法同上类似. 频率由
eureka.server.renewalThresholdUpdateIntervalMs
控制.
应用实例注册时, 增加了注册实例数, 所以要重算.
实例下线时, 同理.
自动清理机制
清理过期 lease 的也是一个定时任务 EvictionTask, 频率由 eureka.server.evictionIntervalTimerInMs, 默认为 60 秒.
1. EvictionTask 代码
- class EvictionTask extends TimerTask {
- @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);
- }
- }
- }
那个 getCompensationTimeMs()方法的意思是, 定时器可能比设定的时间更晚触发, 原因可能是 GC 等. 假设本来要在上一次执行后 60s 再次触发, 但因为 GC 在第 70 秒才被触发, 这时去检查有没有 lease 有没有超期无 renew 不能用 70s 来算, 而应该还用 60s 来算, 因为在这 10s 的 GC 时间中, 很可能此服务端无法处理注册请求. 补偿的 10s 就是这个意思, 就是允许实例多超期这 10s.
2. 清理逻辑
evict(compensationTime)又比较长, 下面分段分析.
判断是不是启用了自我保护机制, 如是则不再清理.
遍历 registry 中的所有实例, 比较当前时间和 lastUpdatedTime + duration + compensationTime, 如果前者大, 说明已过期.
计算最大可清理的实例数量. 不得让清理后的实例数量低于当前数量 *
eureka.server.renewalPercentThreshold
, 默认又是 0.85. 这个阈值还是很高的, 如果有大量实例过期, 就需要分多批执行才能清理完.
随机清理过期的租约. 由于租约是按照应用顺序添加到数组, 通过随机的方式, 尽量避免单个应用被全部过期.
最后, 调用 internalCancel 处理每个要被清理的 lease, 这个方法就是前面的 cancel 阶段会调的.
这个自动清理似乎没有告知同伴节点, 大家各做各的. 可见 Eureka 的一致性是蛮低的.
来源: https://juejin.im/post/5c093b9be51d451d8e5ca3da