eureka 源码解析

eureka 源码解析

项目中遇到eureka客户端注册在服务端上明明已经下线但是却在服务端列表依然存在,还有的是服务明明存在,客户端却请求失败的情况.本次从源码的角度来分析eureka的客户端和服务端之间的通信过程.

基本概念

  1. 服务端: 为eureka的服务端,存储着客户端的信息
  2. 客户端: 为eureka的客户端,也就是我们通常的微服务就相当于eureka的客户端.通常都是生产者和消费者共存.

## 客户端注册自身到服务端并且拉取服务的流程

DiscoveryClient 类为客户端的工作类,其中构造方法中

在这里插入图片描述

上图可以看到 配置了 eureka.client.fetch-registry=true 之后,就会进入fetchRegistry 方法,方法执行之后又会开启一个定时任务,我们先来看拉取方法
在这里插入图片描述

方法中的各种或判断,只要有一个为true,就会执行全量获取方法,否则就是增量获取.其中如果配置了 eureka.client.disable-delta=true就会永远都是全量获取.我们先看全量获取的逻辑
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9APC5L4P-1609403162853)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229213042905.png)]

全量获取的逻辑其实就是从服务端获取服务之后存入本地,没什么好说的. 注意客户端存储服务的名称为 localRegionApps

之后就是执行定时器任务的逻辑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-22CEJvUa-1609403162854)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229213659453.png)]

这是开启定时从eureka服务端获取数据的定时任务 eureka.client.registry-fetch-interval-seconds 控制定时器的频率,定时器开启了一个 CacheRefreshThread 的定时任务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C8ViP3uc-1609403162856)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229213854868.png)]

定时任务中又调用了 fetchRegistry 方法.

定时任务还定义了一个心跳的方法

开启了一个定时任务频率由 eureka.instance.lease-renewal-interval-in-seconds 控制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UJJoD4uY-1609403162857)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229214114627.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M5BYKH5R-1609403162857)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229214233670.png)]

方法的最后还注册了一个状态监听器,当状态发生变化的时候,重新向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 方法

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6UwBybB0-1609403162860)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229223332916.png)]

可以看到,服务端收到注册请求的时候,就将服务存入一个 registry 和一个 recentlyChangeedQueue 中,我们来看看是什么类型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Qe1yqSu-1609403162861)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229223505333.png)]

registry 是一个map嵌套格式, 第一层为实例的名称,第二层为实例的id

而recentlychangedQueue 则是一个 队列

接收心跳

InstanceResource 类 renewLease方法接收客户端心跳.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ngu5EJbY-1609403162861)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229224002855.png)]

传递服务名称 和id 去更新心跳

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gV0dYf6j-1609403162862)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229224400254.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XS6E7jbA-1609403162862)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229224434506.png)]

也就是更新了一下最后更新时间

删除过期的客户端

在 AbstractInstanceRegistry 类 的 postInit方法中定义了定时删除客户端的定时器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RuueQntw-1609403162863)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229231749385.png)]

eureka.server.eviction-interval-timer-in-ms 定义了定时器的扫描频率 默认60秒

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z07vwvhG-1609403162864)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229232526641.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-29C1e1Xg-1609403162865)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229233327979.png)]

就是判断当前时间是不是超过了上次更新时间加上 duration 由 eureka.instance.lease-expiration-duration-in-seconds 控制, 剩下一个是补偿算法算出的额外时间,不用管. 可以得出 服务端会把上次更新时间超过 设定值的服务踢出列表.

现在需要看一下什么情况下eureka会开启自我保护模式,直接执行清除方法

自我保护机制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bsa0xK4F-1609403162865)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201229234039554.png)]

设置 eureka.server.enable-self-preservation=false 关闭自我保护模式,则方法就会直接去扫描,否则会进行如下判断

如果开启自我保护模式,则会必须同时满足

numberOfRenewsPerMinThreshold>0getNumOfRenewsInLastMin > numberOfRenewsPerMinThreshold

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FZOLYgBO-1609403162866)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201230225408298.png)]

首先解释一下这几个参数

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个心跳,及时掉线了一个服务,还是可以满足服务端的期待值.只要满足了期待,就会进入清除服务的方法.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-spZWOvuQ-1609403162866)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201230235006102.png)]

