《Dubbo 实现原理与源码解析 —— 精品合集》《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》

摘要: 原创出处 http://www.iocoder.cn/Eureka/instance-registry-fetch-delta/ 「芋道源码」欢迎转载,保留摘要,谢谢!

本文主要基于 Eureka 1.8.X 版本


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

1. 概述

本文主要分享 Eureka-Client 向 Eureka-Server 获取增量注册信息的过程

前置阅读:《Eureka 源码解析 —— 应用实例注册发现(六)之全量获取》

FROM 《深度剖析服务发现组件Netflix Eureka》

Eureka-Client 获取注册信息,分成全量获取增量获取。默认配置下,Eureka-Client 启动时,首先执行一次全量获取进行本地缓存注册信息,而后每 30增量获取刷新本地缓存( 非“正常”情况下会是全量获取 )。

本文重点在于增量获取

推荐 Spring Cloud 书籍

推荐 Spring Cloud 视频

2. 应用集合一致性哈希码

Applications.appsHashCode ,应用集合一致性哈希码

增量获取注册的应用集合( Applications ) 时,Eureka-Client 会获取到:

  1. Eureka-Server 近期变化( 注册、下线 )的应用集合
  2. Eureka-Server 应用集合一致性哈希码

Eureka-Client 将变化的应用集合和本地缓存的应用集合进行合并后进行计算本地的应用集合一致性哈希码。若两个哈希码相等,意味着增量获取成功;若不相等,意味着增量获取失败,Eureka-Client 重新和 Eureka-Server 全量获取应用集合。

Eureka 比较应用集合一致性哈希码,和日常我们通过哈希码比较两个对象是否相等类似。

2.1 计算公式

appsHashCode = ${status}_${count}_

  • 使用每个应用实例状态( status ) + 数量( count )拼接出一致性哈希码。若数量为 0 ,该应用实例状态不进行拼接。状态以字符串大小排序
  • 举个例子,8 个 UP ,0 个 DOWN ,则 appsHashCode = UP_8_ 。8 个 UP ,2 个 DOWN ,则 appsHashCode = DOWN_2_UP_8_
  • 实现代码如下:

    // Applications.java
    public String getReconcileHashCode() {
    // 计数集合 key:应用实例状态
    TreeMap<String, AtomicInteger> instanceCountMap = new TreeMap<String, AtomicInteger>();
    populateInstanceCountMap(instanceCountMap);
    // 计算 hashcode
    return getReconcileHashCode(instanceCountMap);
    }
    • 调用 #populateInstanceCountMap() 方法,计算每个应用实例状态的数量。实现代码如下:

      // Applications.java
      public void populateInstanceCountMap(Map<String, AtomicInteger> instanceCountMap) {
      for (Application app : this.getRegisteredApplications()) {
      for (InstanceInfo info : app.getInstancesAsIsFromEureka()) {
      // 计数
      AtomicInteger instanceCount = instanceCountMap.computeIfAbsent(info.getStatus().name(),
      k -> new AtomicInteger(0));
      instanceCount.incrementAndGet();
      }
      }
      }

      public List<Application> getRegisteredApplications() {
      return new ArrayList<Application>(this.applications);
      }

      // Applications.java
      public List<InstanceInfo> getInstancesAsIsFromEureka() {
      synchronized (instances) {
      return new ArrayList<InstanceInfo>(this.instances);
      }
      }
      • 计数那块代码,使用 Integer 即可,无需使用 AtomicInteger 。
  • 调用 #getReconcileHashCode() 方法,计算 hashcode 。实现代码如下:

    public static String getReconcileHashCode(Map<String, AtomicInteger> instanceCountMap) {
    StringBuilder reconcileHashCode = new StringBuilder(75);
    for (Map.Entry<String, AtomicInteger> mapEntry : instanceCountMap.entrySet()) {
    reconcileHashCode.append(mapEntry.getKey()).append(STATUS_DELIMITER) // status
    .append(mapEntry.getValue().get()).append(STATUS_DELIMITER); // count
    }
    return reconcileHashCode.toString();
    }

2.2 合理性

本小节,建议你理解完全文后,再回到此处
本小节,建议你理解完全文后,再回到此处
本小节,建议你理解完全文后,再回到此处

