eureka作为微服务中的注册中心,为微服务集群间各个服务进行调用提供寻址的功能,有了它集群间的服务只需要指定服务名称就可以了,无需再去关心服务具体部署的服务器IP,即可正常调用。下面来对其中我们开发中会接触的主要机制的实现原理进行剖析。一些具体细节这里不做详细的分析,只关注如下2个大方向的东西:1.注册相关的机制、2.客户端和服务端的启动流程。
Eureka服务端启动流程
首先需要说明的是eureka server(后面简称服务端)是在eureka client(后面简称客户端)的基础上进一步封装的一个东西;也就是说客户端有的东西服务端也有。服务端额外多的东西就是对注册表的处理部分。它的启动流程如下:
- 初始化环境配置;这个我们在日常开发中几乎都是使用的默认的;
- 读取服务端的配置信息,也就是读取eureka-server.properties配置文件;由于eureka中对这种配置类采用的是面向接口的方式,因此非常好扩展,在spring中是重新实现了这些配置类接口的。
- 构建应用管理器;读取eureka-client.properties配置文件,选择其中的部分配置,基于构造者模式创建服务实例交给应用管理器。
- 读取eureka-client.properties配置文件构建客户端信息;这里的操作和客户端的启动流程几乎就是一样的,因此这里就不做详细说明了。
- 创建注册表感知器registry,这个也可以称为注册表管理器。这里会维护注册表信息。
- 创建服务端集群节点信息管理器;也就是我们配置的集群地址信息,默认10分钟检查一次。
- 基于上面的信息创建服务端的上下文信息;这里在进行初始化的时候会对相关资源进行初始化;启动相关的定时。
- 从任意一个其他的服务端节点上拉取注册表信息;注意这里只有将fetchRegistry配置为true才是有效的(因为这里最终是从服务端中的客户端中的注册信息里面拿到的,这也是为什么网上很多说是从任意一个其他节点去拉取,因为客户端就是选择的一个去拉取的),同时将registry-sync-retries配置的值大于0(spring中默认值是0,也就是在spring-cloud中是不会执行这一步的)
- 启动注册表感知器registry的定时;这个定时主要就是检查注册表中是否有过期的注册信息。
- 最后进行监听器的绑定。
相关的流程图如下:
关于集群中 fetchRegistry 的配置
fetchRegistry在部署eureka server集群的时候,该如何配置?为什么spring官方重新封装的时候直接默认的让此参数对eureka server来说几乎无效?
因为如果将fetchRegistry配置为true,那么就会启动eureka client中使用到的很多线程资源,而这些东西对于eureka server来说是没有用的;配置fetchRegistry为true唯一的作用就是在启动时主动的拉取其他节点的注册表信息,之后就没有什么用了;eureka server集群间的注册表同步是通过主动向其他节点转发相关的注册表事件。
因此非常的鸡肋,如果说经常需要新增节点可能还需要,但实际上扩容节点的操作很少进行。因此在eureka server 集群中可以直接将fetchRegistry设置为false即可。
Eureka客户端启动流程
eureka客户端的启动主要就是几个定时和之后进行注册表维护的网络请求资源初始化。
- 构建应用管理器;读取eureka-client.properties配置文件,选择其中的部分配置,基于构造者模式创建服务实例交给应用管理器。
- 读取eureka-client.properties配置信息构建客户端的配置信息;
- 根据上面的配置信息和应用管理器构建客户端;
- 创建心跳、缓存等需要的线程池;
- 创建网络通信组件;后面发送注册信息、心跳信息这些请求都是通过它来处理的;
- 判断是否需要拉取注册表信息,若是则会全量拉取一次注册表信息;
- 启动相关的定时任务:注册表更新任务(默认30s执行一次)、心跳定时任务(默认30s执行一次)、创建服务状态更新定时任务(默认30s执行一次,这个就是留个我们自定义服务上下线状态的判断逻辑的);
- 启动服务状态更新定时任务(第一次延迟40s执行);这里面就是向服务端发送注册信息的实现。
- 最后进行监听器的绑定。
相关的流程图如下:
Eureka注册表的原理
eureka的注册表中保存中服务的注册信息,下面我们通过如下几个点来对其原理进行简析。
注册表抓取和缓存机制
其基本流程图如下:
注册表的数据结构和缓存机制
eureka server中对注册表的信息进行多重缓存,分为:
- 只读缓存(ConcurrentMap):会有定时任务默认每隔30s主动的去和读写缓存里面的信息同步一次;
- 读写缓存(guava的LoadingCache):在创建LoadingCache的时候默认设置的过期时间是180s;
- 注册表:这个就是实时的本地注册信息,每次客户端的注册信息更新后,都会实时的保存在这里;同时在更新它的时候会将读写缓存中的值设置为失效状态。
注册表信息读取流程
注册表的拉取分为全量和增量;在初次拉取时使用的是全量,后面使用的都是增量拉取的。
全量拉取流程:
- 服务端收到客户端的请求后,会直接从只读缓存里面取值,如果有就返回,否则进行下一步;
- 只读缓存里面没有时,会从读写缓存里面取值,如果有就返回,同时将其设置达到只读缓存里面;否则进行下一步;
- 读写缓存里面没有时,会触发LoadingCache的load方法,这里面会从本地注册表中取值返回。
增量拉取流程:
- 服务端收到客户端的请求后,会直接从只读缓存里面取增量信息,如果有就返回,否则进行下一步;
- 只读缓存里面没有时,会从读写缓存里面取增量信息,如果有就返回,同时将其设置达到只读缓存里面;否则进行下一步;
- 读写缓存里面没有时,会触发LoadingCache的load方法,这里面会增量队列中获取变化的信息然后返回;
服务端集群间的注册信息如何同步的
要回答这个问题,我们就需要先了解客户端发送注册信息和心跳信息的整个流程,看了下面的注册和心跳流程这个问题也就可以解释了。
注册的流程来说明:
- 客户端在启动的最后一步启动服务状态更新定时任务时,里面的定时任务就会向服务端发送注册信息;
- 客户端会选从配置的服务服务注册地址中选择第一个进行尝试,如果成功后面都会用这个,直到失败才会切换到下一个;
- 服务端收到注册请求后,更新本地注册表中注册信息,将读写缓存中的缓存设置为失效状态;同时将注册表的变更信息保存到最近变更队列中;
- 将注册请求信息转发给eureka server集群中的其他节点。
心跳的请求也是在服务端自己处理完成后,会自动将这个请求转发给集群中的其他节点。心跳的操作就是更新注册信息中的租约时间,这里就不详细说明了。
注意这种通知集群中其他节点的操作在失败后会不断的重试,同时正式由于有这个操作,因此服务端的fetchRegistry配置为false,集群间的注册信息依然可以正常同步的原因。
客户端的注册信息什么时候会被摘除
客户端的注册信息被摘除主要是这2种情况:1.客户端服务主动下线;2.服务异常。
客户端服务主动下线
客户端服务下线:主动取消注册信息,这种服务端直接接收请求然后删除即可;其流程图如下:
服务异常
客户端异常:没有发送取消请求或者是服务端没有正常接收和处理取消请求的情况下,此时就需要服务端自己定制一套注册信息过期机制,这也就是发送心跳的作用。
服务端中注册表信息过期检查的定时任务默认每隔60s检查一次,其大致流程如下:
- 判断的过期的依据是:当前时间戳 > (上一次发送租约的时间戳 + 过期时间(默认90s) + 补充时间(就是距离上一次执行任务的时间超过定时任务配置的60s执行一次的周期时间));但是由于在设置上一次发送租约的时间戳时候额外加上了一个过期时间;因此最终注册表的过期时间就至少是180s。
- 选择15%的过期注册信息,然后调用取消操作来删除注册信息;同时会通知集群中其他的节点。
Eureka源码阅读建议
spring-cloud-eureka中的server和client是对netflix的eureka进行了封装,加了一些注解来对spring boot进行支持。因此在阅读eureka源码时,应该先从netflix eureka开始看起,之后再去查看spring cloud封装的eureka的源码就会轻松许多。eureka源码地址:https://github.com/Netflix/eureka 、spring-cloud-eureka源码地址:https://github.com/spring-cloud/spring-cloud-netflix 。
建议不要直接从github仓库里面去拉取,直接去下载对应版本的压缩包即可。
网上对eureka源码分析的文章有很多,这里推荐2篇写得非常不错的博文: