摘要: 原创出处 http://www.iocoder.cn/Eureka/instance-registry-evict/ 「芋道源码」欢迎转载,保留摘要,谢谢!html
本文主要基于 Eureka 1.8.X 版本java
🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:segmentfault
- RocketMQ / MyCAT / Sharding-JDBC 全部源码分析文章列表
- RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
- 您对于源码的疑问每条留言都将获得认真回复。甚至不知道如何读源码也能够请教噢。
- 新的源码解析文章实时收到通知。每周更新一篇左右。
- 认真的源码交流微信群。
本文主要分享 Eureka-Server 过时超时续租的租约。数组
FROM 《深度剖析服务发现组件Netflix Eureka》
微信
推荐 Spring Cloud 书籍:网络
推荐 Spring Cloud 视频:架构
正常状况下,应用实例下线时候会主动向 Eureka-Server 发起下线请求。但实际状况下,应用实例可能异常崩溃,又或者是网络异常等缘由,致使下线请求没法被成功提交。app
介于这种状况,经过 Eureka-Client 心跳延长租约,配合 Eureka-Server 清理超时的租约解决上述异常。dom
com.netflix.eureka.registry.AbstractInstanceRegistry.EvictionTask
,清理租约过时任务。在 Eureka-Server 启动时,初始化 EvictionTask 定时执行,实现代码以下:ide
// AbstractInstanceRegistry.java /** * 清理租约过时任务 */ private final AtomicReference<EvictionTask> evictionTaskRef = new AtomicReference<EvictionTask>(); protected void postInit() { // .... 省略无关代码 // 初始化 清理租约过时任务 if (evictionTaskRef.get() != null) { evictionTaskRef.get().cancel(); } evictionTaskRef.set(new EvictionTask()); evictionTimer.schedule(evictionTaskRef.get(), serverConfig.getEvictionIntervalTimerInMs(), serverConfig.getEvictionIntervalTimerInMs()); }
配置 eureka.evictionIntervalTimerInMs
,清理租约过时任务执行频率,单位:毫秒。默认,60000 毫秒。
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); } } }
调用 #compensationTimeMs()
方法,得到补偿时间毫秒数。计算公式 = 当前时间 - 最后任务执行时间 - 任务执行频率。为何须要补偿时间毫秒数,在 「4. 过时逻辑」Lease#isisExpired(additionalLeaseMs)
方法 揭晓。#compensationTimeMs()
实现代码以下:
/** * 最后任务执行时间 */ private final AtomicLong lastExecutionNanosRef = new AtomicLong(0L); long getCompensationTimeMs() { long currNanos = getCurrentTimeNano(); long lastNanos = lastExecutionNanosRef.getAndSet(currNanos); if (lastNanos == 0L) { return 0L; } long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos); long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs(); return compensationTime <= 0L ? 0L : compensationTime; }
因为 JVM GC ,又或是时间偏移( clock skew ) 等缘由,定时器执行实际比预期会略有延迟。笔者在本机低负载运行,大概 10 ms 内。
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.
调用 #evict(compensationTime)
方法,执行清理过时租约逻辑,在 「4. 过时逻辑」 详细解析。
调用 #evict(compensationTime)
方法,执行清理过时租约逻辑,实现代码以下:
1: public void evict(long additionalLeaseMs) { 2: logger.debug("Running the evict task"); 3: 4: if (!isLeaseExpirationEnabled()) { 5: logger.debug("DS: lease expiration is currently disabled."); 6: return; 7: } 8: 9: // 得到 全部过时的租约 10: // We collect first all expired items, to evict them in random order. For large eviction sets, 11: // if we do not that, we might wipe out whole apps before self preservation kicks in. By randomizing it, 12: // the impact should be evenly distributed across all applications. 13: List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>(); 14: for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) { 15: Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue(); 16: if (leaseMap != null) { 17: for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) { 18: Lease<InstanceInfo> lease = leaseEntry.getValue(); 19: if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) { // 过时 20: expiredLeases.add(lease); 21: } 22: } 23: } 24: } 25: 26: // 计算 最大容许清理租约数量 27: // To compensate for GC pauses or drifting local time, we need to use current registry size as a base for 28: // triggering self-preservation. Without that we would wipe out full registry. 29: int registrySize = (int) getLocalRegistrySize(); 30: int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold()); 31: int evictionLimit = registrySize - registrySizeThreshold; 32: 33: // 计算 清理租约数量 34: int toEvict = Math.min(expiredLeases.size(), evictionLimit); 35: if (toEvict > 0) { 36: logger.info("Evicting {} items (expired={}, evictionLimit={})", toEvict, expiredLeases.size(), evictionLimit); 37: 38: // 逐个过时 39: Random random = new Random(System.currentTimeMillis()); 40: for (int i = 0; i < toEvict; i++) { 41: // Pick a random item (Knuth shuffle algorithm) 42: int next = i + random.nextInt(expiredLeases.size() - i); 43: Collections.swap(expiredLeases, i, next); 44: Lease<InstanceInfo> lease = expiredLeases.get(i); 45: 46: String appName = lease.getHolder().getAppName(); 47: String id = lease.getHolder().getId(); 48: EXPIRED.increment(); 49: logger.warn("DS: Registry: expired lease for {}/{}", appName, id); 50: internalCancel(appName, id, false); 51: } 52: } 53: }
第 3 至 7 行 :判断容许执行清理过时租约逻辑,主要和自我保护机制有关,在 《Eureka 源码解析 —— 应用实例注册发现(四)之自我保护机制》 有详细解析。
第 9 至 24 行 :得到全部过时的租约集合。
第 19 行 :调用 Lease#isisExpired(additionalLeaseMs)
方法,判断租约是否过时,实现代码以下:
// Lease.java public boolean isExpired(long additionalLeaseMs) { return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs)); } public void renew() { lastUpdateTimestamp = System.currentTimeMillis() + duration; }
😈注意:在不考虑 additionalLeaseMs
参数的状况下,租约过时时间比预期多了一个 duration
,缘由在于 #renew()
方法错误的设置 lastUpdateTimestamp = System.currentTimeMillis() + duration
,正确的设置应该是 lastUpdateTimestamp = System.currentTimeMillis()
。
Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to +duration more than what it should be, the expiry will actually be 2 * duration. This is a minor bug and should only affect instances that ungracefully shutdown. Due to possible wide ranging impact to existing usage, this will not be fixed.
TODO[0023]:additionalLeaseMs
第 26 至 34 行 :计算最大容许清理租约的数量,后计算容许清理租约的数量。
😈注意:即便 Eureka-Server 关闭自我保护机制,若是使用renewalPercentThreshold = 0.85
默认配置,结果会是分批逐步过时。举个例子:
// 假设 20 个租约,其中有 10 个租约过时。 // 第一轮执行开始 int registrySize = 20; int registrySizeThreshold = (int) (20 * 0.85) = 17; int evictionLimit = 20 - 17 = 3; int toEvict = Math.min(10, 3) = 3; // 第一轮执行结束,剩余 17 个租约,其中有 7 个租约过时。 // 第二轮执行开始 int registrySize = 17; int registrySizeThreshold = (int) (17 * 0.85) = 14; int evictionLimit = 17 - 14 = 3; int toEvict = Math.min(7, 3) = 3; // 第二轮执行结束,剩余 14 个租约,其中有 4 个租约过时。 // 第三轮执行开始 int registrySize = 14; int registrySizeThreshold = (int) (14 * 0.85) = 11; int evictionLimit = 14 - 11 = 3; int toEvict = Math.min(4, 3) = 3; // 第三轮执行结束,剩余 11 个租约,其中有 1 个租约过时。 // 第四轮执行开始 int registrySize = 11; int registrySizeThreshold = (int) (11 * 0.85) = 9; int evictionLimit = 11 - 9 = 2; int toEvict = Math.min(1, 2) = 1; // 第四轮执行结束,剩余 10 个租约,其中有 0 个租约过时。结束。
renewalPercentThreshold = 0
。因为 JVM GC ,或是本地时间差别缘由,可能自我保护机制的阀值 expectedNumberOfRenewsPerMin
、numberOfRenewsPerMinThreshold
不够正确,在过时这个相对“危险”的操做,从新计算自我保护的阀值。
第 35 至 51 行 :随机清理过时的租约。因为租约是按照应用顺序添加到数组,经过随机的方式,尽可能避免单个应用被所有过时。
i
)。第 50 行 :调用 #internalCancel()
方法,下线已过时的租约,在 《Eureka 源码解析 —— 应用实例注册发现(四)之自我保护机制》「3.2 下线应用实例信息」 有详细解析。
😫 本来以为比较容易的一篇文章,结果消耗了比想象中的时间,可能有四个小时。主要卡在补偿时间,目前也没弄懂。若是有知道的胖友,麻烦告知下。
胖友,分享个人公众号( 芋道源码 ) 给你的胖友可好?