Eureka 核心源码解析
Eureka 作为 Spring Cloud 的核心模块之一,担任着服务注册发现等重要作用。如果梳理一下 Eureka 实际的工作流程,大体可以将它分为以下几个部分:
服务注册
服务续约
服务剔除
服务下线
服务发现
集群信息同步
上述各个方面,基于服务的运行场景不同,可能分别从 Eureka 的服务端(注册中心)与客户端(包含服务提供者与服务调用者)进行分析,为了简便下文中将 Eureka 服务端称为 Eureka-server,客户端称为 Eureka-client。本文先来说说基础的服务注册。
服务注册
Eureka-client
在 Eureka-client 中,DiscoveryClient 这个类用来和 Eureka-server 互相协作,看一下它的注释,它可以完成服务注册,服务续约,服务下线,获取服务列表等工作,可以说它完成了 client 的大多数功能。首先,看一下用来向 eureka-server 发起注册请求的register
方法:
调用 AbstractJerseyEurekaHttpClient
类的register
方法:
Jersey 是一个 Restful 请求服务的框架,与常用的 springmvc 类似,后面会讲到在 Eureka-server 拦截请求的时候也用到了 Jersy。
在这里调用底层类:
通过 HTTP 客户端发送 http 请求,并构建响应结果。
Eureka-server
在 Eureka-server,配置好yml
文件中必需的参数后,只需要一个注解开启:
查看该注解的实现方法,发现为空白注解,并使用了@Import
:
查看EurekaServerMarkerConfiguration
类的实现:
在这里只向 spring 容器中注入 bean,没有任何意义。这里用到了 Springboot 的自动装配(这个不熟悉的可以参考springboot零配置启动):
发现 Eureka server 核心的自动配置类EurekaServerAutoConfiguration
我们看到,在这个类上有条件注入注解:
只有在 Spring 容器中存在 Marker 这个 Bean 时才会实例化这个类,所以@EnableEurekaServer
就相当于一个开关,起到标识的作用。
在这个配置类中定义了拦截器,同样使用 Jersy 拦截请求:
ApplicationResource
类的addInstance
方法接收请求,在对实例的信息进行验证后,向服务注册中心添加实例:
进入InstanceRegistry
的register
方法:
在这里做了两个功能:
1、调用handleRegistration
,在方法中使用publishEvent
发布了监听事件 。Spring 支持事件驱动,可以监听者模式进行事件的监听,这里广播给所有监听者,收到一个服务注册的请求。
至于监听器,可以由我们自己手写实现,参数中的事件类型 spring 会帮我们直接注入:
2、调用父类PeerAwareInstanceRegistryImpl
的register
方法:
进行了下面的操作:
① 拿到微服务的过期时间,并进行更新
② 将服务注册交给父类完成
③ 完成集群信息同步(这个会在后面说明)
调用父类AbstractInstanceRegistry
的register
方法,在这开始真正开始做服务注册。先说一下在这个类中定义的 Eureka-server 的服务注册列表的结构:
ConcurrentHashMap
中外层的 String 表示服务名称;
Map
中的 String 表示服务节点的 id (也就是实例的 instanceid);
Lease
是一个心跳续约的对象,InstanceInfo
表示实例信息。
首先,注册表根据微服务的名称或取 Map,如果不存在就新建,使用putIfAbsent
。
然后,从gMap
(gMap 就是该服务的实例列表)获取一次服务实例,判断这个微服务的节点是否存在,第一次注册的情况下一般是不存在的
当然,也有可能会发生注册信息冲突时,这时 Eureka 会根据最后活跃时间来判断到底覆盖哪一个:
这段代码中,Eureka 拿到存在节点的最后活跃时间,和当前注册节点的发起注册时间,进行对比。当存在的节点的最后活跃时间大于当前注册节点的时间,就说明之前存在的节点更活跃,就替换当前节点。
这里有一个思想,就是如果 Eureka 缓存的老节点更活跃,就说明它能够使用,而新来的服务我并不知道是否能用,那么 Eureka 就保守的使用了可用的老节点,从这一点也保证了可用性
在拿到服务实例后对其进行封装:
Lease 是一个心跳续约的包装类,里面存放了注册信息,最后操作时间,注册时间,过期时间,剔除时间等信息。在这里把注册实例及过期时间放到这个心跳续约对象中,再把心跳续约对象放到gmap
注册表中去。之后进行改变服务状态,系统数据统计,至此一个服务注册的流程就完成了。
注册完成后,查看一下registry
中的服务实例,发现我们启动的 Eureka-client 都已经放在里面了:
服务续约
Eureka-client
服务续约由 Eureka-client 端主动发起,由之前介绍过的DiscoveryClient
类中的renew
方法完成,主要内容仍然是发送 http 请求:
每隔 30 秒进行一次续约,调用AbstractJerseyEurekaHttpClient
的sendHeartBeat
方法:
Eureka-server
在 Eureka-server 端,服务续约的调用链与服务注册基本相同:
主要看一下AbstractInstanceRegistry
的renew
方法:
先从注册表获取该服务的实例列表 gMap,再从 gMap 中通过实例的 id 获取具体的 要续约的实例。之后根据服务实例的InstanceStatus
判断是否处于宕机状态,以及是否和之前状态相同。如果一切状态正常,最终调用Lease
中的renew
方法:
可以看出,其实服务续约的操作非常简单,它的本质就是修改服务的最后的更新时间。将最后更新时间改为系统当前时间加上服务的过期时间。值得提一下的是,lastUpdateTimestamp
这个变量是被volatile
关键字修饰的。
之前的文章中我们讲过volitaile
是用来保证可见性的。那么要被谁可见呢,提前说一下,这里要被服务剔除中执行的定时任务可见,后面会具体分析。
服务剔除
Eureka-server
当 Eureka-server 发现有的实例没有续约超过一定时间,则将该服务从注册列表剔除,该项工作由一个定时任务完成的。该任务的定义过程比较复杂,仅列出其调用过程:
在AbstractInstanceRegistry
的postInit
方法中,定义EvictionTask
定时任务,构建定时器启动该任务,执行任务中剔除方法 evict()
。
任务的时间被定义为 60 秒,即默认每分钟执行一次。具体查看evit()
剔除方法:
实现了功能:
1、新建实例列表expiredLeases
,用来存放过期的实
2、遍历registry
注册表,对实例进行检测工作,使用isExpired
方法判断实例是否过期:
解释一下各个参数的意义:
这里进行判断:
当该条件成立时,认为服务过期。在 Eureka 中过期时间默认定义为 3 个心跳的时间,一个心跳是 30 秒,因此过期时间是 90 秒。当该条件成立时,认为服务过期。在 Eureka 中过期时间默认定义为 3 个心跳的时间,一个心跳是 30 秒,因此过期时间是 90 秒
当以上两个条件之一成立时,判断该实例过期,将该过期实例放入上面创建的列表中。注意这里仅仅是将实例放入 List 中,并没有实际剔除。
在实际剔除任务前,需要提一下 eureka 的自我保护机制,当 15 分钟内,心跳失败的服务大于一定比例时,会触发自我保护机制。
这个值在 Eureka 中被定义为 85%,一旦触发自我保护机制,Eureka 会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据。
参数意义:
上面的代码中根据自我保护机制进行了判断,使用 Min 函数计算两者的最小值,剔除较小数量的服务实例。
举个例子,假如当前共有 100 个服务,那么剔除阈值为 85,如果 list 中有 60 个服务,那么就会剔除该 60 个服务。但是如果 list 中有 95 个服务,那么只会剔除其中的 85 个服务,在这种情况下,又会产生一个问题,eureka-server 该如何判断去剔除哪些服务,保留哪些服务呢?
这里使用了随机算法进行剔除,保证不会连续剔除某个微服务的全部实例。最终调用internalCancel
方法,实际执行剔除。
其实剔除操作的实质非常简单,就是从gMap
中remove
掉这个节点,并从缓存中剔除。
服务下线
Eureka-client
当 eureka-client 关闭时,不会立刻关闭,需要先发请求给 eureka-server,告知自己要下线了。主要看一下客户端shutdown
方法,其中调用关键的unregister
方法:
调用AbstractJerseyEurekaHttpClient
的cancel
方法
发送 http 请求告诉 eureka-server 自己下线。
Eureka-server
调用AbstractInstanceRegistry
中 cancel
方法:
最终还是调用了和服务剔除中一样的方法,remove
掉了gMap
中的实例。
服务发现
Eureka-client
在学习服务发现的源码前,先写一个测试用例:
调用DiscoveryClient
的getInstances
方法,可以根据服务 id 获取服务实例列表:
那么这里就有一个问题了,我们还没有去调用微服务,那么服务列表是什么时候被拉取或缓存到本地的服务列表的呢?答案是在这里调用了CompositeDiscoveryClient
的 getInstances()
方法:
中间调用过程省略:
查看Applications
中的getInstancesByVirtualHostName
方法:
发现一个名为virtualHostNameAppMap
的 Map 集合中已经保存了当前所有注册到 eureka 的服务列表。
也就是说,在我们没有手动去调用服务的时候,该集合里面已经有值了,说明在 Eureka-server 项目启动后,会自动去拉取服务,并将拉取的服务缓存起来。
那么追根溯源,来查找一下服务的发现究竟是什么时候完成的。回到DiscoveryClient
这个类,在它的构造方法中定义了任务调度线程池cacheRefreshExecutor
,定义完成后,调用initScheduledTask
方法:
在这个 thread 中,调用了refreshRegistry()
方法:
在fetchRegistry
方法中,执行真正的服务列表拉取:
在fetchRegistry
方法中,先判断是进行增量拉取还是全量拉取:
1、全量拉取
当缓存为null
,或里面的数据为空,或强制时,进行全量拉取,执行getAndStoreFullRegistry
方法:
2、增量拉取
只拉取修改的,执行getAndUpdateDelta
方法:
①②:先发送 http 请求,获取在 eureka-server 中修改或新增的集合
③:判断,若拉取的集合为 null,则进行全量拉取
④:更新操作,在updateDelta
方法中,根据类型进行更改
⑤:获取一致性的 hashcode 值,用来校验 eureka-server 集合和本地是否一样
在这进行判断,若远程集合的 hash 值等于缓存中的 hash 值,不需要拉取,否则再进行拉取一次。
最后提一下,Applications
中定义的以下这些变量,都是在 eureka-server 中准备好的,直接拉取就可以了。
对服务发现过程进行一下重点总结:
服务列表的拉取并不是在服务调用的时候才拉取,而是在项目启动的时候就有定时任务去拉取了,这点在
DiscoveryClient
的构造方法中能够体现;服务的实例并不是实时的 Eureka-server 中的数据,而是一个本地缓存的数据;
缓存更新根据实际需求分为全量拉取与增量拉取。
集群信息同步
Eureka-server
集群信息同步发生在 Eureka-server 之间,之前提到在PeerAwareInstanceRegistryImpl
类中,在执行register
方法注册微服务实例完成后,执行了集群信息同步方法replicateToPeers
,具体分析一下该方法:
首先,遍历集群节点,用以给各个集群信息节点进行信息同步。
然后,调用replicateInstanceActionsToPeers
方法,在该方法中根据具体的操作类型 Action,选择分支,最终调用PeerEurekaNode
的register
方法:
最终发送 http 请求,但是与普通注册操作不同的时,这时将集群同步的标识置为 true,说明注册信息是来自集群同步。
在注册过程中运行到addInstance
方法时,单独注册时isReplication
的值为 false,集群同步时为 true。通过该值,能够避免集群间出现死循环,进行循环同步的问题。
最后
到这里,Eureka 声明周期中比较重要的六个部分我们就讲完了。由于篇幅有限,只能讲一下大致的流程,如果您还想再深入了解一些,不妨自己看看源码,毕竟,源码时最好的老师。
如果觉得对您有所帮助,小伙伴们可以点赞、转发一下,非常感谢。
公众号
码农参上
,加个好友,做个点赞之交啊
版权声明: 本文为 InfoQ 作者【码农参上】的原创文章。
原文链接:【http://xie.infoq.cn/article/21fcbfee2a16543ba0c9f236a】。文章转载请联系作者。
评论