Spring-Cloud-Eureka 源码解析
小弟有一个开源项目,希望大家可以多多一键三连,谢谢大家
后续的源码解析也都会进行同步更新上去
核心流程
eureka server的启动,相当于是注册中心的启动 -> 启动的过程搞清楚,初始化了哪些东西
eureka client的启动,相当于是服务的启动,初始化了哪些东西
eureka运行的核心的流程,eureka client往eureka server注册的过程,服务注册;服务发现,eureka client从eureka server获取注册表的过程;服务心跳,eureka client定时往eureka server发送续约通知(心跳);服务实例摘除;通信,限流,自我保护,server集群
eureka server怎么启动的?
eureka server启动,其本身是一个web服务,会有web.xml文件可以使用servlet容器进行启动,根据web.xml里面的配置filter进行监控数据与EurekaBootStrap类的启动。而在eureka集群模式中启动时,eureka会将自己也当作为eureka-client进行注册到配置的eureka-server服务节点上去,通过client注册到server端的方式去同步节点注册表等数据同步。而eureka整体都是通过jersey矿建进行http rest接口请求来进行通信。而eureka-server在启动的时候会将 eureka-resources里面所有jsp页面和静态资源进行打包到eureka-server包里面,所以听过外部访问接口就可以看到eureka-server服务控制台。
web.xml文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- eureka server 就是一个web应用,整体会打成一个war包 -->
<!-- eureka server 初始化 -->
<listener>
<listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
</listener>
<!-- eureka server 状态信息拦截器 -->
<filter>
<filter-name>statusFilter</filter-name>
<filter-class>com.netflix.eureka.StatusFilter</filter-class>
</filter>
<!-- eureka server 请求授权认证处理拦截器 -->
<filter>
<filter-name>requestAuthFilter</filter-name>
<filter-class>com.netflix.eureka.ServerRequestAuthFilter</filter-class>
</filter>
<!-- eureka server 限流相关的拦截器 -->
<filter>
<filter-name>rateLimitingFilter</filter-name>
<filter-class>com.netflix.eureka.RateLimitingFilter</filter-class>
</filter>
<!-- eureka server 编码压缩拦截器 -->
<filter>
<filter-name>gzipEncodingEnforcingFilter</filter-name>
<filter-class>com.netflix.eureka.GzipEncodingEnforcingFilter</filter-class>
</filter>
<!-- eureka server 整体核心拦截器,拦截所有请求 -->
<filter>
<filter-name>jersey</filter-name>
<filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class>
<init-param>
<param-name>com.sun.jersey.config.property.WebPageContentRegex</param-name>
<param-value>/(flex|images|js|css|jsp)/.*</param-value>
</init-param>
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>com.sun.jersey;com.netflix</param-value>
</init-param>
<!-- GZIP content encoding/decoding -->
<init-param>
<param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
<param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
</init-param>
<init-param>
<param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
<param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>statusFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>requestAuthFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Uncomment this to enable rate limiter filter.
<filter-mapping>
<filter-name>rateLimitingFilter</filter-name>
<url-pattern>/v2/apps</url-pattern>
<url-pattern>/v2/apps/*</url-pattern>
</filter-mapping>
-->
<filter-mapping>
<filter-name>gzipEncodingEnforcingFilter</filter-name>
<url-pattern>/v2/apps</url-pattern>
<url-pattern>/v2/apps/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>jersey</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>jsp/status.jsp</welcome-file>
</welcome-file-list>
</web-app>
从上面的 web.xml 文件中能够看到 eureka-server在servlet容器中启动时,会去监听那些类和请求转发处理
eureka-server 启动初始化环境配置内容
在eureka-server启动时,扫描web.xml文件,并且启动EurekaBootStrap类执行 contextInitialized
方法,里面执行的第一个方法就是 initEurekaEnvironment()
通过 ConfigurationManager
对象进行配置文件读取与解析,首先第一步根据 eureka.datacenter
来进行判断当前 (eureka 数据中心,eureka 运行环境),如果没有设置,默认配置就是 test
。在 ConfigurationManager.getConfigInstance()
方法使用 double check + volatile。
方式去使用单例模式,是一个亮点。
在配置环境和数据中心初始化好之后,才开始进行 eureka-server.properties
文件的读取配置,eureka的配置文件读取的整体设计思想是以 interface
接口的方式去处理和读取数据,并且如果 eureka-server.properties
文件中没有配置那么就读取默认配置数据。通过 DefaultEurekaServerConfig
类的 init
方法进行 eureka-server.properties
文件的读取。
eureka-server 创建一个自身 eureka-client 服务实例连接 eureka-server
在 EurekaBootStrap
对象中的 initEurekaServerContext
方法里面初始化好上面的配置文件之后,开始进行初始化 eureka-client
实例。为什么?因为之前说过,eureka-server
本身也是一个 eureka-client
需要注册到配置文件中的 eureka-server
里面,所以需要创建一个 eureka-client
实例对象。而在初始化 eureka-client
实例之前,也需要去读取一下 eureka-client.properties
配置文件,读取方式和之前的 eureka-server.properties
配置文件相同,然后根据 ApplicationInfoManager
对象进行创建一个 eureka-client
实例对象,通过 EurekaConfigBasedInstanceInfoProvider
对象使用Builder
模式进行创建一个新得 eureka-client
的实例详细信息,根据之前创建的 EurekaInstanceConfig
和 InstanceInfo
进行初始化一个属于 eureka-server
的 eureka-client
实例节点信息。
最后,基于ApplicationInfoManager
(包含了服务实例的信息、配置,作为服务实例管理的一个组件),eureka-client
相关的配置,一起构建了一个EurekaClient
,但是构建的时候,用的是EurekaClient
的子类,DiscoveryClient
。
如果是eureka server的话,我们在玩儿spring cloud的时候,会将这个fetchRegistry给手动设置为false,因为如果就单个eureka server启动的话,就不能设置,但是如果是eureka server集群的话,就还是要保持为true。registerWithEureka是否要设置为true。
eureka-client 整体初始化流程
(1)读取EurekaClientConfig,包括TransportConfig
(2)保存EurekaInstanceConfig和InstanceInfo
(3)处理了是否要注册以及抓取注册表,如果不要的话,释放一些资源
(4)支持调度的线程池
(5)支持心跳的线程池
(6)支持缓存刷新的线程池
(7)EurekaTransport,支持底层的eureka client跟eureka server进行网络通信的组件,对网络通信组件进行了一些初始化的操作
(8)如果要抓取注册表的话,在这里就会去抓取注册表了,但是如果说你配置了不抓取,那么这里就不抓取了
(9)初始化调度任务:如果要抓取注册表的话,就会注册一个定时任务,按照你设定的那个抓取的间隔,每隔一定时间(默认是30s),去执行一个CacheRefreshThread,给放那个调度线程池里去了;如果要向eureka server进行注册的话,会搞一个定时任务,每隔一定时间发送心跳,执行一个HeartbeatThread;创建了服务实例副本传播器,将自己作为一个定时任务进行调度;创建了服务实例的状态变更的监听器,如果你配置了监听,那么就会注册监听器
Eureka-Server 启动
PeerAwareInstanceRegistry
:可以感知eureka-server
集群的服务实例注册表,eureka-client
(作为服务实例)过来注册的注册表,而且这个注册表是可以感知到eureka-server
集群的。假如有一个eureka-server
集群的话,这里包含了其他的eureka-server
中的服务实例注册表的信息的。
PeerEurekaNodes
,代表了eureka-server
集群,peers大概来说多个相同的实例组成的一个集群,peer就是peers集群中的一个实例,PeerEurekaNodes,大概来说,猜测,应该是代表的是eureka server集群
EurekaServerContext
,代表了当前这个eureka server的一个服务器上下文,包含了服务器需要的所有的东西。将这个东西放在了一个holder中,以后谁如果要使用这个EurekaServerContext,直接从这个holder中获取就可以了。这个也是一个比较常见的用法,就是将初始化好的一些东西,放在一个holder中,然后后面的话呢,整个系统运行期间,谁都可以来获取,在任何地方任何时间,谁都可以获取这个上下文,从里面获取自己需要的一些组件。
首先会先创建一个 PeerAwareInstanceRegistry
注册表,因为是注册表需要进行数据录入,所以需要去创建一个与 eureka-server
相关的 PeerEurekaNodes
节点为什么是 s
呢?因为 eureka-server
有集群模式,所以需要多个。然后根据 PeerAwareInstanceRegistry
,PeerEurekaNodes
,EurekaServerConfig
ApplicationInfoManager
进行初始化 EurekaServerContext
eureka server端的服务上下文,为了能够更好的获取到 EurekaServerContext
对象所以使用了一个比较常用的 EurekaServerContextHolder
Holder模式进行存放在 EurekaServerContextHolder
对象中
然后调用 EurekaServerContext
的 initialize
方法进行启动,首先 start PeerEurekaNodes
的 eureka-server
的启动 eureka-server
,然后将启动好的 eureka-server
注册到 PeerAwareInstanceRegistry
中
在本地的 eureka-server
启动完成之后,因为有集群模式的存在,所以需要进行同步一下其他服务节点的注册表信息。所以在 PeerAwareInstanceRegistry
对象中的 syncUp
方法就是同步集群节点的注册表信息。而在同步的过程中使用的是三级缓存机制进行一个batch
批处理模式进行分批同步。
Eureka-Client 服务端 启动流程,服务注册
eureka-client
服务端的注册流程,和 eureka-server
启动时自身创建的 eureka-client
流程基本一样
(1)读取EurekaClientConfig,包括TransportConfig
(2)保存EurekaInstanceConfig和InstanceInfo
(3)处理了是否要注册以及抓取注册表,如果不要的话,释放一些资源
(4)支持调度的线程池
(5)支持心跳的线程池
(6)支持缓存刷新的线程池
(7)EurekaTransport,支持底层的eureka client跟eureka server进行网络通信的组件,对网络通信组件进行了一些初始化的操作
(8)如果要抓取注册表的话,在这里就会去抓取注册表了,但是如果说你配置了不抓取,那么这里就不抓取了
(9)初始化调度任务:如果要抓取注册表的话,就会注册一个定时任务,按照你设定的那个抓取的间隔,每隔一定时间(默认是30s),去执行一个CacheRefreshThread,给放那个调度线程池里去了;如果要向eureka server进行注册的话,会搞一个定时任务,每隔一定时间发送心跳,执行一个HeartbeatThread;创建了服务实例副本传播器,将自己作为一个定时任务进行调度;创建了服务实例的状态变更的监听器,如果你配置了监听,那么就会注册监听器
在异常配置中执行完毕服务会自动注册到 eureka-server
的注册表中,但是其最关键的注册步骤是在 InstanceInfoReplicator
对象中进行执行的,而在 eureka-client
启动时初始化 DiscoveryClient
对象,在 DiscoveryClient
创建时会初始化一堆的调度任务,在调度任务中根据 eureka-client.properties
配置文件的熟悉 shouldRegisterWithEureka
进行判断是否可以注册到eureka-server
注册中心,默认为 true
可以直接注册到注册中心,然后调用 InstanceInfoReplicator.start()
方法进行调用 discoveryClient.register()
方法通过 eurekaTransport
网络通信组件请求到 eureka-server
的 apps/{appId}
接口进行服务注册的请求发送。
Eureka-Server 接收到注册服务请求处理逻辑
Eureka-Server
在接收到 eureka-client
发送的注册请求是,会将url分发到 com.netflix.eureka.resources.ApplicationResource#addInstance
接口上面,进行注册逻辑处理,上报过来的数据是 InstanceInfo
主要包含两块数据:1、主机名、ip地址、端口号、url地址;1、lease(租约)的信息:保持心跳的间隔时间,最近心跳的时间,服务注册的时间,服务启动的时间,
最核心的一点是还是调用 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
的服务注册方法。在调用 register
接口时,其实本质上还是调用父类的AbstractInstanceRegistry
的register
方法,在父类 AbstractInstanceRegistry
里面保存实例注册信息数据的数据类型是使用的Map格式的(ConcurrentHashMap<String, Map<String, Lease>>),通过 AppName
作为 Key
,服务实例作为 value
,比如:
appName,APPLICATION0,服务名称,ServiceA,或者是别的什么名称
instanceId,i-0000001,服务实例id,一个服务名称会对应多个服务实例,每个服务实例的服务名称当然是一样的咯,但是服务实例id是不一样的
然后将当前的数据加入到 register Map中,具体流程如下图
eureka-server
接收到服务注册请求时在执行完注册逻辑时,在集群模式下通过 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers
方法同步到其他的 eureka-server
服务中
Eureka-Client 第一次启动全量更新注册表
在 eureka-client
初始化时创建 DiscoveryClient
对象时会根据 eureka-client.properties
配置文件中的 shouldFetchRegistry
字段进行判断是否需求全量拉取,默认是 true
,所以在初始化时就会调用本地的 fetchRegistry
方法进行获取全量注册表通过 eurekaTransport
调用 apps/
接口,将获取到的全量服务信息,重新放入到本地的 Applications
里面,在获取完之后 Applications
会重新根据当前服务列表计算出一个新的HashCode
值
Eureka-Server 二级缓存更新注册表信息
在 eureka-server
端接收到拉取注册表的请求,会从 ResponseCache
接口里面根据生成的 Key
去获取对应的 Value
数据,而 ResponseCache
相应的实现类是 ResponseCacheImpl
,里面有一个可读 readOnlyCacheMap
数据Map还有一个 readWriteCacheMap
读写数据Map,其里面的实现是从 readOnlyCacheMap
数据中获取。
而 readOnlyCacheMap
里面的数据是根据调度每 30s
从 readWriteCacheMap
更新到 readOnlyCacheMap
里面,而 readWriteCacheMap
则是根据 PeerAwareInstanceRegistry
注册表去获取所有的 eureka-server
的 Application
相关节点信息。属于被动过期
在 readWriteCacheMap
在构建的时候,指定了一个自动过期的时间,默认值就是180s
,所以你往readWriteCacheMap
中放入一个数据过后,自动会等180s
过后,就将这个数据给他过期了,属于定时过期
。
而当服务实例主动下线时,也会去请求 eureka-server
的接口调用 apps/" + appName + '/' + id
是 DELEATE
模式的访问,而当 eureka-server
主动下线时就会去请求这个接口,而在接到请求时,会调用ResponseCache.invalidate()
,将之前缓存好的ALL_APPS这个key对应的缓存,给他过期掉
将readWriteCacheMap中的ALL_APPS缓存key,对应的缓存给过期掉,属于主动过期
Eureka-Client 获取增量注册表
Eureka-client
在启动的时候会通过 DiscoveryClient
对象的创建方法进行拉取增量注册表,并且在 cacheRefresh
调度任务中进行周期性的拉取最新更新的注册表数据,而增量注册表的数据主要还是通过 eureka-client的eurekaTransport
组件进行请求 eureka-server的 apps/delta
服务接口进行获取。
而 eureka-server
主要还是通过缓存注册表中的 com.netflix.eureka.registry.AbstractInstanceRegistry#recentlyChangedQueue
父类的 最新修改的服务实例队列进行获取,而 recentlyChangedQueue
里面的数据是通过实例有变更的操作然后会自动 add
到队列中,但是此队列仅仅只保存 3min内
的数据,并且 com.netflix.eureka.registry.AbstractInstanceRegistry#getDeltaRetentionTask
定时调度任务会 30s
执行一次,进行判断当前的 recentlyChangedQueue
队列数据是否有超过 3min
的数据,如果有,那么就会清除掉。
在 eureka-client
获取到增量注册表中的数据时会生成一个 HashCode
,会和本地保存的eureka-server
的注册表数据创建的 HashCode
进行比对,如果不相等,那么就重新全量获取一下注册表数据。
Eureka-Client 服务续约
在 eureka-client
进行初始化的时候会进行创建 DiscoveryClient
对象,而这个对象中会初始化 heartbeat
线程进行与 eureka-server
进行服务续约,其主要调用的还是 eureka-client
对应的 eurekaTransport
网络通信组件去请求 eureka-server
的 "apps/" + appName + '/' + id
接口,进行执行 服务续约逻辑,其主要的还是调用 com.netflix.eureka.registry.AbstractInstanceRegistry#renew
方法将当前的实例 lastUpdateTimestamp
字段时间延长 duration
字段的时间默认是 (90*1000)更新时间戳,而heartbeat
线程默认 30s
执行一次。
Eureka-Client 服务下线
eureka-client
服务下线时,调用的是 com.netflix.discovery.DiscoveryClient#shutdown
方法,里面会关闭所有的调度任务,并且去通知 eureka-server
当前实例服务下线 通过 eurekaTransport
通信组件调用 "apps/" + appName + '/' + id
DELETE
模式请求到 eureka-server
端 com.netflix.eureka.resources.InstanceResource#cancelLease
接口,然后服务注册表 PeerAwareInstanceRegistryImpl
调用 cancel
方法把当前的服务实例从当前的注册表中清除并且把当前的实例添加到 recentlyChangedQueue
最近修改实例的队列中,并且把当前实例的 ResponseCacheImpl
缓存清除掉
而在 eureka-server
服务中对应的缓存数据 30s
过后也会进行数据清理,对应的其他的 eureka-client
在拉取注册表时就会把当前的下线服务实例进行状态更新并且清除。
Eureka-Server 监控 Eureka-Client故障之后自动下线
eureka-server
在启动的时候回去初始化一个 EvictionTask
线程,主要是监控根据当前时间去判断
(当前服务实例心跳时间 > (上次心跳时间 + 90 + additionalLeaseMs))也就是说上次 服务实例的心跳时间加上默认过期时间(90s)小于本次监控的时间,那么就认为当前的服务已经下线了,因为在规定时间内,服务实例没有上报心跳,那么就表示当前的服务实例已经挂掉。因为服务实例在 renew()
的时候默认也加了90s
所以过期的周期为 180s
,因为其他实例在同步注册表时是 30s
同步一次,所以其他服务实例的感知服务下线的时间会在慢一点。
根据上面的公式计算得到的服务实例就是已经下线的服务实例,然后根据 getRenewalPercentThreshold
默认(0.85)属性去保留总实例的 85%的实例(比如:总共有10个实例 10 ✖️ 0.85 =8.5,因为是int强转即 8,所以最多也就是只能摘除2个服务实例 )然后根据计算出来的数据,当前总下线服务实例的集合➖8=最后需要下线的服务实例集合,然后开始循环一个一个调用 com.netflix.eureka.resources.InstanceResource#cancelLease
接口进手动服务下线。
Eureka-Server 网络故障自我保护机制
假如 Eureka-Server
本身的网络故障时,这个时候会接收不到 eureka-client
的心跳上报,那么在 eureka-server
的自动监控服务下线的线程就会误下线正确的实例,所以这时候就需要 eureka-server
自身的服务保护机制,主要是在心跳线程运行时的 Task任务中首先判断是否开启了自我保护机制 enableSelfPreservation
配置默认为 true
自动开启,开启之后会进行心跳的次数比对,因为 eureka
希望每个服务实例每分钟会有两次的心跳过来,而总体的期望心跳值计算公式为:(总实例数 * (60/2)* 0.85)这个公式计算出来的数据是当前总实例最低的心跳次数为多少,如果当前的心跳次数大于最低的心跳阈值,那么就不会触发自我保护机制,如果要是心跳次数小于当前的阈值,那么就会开启自我保护机制,开启之后就不会进行服务下线的操作。而每次的服务实例进行心跳续约的时候,就会自身增加一次记录值 com.netflix.eureka.util.MeasuredRate#currentBucket
Eureka-Server 集群模式
eureka-server
在集群模式启动时,首先 eureka-server
本身是一个 eureka-client
,所以会去注册到配置的集群 eureka-server
链接地址上面,所以在启动是,会在对方的 eureka-server
服务器进行注册,并且能获取到对方服务器的实例列表,并且会同步到本地的 集群列表中 通过 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#syncUp
方法进行同步。而当 eureka-client
进行注册到 eureka-server
服务器上面时,也会去同步到配置文件中配置的集群服务器地址上面通过 com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers
方法进行同步,所以通过以上方式可以进行注册表数据同步
而数据同步的模式是使用的三级缓存batch处理
当 eureka-server
收到 eureka-client
的服务注册,心跳续约,服务下线时,这时候去同步集群服务实例信息,首先会把当前过来的操作封装成一个TaskHolder
先放入一个 com.netflix.eureka.util.batcher.AcceptorExecutor#acceptorQueue
队列中先进行暂存,并且在创建 com.netflix.eureka.util.batcher.AcceptorExecutor
对象时就已经把其内部线程 com.netflix.eureka.util.batcher.AcceptorExecutor.AcceptorRunner
进行启动,而这个线程主要是将 com.netflix.eureka.util.batcher.AcceptorExecutor#acceptorQueue
队列里面的数据放入到 com.netflix.eureka.util.batcher.AcceptorExecutor#processingOrder
队列里面,然后根据 TaskHolder
的到期时间进行分批放入到 com.netflix.eureka.util.batcher.AcceptorExecutor#batchWorkQueue
队列里面,然后 BatchWorkerRunnable
线程会去从 batchWorkQueue
队列中批量获取数据,然后调用 com.netflix.eureka.cluster.ReplicationTaskProcessor#process(java.util.List<com.netflix.eureka.cluster.ReplicationTask>)
方法进行数据批量推送到 eureka-server
服务器,而服务器接收到数据会根据服务实例的 action
进行判断当前过来的操作是什么类型的从而进行再次分发。