• Eureka处理流程


    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启动

    @EnableEurekaServer->
    	import(EurekaServerMarkerConfiguration)->
    		注册Bean(EurekaServerMarkerConfiguration.Marker)->
    			Marker激活了EurekaServerAutoConfiguration这个配置类
    
    • 1
    • 2
    • 3
    • 4

    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端

    Bean =EurekaAutoServiceRegistration】
    通过com.netflix.discovery.shared.transport.jersey.AbstractJerseyEurekaHttpClient#register向EurekaServer注册服务。
    
    • 1
    • 2

    EurekaServer端

    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表示这个服务名对应多个实例节点
    2Map<String, Lease<InstanceInfo>>这个Map,key是ip+端口,value是Lease<InstanceInfo>对象,也就是这个实例的更多信息,比如过期时间,最近活跃时间等等。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2、服务续约:

    服务续约由Eureka-client端主动发起请求Eureka服务端,间隔时间30s,Eureka服务端收到请求,刷新节点的活跃时间。因为Eureka服务端,有定时任务,就是基于这个活跃时间来考虑是否剔除服务。

    Eureka-client端

    请求Eureka服务端,由DiscoveryClient#renew方法完成,主要是发送http请求,每隔30秒进行一次续约,
    里面调用AbstractJerseyEurekaHttpClient#sendHeartBeat方法
    
    • 1
    • 2

    Eureka-server端

    Eureka-server端服务续约的调用链与服务注册基本相同
    InstanceRegistry#renew() -> 
    	PeerAwareInstanceRegistry#renew()-> 
    		AbstractInstanceRegistry # renew() 主要逻辑还是AbstractInstanceRegistry的renew方法
    
    renew的方法逻辑操作非常简单,它的本质就是修改服务的【最后更新时间】。将最后更新时间改为:【系统当前时间】+【服务的过期时间】
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3、服务剔除

    服务主要是Eureka服务端,通过定时任务检测注册节点的【活跃时间】,如果超过90s就会剔除。

    Eureka-server端

    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里面,
    这个里面存的是最近剔除的节点,在集群同步,或者拉取注册表的时候,要用到。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    关于自我保护

    首先是阈值是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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    自我保护机制,详细解读:跳转
    代码流程:跳转

    4、服务下线

    当eureka-client关闭时,不会立刻关闭,需要先发请求给eureka-server服务端,告知自己要下线了。

    Eureka-client端:

    Eureka客户端请求EurekaServer服务端,通过DiscoveryClient#shutdown方法调用EurekaServer服务端
    
    • 1

    Eureka-server端

    收到请求,进到AbstractInstanceRegistry#cancel方法, 最终还是调用了和服务剔除中一样的方法,remove掉了注册表中的实例
    
    • 1

    5、服务发现

    是指EurekaClient 消费者,通过Http调用EurekaServer服务端接口,获取注册表信息

    Eureka-client端:

    DiscoveryClient#getInstances方法,可以根据服务id获取服务实例列表。那么这里就有一个问题了,我们还没有去调用微服务,那么服务列表是什么时候被拉取或缓存到本地的服务列表的呢?

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    对服务发现过程进行一下重点总结:

    1、服务列表的拉取并不是在服务调用的时候才拉取,而是在项目启动的时候就有定时任务去拉取了,这点在DiscoveryClient的构造方法中能够体现;
    2、服务的实例并不是实时的Eureka-server中的数据,而是一个本地缓存的数据;
    3、缓存更新根据实际需求分为全量拉取与增量拉取。

    6、集群信息同步

    Eureka-server端:

    集群信息同步发生在Eureka-server之间,之前提到在PeerAwareInstanceRegistryImpl类中,在执行register方法注册微服务实例完成后,
    执行了集群信息同步方法replicateToPeers
    
    首先,遍历集群节点,用以给各个集群信息节点进行信息同步。最终发送http请求,请求各个EureakServer节点。
    调用EurekaServerApplicationResource类里面的addInstance,注意EurekaClient注册的时候,也是调的这个方法,
    单独注册时isReplication的值为false,集群同步时为true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Eureka三级缓存

    服务端的缓存机制

    服务端采用三级缓存(registry,readWriteCacheMap,readOnlyCacheMap)来存储注册表信息。

    三级缓存的目的是为了将注册服务和获取服务区分开,避免了高并发的同时对一个缓存的读写操作,有效避免读写冲突。保证性能。

    一级缓存 = ConcurrentHashMap registry 服务一开始注册进来的地方
    二级缓存 = Loading readWriteCacheMap,本质上是guava的缓存,包含失效机制,保存服务信息的对外输出数据结构。
    三级缓存 = ConcurrentHashMap readOnlyCacheMap 本质上是HashMap,无过期时间,保存服务信息的对外输出数据结构。

    设置缓存
    (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后数据会失效,然后二级缓存数据清空。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    三级缓存的弊端:

    三级缓存的问题很明显,就是服务下线之后,不能及时通知到三级缓存中,注册信息的获取者(客户端)拿到的注册信息不是实时的。(当让客户端的获取也不是实时的,要间隔30s才会去主动获取)

  • 相关阅读:
    年薪40w的高级网络工程师面试过程,面试过程不难,但你要真做过才能回答上来
    操作系统4小时速成:I/O管理,spooling脱机技术,设备分配,分配数据结构,IO调度,IO子系统,IO控制方式,IO分类
    如何对测试进行划分(一)
    ES6 let与const
    C++ 函数
    记一次语音播报功能
    Python jupyter notebook Katex|Latex
    【实战】SpringBoot对接外部API并一步一步性能调优
    C#知识|账号管理系统:多条件动态查询条件的编写。
    nginx知识点
  • 原文地址:https://blog.csdn.net/weixin_37862824/article/details/134191480