发送实例列表给客户端

全量获取

回顾一下客户端全量获取的过程,就是发送请求然后存入本地的变量中结束. 客户端发送的全量拉取数据请求由服务端的 ApplicationsResource 类的 getContainers 方法处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VsgYc7N2-1609403162867)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201230233040139.png)]

在这里插入图片描述

其中 shouldUseReadOnlyResponseCache 为是否启用二级缓存开关,默认开启 由 eureka.server.use-read-only-response-cache 控制

在这里插入图片描述

其中只读map只是一个单纯的 ConcurrentMap. 而读写map则是 Guava 的 LoadingCache

在这里插入图片描述

其中一级缓存的失效时间默认是 180秒 由 eureka.server.response-cache-auto-expiration-in-seconds 设置. 当服务下线、过期、注册、状态变更,都会来清除此缓存中的数据.

真正获取的方法
在这里插入图片描述

增量获取

ApplicationsResourcegetContainerDifferential 方法实现增量获取

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们可以看到 eureka 服务端对于增量获取的请求就是遍历 recentlyChangedQueue 来返回. 这个 recentlyChangedQueue 从字面意义上就可以看得出表示eureka记录客户端最近变化的记录.

那我们来看看这个队列的生命周期

在这里插入图片描述

在初始化的时候定义了一个定时器定时执行 getDeltaRetentionTask 方法, 定时器的频率由 eureka.server.delta-retention-timer-interval-in-ms 控制

定时器方法内部

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DRj6dgkX-1609403162873)(/Users/tyrion/Library/Application Support/typora-user-images/image-20201231150322125.png)]

注意,队列中只会存储上线下线更改状态才会来到 recentlyChangedQueue ,普通的心跳是不会更新这个时间. 超时阈值由 eureka.server.retention-time-in-m-s-in-delta-queue 控制默认180秒.

总结

在默认配置情况下,

  1. 客户端发起注册请求,将自身信息注册到 服务端, 服务端把客户端信息保存在 registry 双层map中 ,并且再放入一份到 recentlyChangedQueue 180秒过期队列中.
  2. 客户端发起一次全量拉取数据, 服务端把 registry中的数据返回给客户端. 客户端把数据存入 localRegionApps
  3. 客户端开启定时器,发送30秒一次的心跳给服务端,服务端接收心跳后更新对应实例的最近更新时间.服务端有一个60秒执行一次的定时清除过期服务的任务,如果关闭了自我保护模式,则会60秒删除一次上次心跳时间已经超过90秒的服务剔除.
  4. 客户端还开启了一个30秒一次的定时拉取服务端的变更数据的定时任务,服务端收到请求后,把自己180秒过期的队列数据给客户端.

一致性哈希引发的数据不一致性

场景1. recentlyChangedQueue 的过期时间变为1毫秒
  1. 开启 服务端
  2. 开启应用A 9001 端口
  3. 开启客户端,此时客户端已经全量获取到服务端数据 哈希code= 1_UP
  4. 关闭9001 端口,启动9002端口的应用A.
  5. 客户端定时获取服务端实例列表的定时器生效,从服务端的 recentlyChangedQueue 中获取数据,但是由于配置导致 队列中永远都是空的,获取下来的数据也为空,所以增量获取数据之后,本地的服务列表里的9001端口实例还是存在本地中,计算出来的哈希code 为 1_UP. 和服务端的状态相同.流程结束.
    结果就是客户端里面永远都保存着 9001的数据
场景2
  1. 开启服务端
  2. 开启 应用 A,B 注册到服务端 2_UP
  3. 开启客户端, 拉取数据 2_UP
  4. A 下线, C 上线 并且recentlyChangedQueue 数据由于网络问题,客户端没有及时来同步,导致过期
  5. D 服务上线. 此时服务端总共上线的实例数量为 B C D 3_UP
  6. 客户端拉取定时服务列表,获得 队列中的 D 数据, A,B,D 3_UP, 哈希code相同,流程结束

默认情况下, 最近变化队列中保存的是180秒内的数据,客户端拉取一次的频率为30秒,可能导致上述bug发生的情况为,客户端前面5次请求都没有获取到数据,第六次的时候,服务端在前30秒内的变更失效,并且之后的变化也和30秒的增减总和不变. 小概率事件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值