eureka 源码解析
项目中遇到eureka客户端注册在服务端上明明已经下线但是却在服务端列表依然存在,还有的是服务明明存在,客户端却请求失败的情况.本次从源码的角度来分析eureka的客户端和服务端之间的通信过程.
基本概念
- 服务端: 为eureka的服务端,存储着客户端的信息
- 客户端: 为eureka的客户端,也就是我们通常的微服务就相当于eureka的客户端.通常都是生产者和消费者共存.
## 客户端注册自身到服务端并且拉取服务的流程
DiscoveryClient 类为客户端的工作类,其中构造方法中
上图可以看到 配置了 eureka.client.fetch-registry=true 之后,就会进入fetchRegistry 方法,方法执行之后又会开启一个定时任务,我们先来看拉取方法
方法中的各种或判断,只要有一个为true,就会执行全量获取方法,否则就是增量获取.其中如果配置了 eureka.client.disable-delta=true就会永远都是全量获取.我们先看全量获取的逻辑
全量获取的逻辑其实就是从服务端获取服务之后存入本地,没什么好说的. 注意客户端存储服务的名称为 localRegionApps
之后就是执行定时器任务的逻辑
这是开启定时从eureka服务端获取数据的定时任务 eureka.client.registry-fetch-interval-seconds 控制定时器的频率,定时器开启了一个 CacheRefreshThread 的定时任务
定时任务中又调用了 fetchRegistry 方法.
定时任务还定义了一个心跳的方法
开启了一个定时任务频率由 eureka.instance.lease-renewal-interval-in-seconds 控制
方法的最后还注册了一个状态监听器,当状态发生变化的时候,重新向eureka服务端注册.
小总结,可以看到当eureka客户端在初次启动的时候就会向服务端拉取全量客户端实例,并且开启两个定时器,一个定时上报心跳,一个定时拉取客户端列表
增量获取 和全量获取
刚才我们看到了从客户端拉取服务的代码,分为全量获取和增量获取,全量获取就是单纯的把数据拷贝到本地的变量里.而增量获取却不一样.
合并的细节和计算一致性哈希的细节都不用看,知道个结果就好.一致性哈希的样子为 5_UP_4_DOWN 含义为5个服务在线4个服务下线
举个例子:
1. 服务端现在有 3个A 服务和两个B服务. 这时候一个 **消费者** 启动,找服务端拉取了一次全量的数据,将5个 **生产者** 实例存入本地这时候服务端和客户端的一致性哈希值都是 **5_UP**
2. A和B各自下线一个实例, 这时候服务端的一致性哈希值为 **3_UP_2_DOWN** ,增量更新数据为 两个下线的服务, 这时候客户端的拉取定时器生效,找服务端要增量数据,服务端便把这两个下线的实例给客户端,客户端拿到两个下线的实例后和本地的5个在线实例对比,同样生成 **3_UP_2_DOWN** 哈希值,更新成功.
3. 增量更新失败的情况. 如果AB下线之后, **消费者** 由于网络问题迟迟没有找服务端要增量数据,导致服务端的增量数据过期了,只剩下了一个服务下线的信息在服务端保存,这时候发送拉取数据的请求,从服务端获取了一个下线的实例.跟本地的 原本 5个在线实例合并之后算出来的哈希值为 **4_UP_1_DOWN** 和服务端的 **3_UP_2_DOWN** 不一致,于是就从新全量获取一次服务.
这个一致性哈希看似完美,其实存在ABA问题,这个后面再说
服务端相关业务流程
接收客户端注册
ApplicationResource 类中 addInstance 方法
可以看到,服务端收到注册请求的时候,就将服务存入一个 registry 和一个 recentlyChangeedQueue 中,我们来看看是什么类型
registry 是一个map嵌套格式, 第一层为实例的名称,第二层为实例的id
而recentlychangedQueue 则是一个 队列
接收心跳
InstanceResource 类 renewLease方法接收客户端心跳.
传递服务名称 和id 去更新心跳
也就是更新了一下最后更新时间
删除过期的客户端
在 AbstractInstanceRegistry 类 的 postInit方法中定义了定时删除客户端的定时器
eureka.server.eviction-interval-timer-in-ms 定义了定时器的扫描频率 默认60秒
就是判断当前时间是不是超过了上次更新时间加上 duration 由 eureka.instance.lease-expiration-duration-in-seconds 控制, 剩下一个是补偿算法算出的额外时间,不用管. 可以得出 服务端会把上次更新时间超过 设定值的服务踢出列表.
现在需要看一下什么情况下eureka会开启自我保护模式,直接执行清除方法
自我保护机制
设置 eureka.server.enable-self-preservation=false 关闭自我保护模式,则方法就会直接去扫描,否则会进行如下判断
如果开启自我保护模式,则会必须同时满足
numberOfRenewsPerMinThreshold>0 和 getNumOfRenewsInLastMin > numberOfRenewsPerMinThreshold
首先解释一下这几个参数
expectedNumberOfClientsSendingRenews eureka期待会发送心跳的客户端数量 ,注意会比实际数量多一个, eureka为了高可用.
serverConfig.getExpectedClientRenewalIntervalSeconds() 服务端期待客户端发送心跳的频率 eureka.server.expected-client-renewal-interval-seconds 设置
serverConfig.getRenewalPercentThreshold() 服务端的阈值因子 默认 0.85 eureka.server.renewal-percent-threshold 设置
所以 服务端期待收到的心跳数目 = (实际客户端个数+1) * (60/ 心跳频率) * 阈值因子.
如果客户端的心跳频率设置的小于服务端的期待心跳频率,则更容易让服务端进入剔除服务的方法.因为服务端只期待一分钟能收到5个心跳,但是客户端一分钟发了20个心跳,及时掉线了一个服务,还是可以满足服务端的期待值.只要满足了期待,就会进入清除服务的方法.
发送实例列表给客户端
全量获取
回顾一下客户端全量获取的过程,就是发送请求然后存入本地的变量中结束. 客户端发送的全量拉取数据请求由服务端的 ApplicationsResource 类的 getContainers 方法处理
其中 shouldUseReadOnlyResponseCache 为是否启用二级缓存开关,默认开启 由 eureka.server.use-read-only-response-cache 控制
其中只读map只是一个单纯的 ConcurrentMap. 而读写map则是 Guava 的 LoadingCache
其中一级缓存的失效时间默认是 180秒 由 eureka.server.response-cache-auto-expiration-in-seconds 设置. 当服务下线、过期、注册、状态变更,都会来清除此缓存中的数据.
真正获取的方法
增量获取
ApplicationsResource 的 getContainerDifferential 方法实现增量获取
我们可以看到 eureka 服务端对于增量获取的请求就是遍历 recentlyChangedQueue 来返回. 这个 recentlyChangedQueue 从字面意义上就可以看得出表示eureka记录客户端最近变化的记录.
那我们来看看这个队列的生命周期
在初始化的时候定义了一个定时器定时执行 getDeltaRetentionTask 方法, 定时器的频率由 eureka.server.delta-retention-timer-interval-in-ms 控制
定时器方法内部
注意,队列中只会存储上线下线更改状态才会来到 recentlyChangedQueue ,普通的心跳是不会更新这个时间. 超时阈值由 eureka.server.retention-time-in-m-s-in-delta-queue 控制默认180秒.
总结
在默认配置情况下,
- 客户端发起注册请求,将自身信息注册到 服务端, 服务端把客户端信息保存在 registry 双层map中 ,并且再放入一份到 recentlyChangedQueue 180秒过期队列中.
- 客户端发起一次全量拉取数据, 服务端把 registry中的数据返回给客户端. 客户端把数据存入 localRegionApps 中
- 客户端开启定时器,发送30秒一次的心跳给服务端,服务端接收心跳后更新对应实例的最近更新时间.服务端有一个60秒执行一次的定时清除过期服务的任务,如果关闭了自我保护模式,则会60秒删除一次上次心跳时间已经超过90秒的服务剔除.
- 客户端还开启了一个30秒一次的定时拉取服务端的变更数据的定时任务,服务端收到请求后,把自己180秒过期的队列数据给客户端.
一致性哈希引发的数据不一致性
场景1. recentlyChangedQueue
的过期时间变为1毫秒
- 开启 服务端
- 开启应用A 9001 端口
- 开启客户端,此时客户端已经全量获取到服务端数据 哈希code= 1_UP
- 关闭9001 端口,启动9002端口的应用A.
- 客户端定时获取服务端实例列表的定时器生效,从服务端的
recentlyChangedQueue
中获取数据,但是由于配置导致 队列中永远都是空的,获取下来的数据也为空,所以增量获取数据之后,本地的服务列表里的9001端口实例还是存在本地中,计算出来的哈希code 为 1_UP. 和服务端的状态相同.流程结束.
结果就是客户端里面永远都保存着 9001的数据
场景2
- 开启服务端
- 开启 应用 A,B 注册到服务端 2_UP
- 开启客户端, 拉取数据 2_UP
- A 下线, C 上线 并且recentlyChangedQueue 数据由于网络问题,客户端没有及时来同步,导致过期
- D 服务上线. 此时服务端总共上线的实例数量为 B C D 3_UP
- 客户端拉取定时服务列表,获得 队列中的 D 数据, A,B,D 3_UP, 哈希code相同,流程结束
默认情况下, 最近变化队列中保存的是180秒内的数据,客户端拉取一次的频率为30秒,可能导致上述bug发生的情况为,客户端前面5次请求都没有获取到数据,第六次的时候,服务端在前30秒内的变更失效,并且之后的变化也和30秒的增减总和不变. 小概率事件