
SpringCloud系列—Spring Cloud 源码分析之Eureka
作者 | 宇木木兮
来源 |今日头条
学习目标
- Eureka主流程推导
Eureka源码分析
第1章 核心流程
1.1 Eureka做了什么事
首先我们都明白,Eureka是用在做服务注册的,而注册中心要实现什么功能呢?这个必须明确了。
- 既然是注册中心,那首先要能保存服务的ip、port等信息吧,这是Eureka-server必须提供的基础功能。
- 当我注册上来之后,还要提供一些动态感知服务上下线的功能吧,如果一个服务上下线,Eureka-server都不知道,那这个玩意儿就一文不值了。
- 当Eureka-server端感知到服务的变化之后总得通知消费端吧,这里就牵扯到时server端主动通知client端呢还是client端自己去拉取信息呢?这个现在不知道,等会去看源码验证。
- OK,上面的功能都实现了,Eureka基本合格了,那还有一步,既然服务的ip和端口都在Eureka-server上面,那我消费端调用服务端的时候,通信是用的OpenFeign,那OpenFeign是怎么知道调用哪个服务的?之前都是写死在application.properties里面的<servicename>.ribbon.listOfServers中,现在Eureka怎么自动写进去的呢?
- 上面4个功能基本上完成了注册中心该有的功能,那这个时候我们再来思考一下,注册中心用于微服务项目中,那注册中心也作为一个服务,它也需要做集群的,这个时候我们就要想一下,做集群怎么保证数据一致性,该基于什么理论?
上面是Eureka要实现的最核心的功能,那这些功能提供出来了,我项目怎么去调用呢?不能直接去调API吧,这多麻烦,还得去学一遍Eureka的api,完犊子了。
这个时候我们就会联想到SpringBoot的自动装配和Starter组件。这两个玩意儿帮我们完成了核心bean的自动注入,底层可以直接拿到bean,然后在Starter组件中应该自动帮我们调用了API的;OK,那回过头来我发现,我的Eureka-client就是一个starter组件,嘿嘿,有点东西了。这个client端核心逻辑肯定是帮我们封装了各种bean,然后帮我们调用了核心api了。
这里针对Eureka的核心功能我们在做一个更简便的总结:
- 实现注册,并存在内存中
- 动态感知服务的健康状态
- 服务的发现,及动态感知服务的变化
1.2 核心流程推导
明确了核心功能,以及如何调用的,接下来我们来大胆地推导一下核心流程图:
大体流程推导出来之后,我们再来通过源码做一个验证。
第2章 源码分析
2.1 服务注册的入口
服务注册是在spring boot应用启动的时候发起的。具体的执行路径我们暂且不看,先回顾一下前面咱们讲过的知识。
我们说spring cloud是一个生态,它提供了一套标准,这套标准可以通过不同的组件来实现,其中就包含服务注册/发现、熔断、负载均衡等,在spring-cloud-common这个包中,
org.springframework.cloud.client.serviceregistry 路径下,可以看到一个服务注册的接口定 义 ServiceRegistry 。它就是定义了spring cloud中服务注册的一个接口。
我们看一下它的类关系图,这个接口有一个唯一的实现 EurekaServiceRegistry 。表示采用的是Eureka Server作为服务注册中心。
2.1.1 注册的时机
服务注册的发起,我们可以猜测一下应该是什么时候完成?大家自要想想其实应该不难猜测到,服务的注册取决于服务是否已经启动好了。而在spring boot中,会等到spring 容器启动并且所有的配置都完成之后来进行注册。而这个动作在spring boot的启动方法中的refreshContext中完成。
我们观察一下finishRefresh这个方法,从名字上可以看到它是用来体现完成刷新的操作,也就是刷新完成之后要做的后置的操作。它主要做几个事情
- 清空缓存
- 初始化一个LifecycleProcessor,在Spring启动的时候启动bean,在spring结束的时候销毁bean
- 调用LifecycleProcessor的onRefresh方法,启动实现了Lifecycle接口的bean
- 发布ContextRefreshedEvent
- 注册Bean,通过JMX进行监控和管理
在这个方法中,我们重点关注 getLifecycleProcessor().onRefresh() ,它是调用生命周期处理器的onrefresh方法,找到SmartLifecycle接口的所有实现类并调用start方法。
2.1.2 SmartLifeCycle
我拓展一下SmartLifeCycle这块的知识, SmartLifeCycle是一个接口,当Spring容器加载完所有的Bean并且初始化之后,会继续回调实现了SmartLifeCycle接口的类中对应的方法,比如(start)。
实际上我们自己也可以拓展,比如在springboot工程的main方法同级目录下,写一个测试类,实现SmartLifeCycle接口,并且通过 @Service 声明为一个bean,因为要被spring去加载,首先得是bean。
接着,我们启动spring boot应用后,可以看到控制台输出了 start 字符串。
我们在DefaultLifecycleProcessor.startBeans方法上加一个debug,可以很明显的看到我们自己定义的TestSmartLifeCycle被扫描到了,并且最后会调用该bean的start方法。在startBeans方法中,我们可以看到它首先会获得所有实现了SmartLifeCycle的Bean,然后会循环调用实现了SmartLifeCycle的bean的start方法,代码如下。
2.1.3 doStart
此时,bean.start(),调用的可能是
EurekaAutoServiceRegistration中的start方法,因为很显然,它实现了SmartLifeCycle接口。
在start方法中,我们可以看到
this.serviceRegistry.register 这个方法,它实际上就是发起服务注册的机制。
此时this.serviceRegistry的实例,应该是 EurekaServiceRegistry , 原因是
EurekaAutoServiceRegistration的构造方法中,会有一个赋值操作,而这个构造方法是在EurekaClientAutoConfiguration 这个自动装配类中被装配和初始化的,代码如下。
2.2 服务的注册流程
接下来我们分析服务注册的流程
this.serviceRegistry.register(this.registration); 方法最终会调用
EurekaServiceRegistry 类中的 register 方法来实现服务注册
2.2.1 register
从上述代码来看,注册方法中并没有真正调用Eureka的方法去执行注册,而是仅仅设置了一个状态以及设置健康检查处理器。我们继续看一下
reg.getApplicationInfoManager().setInstanceStatus方法。
在这个方法中,它会通过监听器来发布一个状态变更事件。ok,此时listener的实例是StatusChangeListener ,也就是调用 StatusChangeListener 的notify方法。这个事件是触发一个服务状态变更,应该是有地方会监听这个事件,然后基于这个事件进行注册。
这个时候我们以为找到了方向,然后点击进去一看,发现它是一个接口。而且我们发现它是静态的内部接口,还无法直接看到它的实现类。
依我多年源码阅读经验,于是又往回找,因为我基本上能猜测到一定是在某个地方做了初始化的工作,于是,我想找到
EurekaServiceRegistry.register方法中的 reg.getApplicationInfoManager 这个实例是什么,而且我们发现ApplicationInfoManager是来自于EurekaRegistration这个类中的属性。而EurekaRegistration又是在EurekaAutoServiceRegistration这个类中实例化的。那我在想,是不是在自动装配中做了什么东西。于是找到EurekaClientAutoConfiguration这个类,果然看到了Bean的一些自动装配,其中包含 EurekaClient 、 ApplicationInfoMangager 、 EurekaRegistration 等。
2.2.2 EurekaClientConfiguration
不难发现,我们似乎看到了一个很重要的Bean在启动的时候做了自动装配,也就是CloudEurekaClient 。从名字上来看,我可以很容易的识别并猜测出它是Eureka客户端的一个工具类,用来实现和服务端的通信以及处理。这个是很多源码一贯的套路,要么在构造方法里面去做很多的初始化和一些后台执行的程序操作,要么就是通过异步事件的方式来处理。接着,我们看一下CloudEurekaClient的初始化过程,它的构造方法中会通过 super 调用父类的构造方法。也就是DiscoveryClient的构造。
2.2.3 CloudEurekaClient
super(applicationInfoManager, config, args);调用父类的构造方法,而CloudEurekaClient的父类是DiscoveryClient.
2.2.4 DiscoveryClient构造
我们可以看到在最终的DiscoveryClient构造方法中,有非常长的代码。其实很多代码可以不需要关心,大部分都是一些初始化工作,比如初始化了几个定时任务
- scheduler
- heartbeatExecutor 心跳定时任务
- cacheRefreshExecutor 定时去同步服务端的实例列表
2.2.5 initScheduledTasks
initScheduledTasks 去启动一个定时任务。
- 如果配置了开启从注册中心刷新服务列表,则会开启cacheRefreshExecutor这个定时任务
- 如果开启了服务注册到Eureka,则通过需要做几个事情.
- 建立心跳检测机制
通过内部类来实例化StatusChangeListener 实例状态监控接口,这个就是前面我们在分析启动过程中所看到的,调用notify的方法,实际上会在这里体现。
2.2.6 onDemandUpdate
这个方法的主要作用是根据实例数据是否发生变化,来触发服务注册中心的数据。
2.2.7 run
run方法实际上和前面自动装配所执行的服务注册方法是一样的,也就是调用 register 方法进行服务注册,并且在finally中,每30s会定时执行一下当前的run 方法进行检查。
2.2.8 register
最终,我们终于找到服务注册的入口了,
eurekaTransport.registrationClient.register 最终调用的是 AbstractJerseyEurekaHttpClient#register(...)`, 当然大家如果自己去看代码,就会发现去调用之前有很多绕来绕去的代码,比如工厂模式、装饰器模式等。
很显然,这里是发起了一次http请求,访问Eureka-Server的apps/${APP_NAME}接口,将当前服务实例的信息发送到Eureka Server进行保存。
至此,我们基本上已经知道Spring Cloud Eureka 是如何在启动的时候把服务信息注册到Eureka Server上的了。
但是,似乎最开始的问题还没有解决,也就是Spring Boot应用在启动时,会调用start方法,最终调用
StatusChangeListener.notify 去更新服务的一个状态,并没有直接调用register方法注册。所以我们继续去看一下 statusChangeListener.notify 方法。
2.2.9 服务总结
至此,我们知道Eureka Client发起服务注册时,有两个地方会执行服务注册的任务
- 在Spring Boot启动时,由于自动装配机制将CloudEurekaClient注入到了容器,并且执行了构造方法,而在构造方法中有一个定时任务每40s会执行一次判断,判断实例信息是否发生了变化,如果是则会发起服务注册的流程
- 在Spring Boot启动时,通过refresh方法,最终调用StatusChangeListener.notify进行服务状态变更的监听,而这个监听的方法受到事件之后会去执行服务注册。
2.3 Server端逻辑
在没分析源码实现之前,我们一定知道它肯定对请求过来的服务实例数据进行了存储。那么我们去Eureka Server端看一下处理流程。
请求入口在:
com.netflix.eureka.resources.ApplicationResource.addInstance() 。
大家可以发现,这里所提供的REST服务,采用的是jersey来实现的。Jersey是基于JAX-RS标准,提供REST的实现的支持,这里就不展开分析了。
2.3.1 addInstance()
当EurekaClient调用register方法发起注册时,会调用
ApplicationResource.addInstance方法。
服务注册就是发送一个POST请求带上当前实例信息到类 ApplicationResource 的 addInstance方法进行服务注册。
2.3.2 register
我们先来看
PeerAwareInstanceRegistryImpl的类关系图,从类关系图可以看出,PeerAwareInstanceRegistry的最顶层接口为LeaseManager与LookupService,
- 其中LookupService定义了最基本的发现示例的行为
- LeaseManager定义了处理客户端注册,续约,注销等操作
在 addInstance 方法中,最终调用的是
PeerAwareInstanceRegistryImpl.register 方法。
- leaseDuration 表示租约过期时间,默认是90s,也就是当服务端超过90s没有收到客户端的心跳,则主动剔除该节点
- 调用super.register发起节点注册
- 将信息复制到Eureka Server集群中的其他机器上,同步的实现也很简单,就是获得集群中的所有节点,然后逐个发起注册
2.3.3 AbstractInstanceRegistry.register
简单来说,Eureka-Server的服务注册,实际上是将客户端传递过来的实例数据保存到Eureka-Server中的ConcurrentHashMap中。
2.3.4 小结
至此,我们就把服务注册在客户端和服务端的处理过程做了一个详细的分析,实际上在Eureka Server端,会把客户端的地址信息保存到ConcurrentHashMap中存储。并且服务提供者和注册中心之间,会建立一个心跳检测机制。用于监控服务提供者的健康状态。
2.4 Eureka 的多级缓存设计
Eureka Server存在三个变量:(registry、readWriteCacheMap、readOnlyCacheMap)保存服务注册信息,默认情况下定时任务每30s将readWriteCacheMap同步至readOnlyCacheMap,每60s清理超过90s未续约的节点,Eureka Client每30s从readOnlyCacheMap更新服务注册信息,而客户端服务的注册则从registry更新服务注册信息。
2.4.1 多级缓存的意义
这里为什么要设计多级缓存呢?原因很简单,就是当存在大规模的服务注册和更新时,如果只是修改一个ConcurrentHashMap数据,那么势必因为锁的存在导致竞争,影响性能。
而Eureka又是AP模型,只需要满足最终可用就行。所以它在这里用到多级缓存来实现读写分离。注册方法写的时候直接写内存注册表,写完表之后主动失效读写缓存。
获取注册信息接口先从只读缓存取,只读缓存没有再去读写缓存取,读写缓存没有再去内存注册表里取(不只是取,此处较复杂)。并且,读写缓存会更新回写只读缓存
- responseCacheUpdateIntervalMs : readOnlyCacheMap 缓存更新的定时器时间间隔,默认为30秒
- responseCacheAutoExpirationInSeconds : readWriteCacheMap 缓存过期时间,默认为 180 秒。
2.4.2 服务注册的缓存失效
在AbstractInstanceRegistry.register方法的最后,会调用invalidateCache(registrant.getAppName(), registrant.getVIPAddress(),registrant.getSecureVipAddress()); 方法,使得读写缓存失效。
2.4.3 定时同步缓存
ResponseCacheImpl的构造方法中,会启动一个定时任务,这个任务会定时检查写缓存中的数据变化,进行更新和同步。
2.5 服务续约
所谓的服务续约,其实就是一种心跳检查机制。客户端会定期发送心跳来续约。那么简单给大家看一下代码的实现
2.5.1 initScheduledTasks
客户端会在
DiscoveryClient.initScheduledTasks 中,创建一个心跳检测的定时任务
2.5.2 HeartbeatThread
然后这个定时任务中,会执行一个 HearbeatThread 的线程,这个线程会定时调用renew()来做续约。
2.5.3 服务端收到心跳请求的处理
在ApplicationResource.getInstanceInfo这个接口中,会返回一个InstanceResource的实例,在该实例下,定义了一个statusUpdate的接口来更新状态
2.5.4 InstanceResource.statusUpdate()
在该方法中,我们重点关注 registry.statusUpdate 这个方法,它会调用
AbstractInstanceRegistry.statusUpdate来更新指定服务提供者在服务端存储的信息中的变化。
2.5.5 AbstractInstanceRegistry.statusUpdate
在这个方法中,会拿到应用对应的实例列表,然后调用Lease.renew()去进行心跳续约。
至此,心跳续约功能就分析完成了。
2.6 服务发现
我们继续来研究服务的发现过程,就是客户端需要能够满足两个功能
在启动的时候获取指定服务提供者的地址列表
Eureka server端地址发生变化时,需要动态感知
2.6.1 DiscoveryClient构造时查询
构造方法中,如果当前的客户端默认开启了fetchRegistry,则会从eureka-server中拉取数据。
2.6.2 fetchRegistry
2.6.3 定时刷新本地地址列表
任务每隔30s更新一次
在DiscoveryClient构造的时候,会初始化一些任务,这个在前面咱们分析过了。其中有一个任务动态更新本地服务地址列表,叫 cacheRefreshTask 。
这个任务最终执行的是CacheRefreshThread这个线程。它是一个周期性执行的任务,具体我们来看一下。
2.6.4 TimedSupervisorTask
从整体上看,TimedSupervisorTask是固定间隔的周期性任务,一旦遇到超时就会将下一个周期的间隔时间调大,如果连续超时,那么每次间隔时间都会增大一倍,一直到达外部参数设定的上限为止,一旦新任务不再超时,间隔时间又会自动恢复为初始值。这种设计还是值得学习的。
2.6.5 refreshRegistry
这段代码主要两个逻辑
- 判断remoteRegions是否发生了变化
- 调用fetchRegistry获取本地服务地址缓存
2.6.6 fetchRegistry
2.6.7 getAndStoreFullRegistry
从eureka server端获取服务注册中心的地址信息,然后更新并设置到本地缓存 localRegionApps 。
2.6.8 服务端查询服务地址流程
前面我们知道,客户端发起服务地址的查询有两种,一种是全量、另一种是增量。对于全量查询的请求,会调用Eureka-server的ApplicationsResource的getContainers方法。
而对于增量请求,会调用
ApplicationsResource.getContainerDifferential。
2.6.9 ApplicationsResource.getContainers
接收客户端发送的获取全量注册信息请求。
2.6.10 responseCache.getGZIP
从缓存中读取数据。
