上一篇文章Eureka(三)——client注册过程中,我们详述了Eureka Client是如何完成注册、续约、获取服务、下线等操作的。本篇文章,我们来记述Eureka Server是如何工作的。
Eureka基本架构
如果上图所示,Eureka的基本架构由3个角色组成:
Eureka Server
提供服务注册和发现。它们之间会做注册服务的同步,从而保证状态一致
Service Provider
服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到。它会向Eureka Server做Register(服务注册)、Renew(服务续约)、Cancel(服务下线)等操作
Service Consumer
服务消费方,从Eureka获取服务列表,从而能够消费服务。它会向Eureka Server获取注册服务列表,并消费服务。
Eureka Server启动过程
首先从SpringBoot的入口开始,@EnableEurekaServer的注解如下:
1 | (ElementType.TYPE) |
它导入了另一个配置类EurekaServerMarkerConfiguration
,这个类功能上没有任何用处,仅仅是添加了一个Marker bean,用来激活EurekaServerAutoConfiguration
。
我们看到在spring-cloud-netflix-eureka-server
的spring.factories
文件中有如下配置:
1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
EurekaServerAutoConfiguraion
上加了@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
这样的配置,只有EurekaServerMarkerConfiguration.Marker
存在的情况下它才能被激活。
EurekaServerAutoConfiguration
在EurekaServerAutoConfiguration
中会完成大部分Bean的新建:
- EurekaServerConfig。服务配置
- EurekaController。一个简单的Controller,负责eureka dashboard的显示,包括Node自身的信息,以及在该节点上保存的注册信息。
- ServerCodecs。提供对json和xml的解析器
- InstanceRegistry。它是注册服务的关键,继承
PeerAwareInstanceRegistryImpl
,提供了服务注册、续约、下线等操作。 - PeerEurekaNodes。维护当前Eureka Server要同步的Peer Node,并通过一个定时任务维护Peer Node的信息。
- EurekaServerContext
- EurekaServerBootstrap
- JerseyApplication
- FilterRegistrationBean。把JerseyApplication作为一个Filter,设置最低优先级并过滤Eureka Context下面所有的请求。
EurekaServerInitializerConfiguration
EurekaServerAutoConfiguration
通过@Import(EurekaServerInitializerConfiguration.class)
导入EurekaServerInitializerConfiguration
。
EurekaServerInitializerConfiguration
实现了SmartLifecycle
接口的start方法,会在所有Spring bean都初始化完成后调用该方法。在start方法中调用EurekaServerBootstrap.contextInitialized
来初始化Eureka Server:
EurekaServerBootstrap
EurekaServerInitializerConfiguration
的start
方法中调用EurekaServerBootstrap
的contextInitialized
方法:
1 | public void contextInitialized(ServletContext context) { |
initEurekaEnvironment
方法中初始化数据中心的信息以及eureka的运行环境initEurekaServerContext
方法有如下步骤:- 注册状态转化器,保持向后兼容
- 判断是否是亚马逊的数据中心
- 调用
EurekaServerContextHolder.initialize
初始化Eureka Server上下文 - 调用
PeerAwareInstanceRegistryImpl.syncUp
从相邻的eureka节点复制注册表。遍历获取到的服务注册信息,调用AbstractInstanceRegistry.register
方法将服务注册到本地。 - 调用
PeerAwareInstanceRegistryImpl.openForTraffic
设置expectedNumberOfRenewsPerMin
和numberOfRenewsPerMinThreshold
的值,并将eureka的状态设为UP
- 调用
EurekaMonitors.registerAllStats
注册所有的监控
PeerAwareInstanceRegistryImpl.syncUp
PeerAwareInstanceRegistryImpl.syncUp
方法调用getApplications()
从相邻的eureka节点复制注册表。然后遍历获取到的服务注册信息,调用AbstractInstanceRegistry.register
方法将服务注册到本地。流程如下:
Eureka Server实现细节
InstanceRegistry
上文我们提高,InstanceRegistry
类是注册服务的关键,提供了服务注册、续约、下线等操作。下图是它的继承关系:
我们再来看看Eureka Server几个对外接口的实现
服务注册
服务注册(register)接口接收Eureka Client的注册请求,其流程如下:
ApplicationResource
类接收Http请求,调用InstanceRegistry.register
方法InstanceRegistry.register
方法中记录日志并广播EurekaInstanceRegisteredEvent
事件,接着调用父类PeerAwareInstanceRegistryImpl
的register
方法。PeerAwareInstanceRegistryImpl.register
方法有两步:- 调用父类
AbstractInstanceRegistry
的register
方法注册服务 - 调用
replicateToPeers
方法向其他Eureka Server节点(Peer)做状态同步
- 调用父类
AbstractInstanceRegistry.register
AbstractInstanceRegistry.register
方法是服务注册最终发生的位置,代码(删除日志代码)如下:
1 | public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) { |
注册的服务列表保存在registry变量中,它是一个嵌套的hashmap:
1 | private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry |
- 第一层hash map的key是app name,也就是服务的应用名字。本例中为
EUREKA-CLIENT
- 第二层hash map的key是instance id,也就是实例的id。本例中为
wangqideimac.lan:eureka-client:8762
PeerAwareInstanceRegistryImpl.replicateToPeers
PeerAwareInstanceRegistryImpl.replicateToPeers
方法将服务复制到另外的Eureka Server节点上,其主要功能代码如下:
1 | for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) { |
遍历Eureka Server节点(peerEurekaNodes.getPeerEurekaNodes()
),排除自身节点,调用replicateInstanceActionsToPeers
方法将服务同步到Eureka Server节点。
PeerEurekaNodes
PeerEurekaNodes
类中保存Eureka Server的节点,以及相应的其他信息。其获取流程如下:
- EurekaBootStrap.contextInitialized
- EurekaBootStrap.initEurekaServerContext
- DefaultEurekaServerContext.initialize
- PeerEurekaNodes.start
peersUpdateTask
在PeerEurekaNodes.start
方法中启动一个周期性的任务来更新Eureka Server的节点:
1 | taskExecutor.scheduleWithFixedDelay( |
更新频率可以通过eureka.server.peerEurekaNodesUpdateIntervalMs
来配置
peersUpdateTask是一个线程:
1 | Runnable peersUpdateTask = new Runnable() { |
updatePeerEurekaNodes
peersUpdateTask线程体中调用updatePeerEurekaNodes(resolvePeerUrls())
方法更新Eureka Server节点。流程如下:
resolvePeerUrls()
方法调用EndpointUtils.getDiscoveryServiceUrls
来获取所有Eureka Server节点的地址。该方法默认从配置文件中读取。
updatePeerEurekaNodes
方法移除失效的节点,增加新的节点。
PeerAwareInstanceRegistryImpl.replicateInstanceActionsToPeers
PeerAwareInstanceRegistryImpl.replicateInstanceActionsToPeers
方法的功能是将服务信息复制到另一个Eureka Server节点中。
它根据节点的不同操作:Cancel
、Heartbeat
、Register
、StatusUpdate
、DeleteStatusOverride
,调用PeerEurekaNode
的不同方法来完成服务信息的复制。这里实际上和Eureka Client对Eureka Server的操作是一样的。
服务续约
服务续约(renew)接口接收Eureka Client的续约请求,其流程如下:
InstanceResource
接收服务续约请求,处理方法为renewLease
。它调用
InstanceRegistry.renew
方法,然后判断renew
方法返回的结果:- 如果返回
true
:创建一个状态为200的响应返回 - 如果返回
false
:说明该服务并没有被注册,创建一个状态为404的响应返回,Client收到这个响应后注册该服务。
- 如果返回
InstanceRegistry.renew
方法首先广播EurekaInstanceRenewedEvent
事件,然后调用父类PeerAwareInstanceRegistryImpl
的renew
方法PeerAwareInstanceRegistryImpl.renew
方法有两步:- 首先调用父类
AbstractInstanceRegistry
的renew
方法, - 调用
replicateToPeers
方法将服务信息同步到另外的节点上。
- 首先调用父类
AbstractInstanceRegistry.renew
AbstractInstanceRegistry.renew
方法是服务续约最终发生的位置,代码(删除日志代码)如下:
1 | public boolean renew(String appName, String id, boolean isReplication) { |
服务下线
服务下线(cancel)接口接收Eureka Client的下线请求,其流程如下:
InstanceResource
接收服务下线请求,处理方法为cancelLease
。它调用
InstanceRegistry.cancel
方法,然后判断cancel
方法返回的结果:- 如果返回
true
:创建一个状态为200的响应返回 - 如果返回
false
:说明该服务并没有被注册,创建一个状态为404的响应返回。
- 如果返回
InstanceRegistry.cancel
方法首先广播EurekaInstanceCanceledEvent
事件,然后调用父类PeerAwareInstanceRegistryImpl
的cancel
方法PeerAwareInstanceRegistryImpl.cancel
方法有两步:- 首先调用父类
AbstractInstanceRegistry
的cancel
方法, - 调用
replicateToPeers
方法将服务信息同步到另外的节点上。 - 重新计算
expectedNumberOfRenewsPerMin
和numberOfRenewsPerMinThreshold
- 首先调用父类
AbstractInstanceRegistry.cancel
方法调用InstanceRegistry.internalCancel
方法。InstanceRegistry.internalCancel
方法首先广播EurekaInstanceCanceledEvent
事件,然后调用父类AbstractInstanceRegistry
的internalCancel
方法
AbstractInstanceRegistry.internalCancel
AbstractInstanceRegistry.internalCancel
方法用于下线任务,其流程如下:
- 从服务注册表
registry
中将相应的服务删除 - 如果服务注册表中不存在相应的服务,则返回
false
- 否则调用
Lease.cancel
方法,将evictionTimestamp
设置为当前时间。然后返回true
服务获取
服务获取(fetch registries)接口接收Eureka Client的获取服务请求,其流程如下:
- 调用
PeerAwareInstanceRegistryImpl.shouldAllowAccess
方法判断是否有权限获取服务信息 - 根据请求生成
Key
使用
Key
从ResponseCacheImpl
中获取服务信息。ResponseCacheImpl
中的readWriteCacheMap
是一个LoadingCache
类型的变量,其中保存服务信息的缓存。如果readWriteCacheMap
中没有保存某个key的值,或者某个key的值失效了,则调用generatePayload
方法生成这个key的值。- 返回经过压缩的服务信息。
失效服务剔除
失效服务剔除(Eviction)用来定期在Eureka Server检测失效的服务,检测标准就是超过一定时间没有renew的服务。其流程如下:
失效服务剔除在AbstractInstanceRegistry#EvictionTask
类中完成。它的启动流程如下:
- PeerAwareInstanceRegistry.openForTraffic
- AbstractInstanceRegistry.postInit,在postInit中启动EvictionTask,其执行的间隔周期默认为60秒
- EvictionTask代码如下:
1 | public void run() { |
- 首先调用
getCompensationTimeMs()
方法获取补偿时间(毫秒) - 然后调用
evict
方法检测失效的服务。
getCompensationTimeMs
1 | long getCompensationTimeMs() { |
补偿时间的计算公式为:当前时间 - 最后eviction任务执行时间 - eviction任务的执行间隔
evict
1 | public void evict(long additionalLeaseMs) { |
- 调用
PeerAwareInstanceRegistry.isLeaseExpirationEnabled
方法判断是否允许剔除失效服务。
1 |
|
该方法和自我保护机制有关,如果Renews(last min) < Renews threshold
,表示Eureka Server进入了自我保护模式,这时Eureka Server不再剔除失效服务。
- 遍历整个服务注册表,调用
Lease.isExpired
方法判断租约是否过期,将过期的租约加入expiredLeases
1 | public boolean isExpired(long additionalLeaseMs) { |
计算最大允许清理租约的数量,以及允许清理租约的数量
即使Eureka Server关闭自我保护机制,如果使用
renewalPercentThreshold = 0.85
默认配置,结果会是分批逐步过期,举个例子:
1 | // 假设 20 个租约,其中有 10 个租约过期。 |
是否开启自我保护的差别,在于是否执行清理过期租约的逻辑。如果想关闭分批逐步过期,设置renewalPercentThreshold = 0
- 随机清理过期的租约。由于租约是按照应用顺序添加到数据,通过随机的方式,尽量避免单个应用被全部过期。调用
AbstractInstanceRegistry.internalCancel
方法下线过期的服务。
总结
Eureka Server中的关键类是InstanceRegistry
,在其中完成以下的几个关键任务:
服务注册
调用
AbstractInstanceRegistry.register
方法注册服务,再调用replicateToPeers
方法向其他Eureka Server节点(Peer)做状态同步。服务列表保存在一个嵌套的ConcurrentHashMap中:
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
:第一层hash map的key是app name,也就是服务的应用名字,本例中为EUREKA-CLIENT
。第二层hash map的key是instance id,也就是实例的id,本例中为wangqideimac.lan:eureka-client:8762
服务续约
调用
AbstractInstanceRegistry.renew
方法续约服务,再调用replicateToPeers
方法向其他Eureka Server节点(Peer)做状态同步。续约的主要操作是更新
registry
中保存的服务实例的续约信息(Lease
),刷新其中的最近续约时间。服务下线
调用
AbstractInstanceRegistry.cancel
方法下线服务(最终调用的是AbstractInstanceRegistry.internalCancel
方法),再调用replicateToPeers
方法向其他Eureka Server节点(Peer)做状态同步。将相应的服务id从服务注册表
registry
中删除。如果服务注册表中不存在指定的服务id则返回false
,否则调用Lease.cancel
方法将evictionTimestamp
设置为当前时间并返回true
服务获取
服务获取功能在
ApplicationsResource.getContainers
方法中执行。它从缓存ResponseCacheImpl
中获取服务信息。ResponseCacheImpl
中有一个LoadingCache
类型的变量readWriteCacheMap
,其中保存服务信息的缓存。同步服务到其他的Eureka Server节点
调用
PeerAwareInstanceRegistryImpl.replicateInstanceActionsToPeers
。将服务信息复制到另一个Eureka Server节点节点中。根据服务不同的操作,向Eureka Server发送不同的HTTP请求。
剔除失效服务
剔除失效服务是一个定时任务,它在
AbstractInstanceRegistry#EvictionTask
类中完成。其间隔时间在EurekaServerConfig.getEvictionIntervalTimerInMs()
中配置,默认为60s。遍历服务列表,将失效的服务实例添加到失效服务列表中(失效服务指的是超过一定时间没有再次续约的服务,该时间由
LeaseInfo.durationInSecs
控制。默认为DEFAULT_LEASE_DURATION
,即90秒),然后从中随机剔除掉一批失效服务(剔除的量由renewalPercentThreshold
控制,默认为0.85
)。
http://nobodyiam.com/2016/06/25/dive-into-eureka/
http://blog.didispace.com/spring-cloud-eureka-register-detail/
https://xli1224.github.io/2017/12/31/spring-cloud-eureka-server-analysis/
http://www.iocoder.cn/Eureka/eureka-server-init-second/
https://blog.tookbra.com/2017/08/25/Spring-Cloud-Eureka-Server-Source/index.html
http://www.iocoder.cn/Eureka/instance-registry-evict/