笔者刚看完应用集合一致性哈希算法的计算公式,处于一脸懵逼的状态。这么精简的方式真的能够校验出数据的一致性么?不晓得有多少读者跟笔者有一样的疑惑。下面我们来论证该算法的合理性( 一本正经的胡说八道 )。

一致性哈希值通过状态 + 数量来计算,那么是不是可能状态总数是一样多,实际分布在不同的应用?那么我们列举模型如下:

UP
应用Am
应用Bn

如果此时应用A 下线了 c 个原应用实例,应用B 注册了 c 个信应用实例,那么处于 UP 状态的数量仍然是 m + n 个。

  • 正常情况下,Eureka-Client 从 Eureka-Server 获取到完整的增量变化并合并,此时应用情况如下表格所示,两者是一致的,一致性哈希算法合理
UP (server)UP (client)
应用Am - cm - c
应用Bn + cn + c
  • 异常情况下【1】,变更记录队列全部过期。那 Eureka-Client 从 Eureka-Server 获取到空的增量变化并合并,此时应用情况如下表格所示,两者应用是不相同的, 一致性哈希值却是相等的,一致性哈希算法不合理
UP (server)UP (client)
应用Am - cm
应用Bn + cn
  • 异常情况下【2】,变更记录队列部分过期,例如应用A 和 应用B 都剩余 w 条变更记录。那 Eureka-Client 从 Eureka-Server 获取到部分的增量变化并合并,两者应用是不相同的,此时应用情况如下表格所示,一致性哈希值却是相等的,一致性哈希算法不合理
UP (server)UP (client)
应用Am - cm - w
应用Bn + cn + w

What ? 从异常情况【1】【2】可以看到,一致性哈希算法竟然是不合理的,那么我们手动来做一次最精简的实验。实验如下:

  • 模拟场景:异常情况【1】,m = n = c = 1 。简单粗暴。
  • 特别配置
    • eureka.retentionTimeInMSInDeltaQueue = 1 ,变更记录队列每条记录存活时长 1 ms。用以实现 Eureka-Client 请求不到完整的增量变化。
    • eureka.deltaRetentionTimerIntervalInMs = 1 ,变更记录队列每条记录过期定时任务执行频率 1 ms。用以实现 Eureka-Client 请求不到完整的增量变化。
    • eureka.shouldUseReadOnlyResponseCache = false ,禁用响应缓存的只读缓存。用以避免等待缓存刷新。
    • eureka.waitTimeInMsWhenSyncEmpty = 1
  • 实验过程
    1. 00:00 启动 Eureka-Server
    2. 00:30 启动应用A ,向 Eureka-Server 注册
    3. 01:00 启动 Eureka-Client ,向 Eureka-Server 获取注册信息,等待获取到应用A
    4. 01:30 关闭应用A 。立即启动应用B ,向 Eureka-Server 注册
    5. 等待 5 分钟,Eureka-Client 无法获取到应用B
    6. 此时应用情况如下表格所示,两者应用是不相同的,一致性哈希值却是相等的,一致性哈希算法不合理。
UP (server)UP (client)
应用A01
应用B10

🙂结论🙂

当然排除掉特别极端的场景,Eureka-Client 从 Eureka-Server 因为网络异常导致一直同步不到增量变化,又恰好应用关闭和开启满足状态统计数量。另外,变更记录队列记录过期时长为 300 秒,增量获取频率为 30 秒,获取的次数有 10 次左右。所以,应用集合一致性哈希码在绝大多数场景是合理的笔者的YY,解决这个极小场景有如下方式:

  • 第一种,修改计算公式 appsHashCode = MD5(${app_name}_${instance_id}_${status}_${count}_) ,增加对应用名和应用实例编号敏感。
  • 第二种,每 N 分钟进行一次全量获取注册信息。

ps :笔者怀着忐忑的心写完了这个小节,如果有不合理的地方,又或者有不同观点的胖友,欢迎一起探讨。谢谢。

TODO[0027][反思]:应用集合一致性哈希算法。

3. Eureka-Client 发起增量获取

《Eureka 源码解析 —— 应用实例注册发现(六)之全量获取》「2.4 发起获取注册信息」 里,调用 DiscoveryClient#getAndUpdateDelta(...) 方法,增量获取注册信息,并刷新本地缓存,实现代码如下:

 1: <