05、Eureka源码系列-服务发现

client抓取全量注册表

入口

客户端会在创建DiscoveryClient实例的时候,也就是DiscoveryClient的构造函数中,根据配置判断是否需要抓取注册表,默认是需要抓取。如果需要的话,第一次抓取采用全量抓取的方式,所谓全量抓取就是请求server的全量抓取接口,然后缓存在本地。调用链路:

  • com.netflix.discovery.DiscoveryClient#DiscoveryClient

    • com.netflix.discovery.DiscoveryClient#initScheduledTasks

      • com.netflix.discovery.DiscoveryClient.CacheRefreshThread#run

        • com.netflix.discovery.DiscoveryClient#refreshRegistry

          • com.netflix.discovery.DiscoveryClient#fetchRegistry

DiscoveryClient中的抓取注册表入口:

 

方法实现:

 

fetchRegistry方法有一个布尔参数,表示是否强制拉取全量注册表,在DiscoveryClient的构造函数中调用这个方法时传入的是false,表示不需要强制全量拉取。

1、 获取cllient本地的注册表缓存![ ][nbsp2];
2、 判断是否全量抓取注册表,如果是第一次拉取注册表的话,会走进这个分支;
3、 拉取全量注册表;
4、 否则拉取增量注册表;

我们接着看抓取全量注册表的逻辑,进入com.netflix.discovery.DiscoveryClient#getAndStoreFullRegistry方法:

 

getAndStoreFullRegistry方法就没什么逻辑了,就是向server端拉取全量注册表的接口发了个请求,接受到返回值后,缓存到client本地。

发送的请求url:localhost:8080/v2/apps,get请求

server端处理抓取全量注册表的请求

入口

服务端处理请求的方法是com.netflix.eureka.resources.ApplicationsResource#getContainers:

 

方法逻辑:

1、 不允许访问的话,直接返回;
2、 生成缓存的key;
3、 从缓存中获取返回值,根据参数处理返回的数据格式;

可以发现,server端没有提供直接拉取原始注册表的方法,client想要拉取注册表,必须经过缓存。

接下来就是eureka-server端的重头戏了,server端注册表缓存机制!

读取缓存

eureka-server的缓存机制分为两层,他俩的缓存键和值是一样的:

  • 只读缓存 readOnlyCacheMap
  • 读写缓存 readWriteCacheMap

 

我们还是从getContainers方法中对缓存的调用入口开始看,调用链路:

  • com.netflix.eureka.resources.ApplicationsResource#getContainers

    • com.netflix.eureka.registry.ResponseCache#get

      • com.netflix.eureka.registry.ResponseCacheImpl#get(com.netflix.eureka.registry.Key, boolean)

        • com.netflix.eureka.registry.ResponseCacheImpl#getValue

最后会调用到ResponseCacheImpl#getValue这个方法,这个方法内部就是获取缓存的逻辑了:

 

eureka-server的缓存机制由两部分缓存构成:

  • 只读缓存
  • 读写缓存

 

获取缓存的策略:

获取缓存的时候先从只读缓存中获取,如果只读缓存为空,再从读写缓存中获取,并且将读写缓存中的值更新到只读缓存中;如果读写缓存也是空的话,从原始注册表中获取,并写入读写缓存和只读缓存。

client每30s定时抓取增量注册表

eureka在设计上为了避免每次都进行全量抓取而引起的性能损耗,所以在第一次进行全量抓取后,后续都采用定时抓取增量注册表的方式。

入口

DiscoverClient的构造方法中,会执行initScheduledTasks方法,这个方法用来启动一些定时调度的线程池,实现client的定时调度,其中就有定时抓取增量注册表的方法逻辑,调用链路:

  • com.netflix.discovery.DiscoveryClient#initScheduledTasks

    • com.netflix.discovery.DiscoveryClient#refreshRegistry

      • com.netflix.discovery.DiscoveryClient#fetchRegistry

上文fetchRegistry方法中,如果client本地没有注册表,那么client会走抓取增量注册表的逻辑分支,进入getAndUpdateDelta方法:

 

拉取增量注册表的逻辑分为三步:

1、 从server拉取的增量注册表为null,则拉取全量注册表;
2、 否则从server获取来的增量注册表与本地注册表合并,合并后用全部application信息计算出一个hashCode;
3、 将刚刚在本地计算出的hashCode与server端返回的hashCode作对比,不相等的话说明出错了,这时候重新拉取全量注册表;

逻辑不复杂,但拉取增量注册表和利用hashCode做对比来增加可用性的思路,值得我们学习,接着我们看服务端是如何处理请求的

server端处理抓取增量注册表的请求

入口

server处理client的请求接口:com.netflix.eureka.resources.ApplicationsResource#getContainerDifferential。

这个接口的逻辑和处理全量注册表请求的几乎一模一样,最大的区别在于获取缓存的key不同:

 

种缓存

全量注册表缓存和增量注册表缓存都在同一个只读缓存和读写缓存中,依赖key中的一个字段来区分,读写缓存的种入口只有一个,就是ResponseCacheImpl的构造函数中:

 

接着我们进入计算缓存值的方法:

 

可以看到根据key的不同,走了不同的逻辑分支,最后调用了不同的生成缓存方法。全量缓存的生成比较简单,就是将server端所有的application都遍历一遍,取出应用信息并组装起来,我们这里就不赘述了,我们看一下增量缓存是怎么生成的:

 

重点在图中的红框,笔者发现增量缓存的数据,是从一个recentlyChangedQueue队列中遍历出来的,这个队列的声明:

 

调用点:

 

通过搜索调用点笔者发现:recentlyChangedQueue即最近更新队列会在实例状态有变更的时候,将实例信息加入队列中。客户端来获取增量缓存的时候,就返回这个队列中的最近有变更的数据。

那这个队列数据是怎么删除的呢,有一个定时任务:

 

定时任务中会将180s之前变更的数据清除掉,所有recentlyChangedQueue队列中会保留180s内变更的实例数据。

这个定时任务会在AbstractInstanceRegistry的构造方法中被启用,默认每30s调用一次:

 

缓存设置

ResponseCacheImpl的构造函数是一个重要的方法,这个方法中包含了:

  • 读写缓存的设置逻辑
  • 定时对比只读缓存和读写缓存,如果有不同,将读写缓存设置进只读缓存中

 

缓存失效

应用实例注册、下线、过期时,失效 readWriteCacheMap 。这里要注意,只会将读写缓存置失效,只读缓存没有主动失效,要等待定时任务将两个缓存同步。

为什么设计缓存

CAP 的选择上,Eureka 选择了 AP ,不同于 Zookeeper 选择了 CP 。

对象清单

DiscoveryClient#localRegionApps 客户端本地缓存的注册表对象

数据结构:AtomicReference

 

配置参数

前缀namespace,默认为eureka

是否进行服务注册

shouldFetchRegistry = true

是否禁用增量抓取功能

disableDelta = false

是否使用服务端的只读缓存

shouldUseReadOnlyResponseCache = true

读写缓存过期时间 (s)

responseCacheAutoExpirationInSeconds = 180

“最近更新队列”中的数据的保留时间 (ms)

retentionTimeInMSInDeltaQueue = 3 * 60 * 1000

“最近更新队列”定时清除的时间 (ms)

deltaRetentionTimerIntervalInMs = 30 * 1000