1、Eureka Server服务端会做什么
1、服务注册
Client服务提供者可以向Server注册服务,并且内部有二层缓存机制来维护整个注册表,注册表是Eureka Client的服务提供者注册进来的。
2、提供注册表
服务消费者用来获取注册表
3、同步状态
通过注册、心跳机制和 Eureka Server同步当前客户端的状态,这里就包括服务提供者和服务消费者。
2、Eureka Server的问题
问题:
1、Eureka Server的自我保护机制是怎么实现,怎么做到15分钟内,服务心跳失败比例高于85%。
2、自我保护机制触发后,有哪些功能会被开启
1、不再从注册列表中移除因为长时间没收到心跳而应该过期的服务,冷却时间是多久?
2、仍然能够接受新服务的注册和查询注册表请求,但是不会被同步到其它节点上
3、当网络稳定时,当前实例新的注册信息会被同步到其它节点中,怎么判断网络恢复
集群问题:
Eureka Server集群中,Eureka Server节点A和Eureka Server节点B是怎么通过P2P的方式完成服务注册表的同步?
Eureka Server集群中,同一个区域的Eureka Client,怎么做到优先和同区域内的Eureka Server进行通信的?
Eureka Client服务提供者,向Eureka Server注册,如果某个节点失败,自动切换到其他节点,是怎么做到的?
Eureka Server什么时候会自动退出自我保护模式?
二、源码概述
1、EurekaServer启动
java
@EnableEurekaServer->
import(EurekaServerMarkerConfiguration)->
注册Bean(EurekaServerMarkerConfiguration.Marker)->
Marker激活了EurekaServerAutoConfiguration这个配置类
2、EurekaServerAutoConfiguration主要包含以下内容
1、创建Bean:【EurekaServerConfigBean】是一个配置类,EurekaServer的所有配置项都是EurekaServerConfigBean这个类里面。
2、创建Bean:【EurekaController】也就是我们通过url,可以访问EurekaServer后台。
3、创建Bean:【PeerAwareInstanceRegistry】处理注册表的类,这个类也会发布事件,发布了注册事件和取消事件(默认没有监听者需要自己实现)
4、创建Bean:【PeerEurekaNodes】初始化了集群节点集合
5、创建Bean:【EurekaServerContext】专业名称叫【EurekaServer上下文】,这个Bean的生成是基于上面创建的Bean: eureka server配置,注册表,集群节点集合来生成。而EurekaServerContext的作用就是初始化eurekaServer上下文,里面会做很多事情。
6、创建Bean:【EurekaServerBootstrap】Eureka Server的启动类
7、创建Bean:【FilterRegistrationBean】主要是对Jersey过滤器的包装,那么这个过滤器干嘛用的 ?
到此EurekaServerAutoConfiguration的创建Bean的任务完成了,但是EurekaServerAutoConfiguration里面还有一@Import(EurekaServerInitializerConfiguration)
注解
3、EurekaServerInitializerConfiguration
EurekaServerInitializerConfiguration里面有个start方法,里面会拿到上面注册Bean:【EurekaServerBootstrap启动类】,来启动Eureak。
然后在通过生成的【EurekaServer上下文】开始初始化,初始化的时候会调用registry.syncUp方法,从相邻的eureka节点复制注册表,通过http调用相邻节点获取所有服务实例。
在通过上面的【PeerAwareInstanceRegistry】把实例注册到本地,这里的实例是指EurekaClient的服务提供者,同时PeerAwareInstanceRegistry里面还有一个【Timer】,这个是定时任务,清理30s没有续约的任务、服务剔除超过90s没过来续约的服务。
原文地址:跳转
三、下面通过代码原理说下实现逻辑
1、服务注册
2、服务续约
3、服务剔除
4、服务下线
5、服务发现
6、集群信息同步
1、服务注册
流程:服务提供者,请求EurekaServer端某个节点注册服务
EurekaClient端
java
Bean =【EurekaAutoServiceRegistration】
通过com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#register向EurekaServer注册服务。
EurekaServer端
java
EurekaServer收到请求,请求进到ApplicationResource#addInstance方法,里面调用【Bean=PeerAwareInstanceRegistry】#register方法,
里面先发布一个Event事件,但是Spring没有监听这个事件,这个是留给我们自己拓展用的,在register里面还有一个很重要的事做,就是注册:
1、首先是设置服务的过期时间90s
2、调用父类完成服务注册,
3、在完成集群信息同步,同步给其他节点。
重点看调用【父类完成注册】,先从注册表的集合中获取服务注册信息:
1、如果注册表存在,那么说明冲突了,就判断哪2个节点的活跃时间比较靠前,保留节点时间最新的节点
2、如果不存在就新建,将EurekaClient的服务提供者封装成InstanceInfo对象, InstanceInfo存放了注册信息,最后操作时间,注册时间,过期时间,
剔除时间等信息,再把这个InstanceInfo对象存到注册表中去。至此一个服务注册的流程就完成了
注意:注册表是一个Map<String, Map<String, Lease<InstanceInfo>>>对象
1、最外层的key是AppName,注册进来的服务的服务名 , value = Map<String, Lease<InstanceInfo>> value表示这个服务名对应多个实例节点
2、Map<String, Lease<InstanceInfo>>这个Map,key是ip+端口,value是Lease<InstanceInfo>对象,也就是这个实例的更多信息,比如过期时间,最近活跃时间等等。
2、服务续约:
服务续约由Eureka-client端主动发起请求Eureka服务端,间隔时间30s,Eureka服务端收到请求,刷新节点的活跃时间。因为Eureka服务端,有定时任务,就是基于这个活跃时间来考虑是否剔除服务。
Eureka-client端
java
请求Eureka服务端,由DiscoveryClient#renew方法完成,主要是发送http请求,每隔30秒进行一次续约,
里面调用AbstractJerseyEurekaHttpClient#sendHeartBeat方法
Eureka-server端
java
在Eureka-server端服务续约的调用链与服务注册基本相同
InstanceRegistry#renew() ->
PeerAwareInstanceRegistry#renew()->
AbstractInstanceRegistry # renew() 主要逻辑还是AbstractInstanceRegistry的renew方法
renew的方法逻辑操作非常简单,它的本质就是修改服务的【最后更新时间】。将最后更新时间改为:【系统当前时间】+【服务的过期时间】
3、服务剔除
服务主要是Eureka服务端,通过定时任务检测注册节点的【活跃时间】,如果超过90s就会剔除。
Eureka-server端
java
当Eureka-server发现有的实例没有续约超过一定时间,则将该服务从注册列表剔除,该项工作由一个定时任务完成的。由下面方法完成
AbstractInstanceRegistry # postInit()
定时任务前面说了在【EurekaServer上下文】初始化的时候,添加了一个Timer定时器,定时器关联的任务是EvictionTask的run方法,在执行任务中
调用剔除方法 evict(), 主要是拿到注册表的所有实例挨个遍历,判断【系统当前时间 > 最后更新时间+过期时间+预留时间】,
并且新建实例列表expiredLeases,用来存放过期的实例。
当该条件成立时,认为服务过期(在Eureka中过期时间默认定义为3个心跳的时间,一个心跳是30秒,因此过期时间是90秒)。
将该过期实例放入上面创建的expiredLeases列表中。注意这里仅仅是将实例放入List中,并没有实际剔除。因为要判断是否超过阈值了,如果超过就从里面取随机数,随机剔除实例ID,注意expiredLeases里面存的是多个服务的实例,不是某一个服务的所有实例。下线的时候从里面取随机数,所以有可能某个服务的所有实例全部被剔除都有可能。
在实际剔除任务前,需要提一下eureka的自我保护机制:
当1分钟内,心跳失败的服务大于一定比例时,会触发自我保护机制。这个值在Eureka中被定义为85%,一旦触发自我保护机制,
Eureka会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据。1分钟是怎么统计数量的?哪些节点保留? 哪些节点删除?
答:使用了随机算法进行剔除,
举个例子,假如当前共有100个服务,那么剔除阈值为85%,也就是最多剔除15个,如果list中有60个服务,那么就会从60个服务里面取15个。有可能一个服务的所有节点全部被剔除。剔除的节点被放到一个queue里面,这个里面存的是最近剔除的节点,在集群同步,或者拉取注册表的时候,要用到。
关于自我保护
java
首先是阈值是85%,比如100个,阈值是85%,那么一次最多剔除15个,当定时任务进来,发现100个里面有10个失效,那么10小于【最大阈值】,那就剔除10个,如果20个失效,20个大于15,所以最多剔除15个,这15个怎么选?
通过for循环15次,每次生成一个随机数,这个随机数是从20里面取,
1、自我保护时期不能进行服务剔除操作
2、过期操作是分批进行
3、服务剔除是随机逐个剔除,均匀分布在所有应用中,其实也不算均匀,是随机抽
4、服务剔除是一个定时任务,默认60秒一次
问题:自我保护时期不能进行服务剔除操作:这个是怎么做到的?
首先是定义了2个变量,一个是【期望续约数】,一个是【前一分钟实际的续约数】。
这个【期望续约数】是通过公式算出来了,比如20个实例,正常情况下1分钟的话会续约40次,
那么期望的续约数应该是40*85%=34个,而如果实际契约数超过这个数量,比如35,
那么EurekaServer认为,服务恢复正常了,应该关闭自我保护机制。
注意:期望续约数是一个动态值,每次会重行计算的。比如服务下线或者上线,期望的数量是会加1或者减1的。
问题:实际续约数是怎么算出来的?
因为剔除的定时任务是1分钟一次,所以有个定时任务专门设置【前一分钟实际续约数量】,MeasuredRate也是60秒一次,他里面定义了2个变量,一个是【一分钟内的续约】数,一个是【上一分钟的续约数】,服务每次注册就会加1,服务下线就减1,当定时任务跑的时候,就会把一分钟的续约数赋值给【上一分钟的续约数】,然后再把【一分钟内的续约】置0
自我保护机制,详细解读:跳转
代码流程:跳转
4、服务下线
当eureka-client关闭时,不会立刻关闭,需要先发请求给eureka-server服务端,告知自己要下线了。
Eureka-client端:
java
Eureka客户端请求EurekaServer服务端,通过DiscoveryClient#shutdown方法调用EurekaServer服务端
Eureka-server端
java
收到请求,进到AbstractInstanceRegistry#cancel方法, 最终还是调用了和服务剔除中一样的方法,remove掉了注册表中的实例
5、服务发现
是指EurekaClient 消费者,通过Http调用EurekaServer服务端接口,获取注册表信息
Eureka-client端:
DiscoveryClient#getInstances方法,可以根据服务id获取服务实例列表。那么这里就有一个问题了,我们还没有去调用微服务,那么服务列表是什么时候被拉取或缓存到本地的服务列表的呢?
java
EurekaDiscoveryClient # getInstances() ->
DiscoveryClient # getInstancesByVipAddress() ->
DiscoveryClient #getInstancesByVipAddress2() ->
Applications # getInstancesByVirtualHostName() 这里居然不是走的http,是读的本地缓存。
Applications中的getInstancesByVirtualHostName方法里面,有一个virtualHostNameAppMap的Map集合中已经保存了当前所有注册到eureka的服务列表。
private final Map<String, VipIndexSupport> virtualHostNameAppMap;
也就是说,在我们没有手动去调用服务的时候,该集合里面已经有值了,说明在Eureka-server项目启动后,会自动去拉取服务,并将拉取的服务缓存起来。
那么追根溯源,来查找一下服务的发现究竟是什么时候完成的。回到DiscoveryClient这个类,
在它的构造方法中定义了任务调度线程池cacheRefreshExecutor,定义完成后,调用initScheduledTask方法,
通过fetchRegistry方法来拉取,不过分2种情况【增量拉取】还是【全量拉取】
【全量拉取】:当缓存为null,或里面的数据为空,或强制时,进行全量拉取,执行getAndStoreFullRegistry方法
【增量拉取】: 只拉取修改的。执行getAndUpdateDelta方法,虽然这里是拉增量,但是如果没拉到数据,
还是会拉全量的数据,然后就是更新操作,更新也有类型,是delete还是Modify,added,这里有个细节校验,
就是拿hashCode和缓存的HashCode对比是否一致,如果一致,说明数据没有变动,如果不一致,
那就说明本地和远程数据不一样,需要重新再拉一次,
对服务发现过程进行一下重点总结:
1、服务列表的拉取并不是在服务调用的时候才拉取,而是在项目启动的时候就有定时任务去拉取了,这点在DiscoveryClient的构造方法中能够体现;
2、服务的实例并不是实时的Eureka-server中的数据,而是一个本地缓存的数据;
3、缓存更新根据实际需求分为全量拉取与增量拉取。
6、集群信息同步
Eureka-server端:
java
集群信息同步发生在Eureka-server之间,之前提到在PeerAwareInstanceRegistryImpl类中,在执行register方法注册微服务实例完成后,
执行了集群信息同步方法replicateToPeers
首先,遍历集群节点,用以给各个集群信息节点进行信息同步。最终发送http请求,请求各个EureakServer节点。
调用EurekaServer的ApplicationResource类里面的addInstance,注意EurekaClient注册的时候,也是调的这个方法,
单独注册时isReplication的值为false,集群同步时为true
Eureka三级缓存
服务端的缓存机制
服务端采用三级缓存(registry,readWriteCacheMap,readOnlyCacheMap)来存储注册表信息。
三级缓存的目的是为了将注册服务和获取服务区分开,避免了高并发的同时对一个缓存的读写操作,有效避免读写冲突。保证性能。
一级缓存 = ConcurrentHashMap<Key,Value> registry 服务一开始注册进来的地方
二级缓存 = Loading<Key,Value> readWriteCacheMap,本质上是guava的缓存,包含失效机制,保存服务信息的对外输出数据结构。
三级缓存 = ConcurrentHashMap<Key,Value> readOnlyCacheMap 本质上是HashMap,无过期时间,保存服务信息的对外输出数据结构。
java
设置缓存
(1)、客户端将服务信息注册在一级缓存registry中。(每30s一次心跳续约)
(2)、一级缓存registry收到注册信息后,先清空二级缓存readWriteCacheMap中的注册信息,然后在同步新数据给readWriteCacheMap二级缓存。
(3)、二级缓存按照30s一次的频率给三级缓存readOnlyCacheMap同步数据
缓存获取
(4)、其他的客户端连接注册中心Server 30s一次的频率从三级缓存readOnlyCacheMap中获取,如果readOnlyCacheMap中获取不到,则直接去一级缓存registry中获取。
缓存更新
(5)、一级缓存中默认每隔60s检查服务续期,如果90秒内服务还没有续期,则删除注册信息。同时同步给二级三级缓存。
(6)、服务下线时,一级缓存registry中的注册信息删除,同时删除二级缓存的数据。30s后二级同步三级缓存时发现二级缓存已失效,则删除三级缓存的注册表信息。则会期间会有时间的延迟。
(7)、二级缓存的默认有效期是180s(3min),3min后数据会失效,然后二级缓存数据清空。
三级缓存的弊端:
三级缓存的问题很明显,就是服务下线之后,不能及时通知到三级缓存中,注册信息的获取者(客户端)拿到的注册信息不是实时的。(当让客户端的获取也不是实时的,要间隔30s才会去主动获取)