SpringCloud-2020.3版本 Eureka源码解析

本文解析了Spring Cloud Eureka的核心流程,包括服务器启动过程、客户端启动配置、服务注册与发现、心跳维护、集群同步及自我保护机制。深入理解了EurekaServer的启动、客户端初始化和核心业务逻辑。
摘要由CSDN通过智能技术生成

Spring-Cloud-Eureka 源码解析

小弟有一个开源项目,希望大家可以多多一键三连,谢谢大家

nirvana-reborn

后续的源码解析也都会进行同步更新上去

核心流程

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的实例详细信息,根据之前创建的 EurekaInstanceConfigInstanceInfo进行初始化一个属于 eureka-servereureka-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有集群模式,所以需要多个。然后根据 PeerAwareInstanceRegistryPeerEurekaNodes ,EurekaServerConfig ApplicationInfoManager进行初始化 EurekaServerContext eureka server端的服务上下文,为了能够更好的获取到 EurekaServerContext对象所以使用了一个比较常用的 EurekaServerContextHolder Holder模式进行存放在 EurekaServerContextHolder对象中

然后调用 EurekaServerContextinitialize方法进行启动,首先 start PeerEurekaNodeseureka-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-serverapps/{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接口时,其实本质上还是调用父类的AbstractInstanceRegistryregister方法,在父类 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里面的数据是根据调度每 30sreadWriteCacheMap更新到 readOnlyCacheMap里面,而 readWriteCacheMap则是根据 PeerAwareInstanceRegistry注册表去获取所有的 eureka-serverApplication相关节点信息。属于被动过期

readWriteCacheMap在构建的时候,指定了一个自动过期的时间,默认值就是180s,所以你往readWriteCacheMap中放入一个数据过后,自动会等180s过后,就将这个数据给他过期了,属于定时过期

而当服务实例主动下线时,也会去请求 eureka-server的接口调用 apps/" + appName + '/' + idDELEATE模式的访问,而当 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-servercom.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进行判断当前过来的操作是什么类型的从而进行再次分发。

Eureka整体架构设计=在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值