Eureka核心源码解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Eureka核心源码解析

Eureka作为Spring Cloud的核心模块之一,担任着服务注册发现等重要作用。本文将从以下几个方面进行Eureka的源码分析,梳理实际工作流程:

image.png

上述各个方面,基于服务的运行场景不同,可能分别从Eureka的服务端(注册中心)与客户端(包含服务提供者与服务调用者)进行分析,为了简便下文中将Eureka服务端称为Eureka-server,客户端称为Eureka-client。

image.png

在Eureka-client中,DiscoveryClient这个类用来和Eureka-server互相协作,看一下它的注释,它可以完成服务注册,服务续约,服务下线,获取服务列表等工作,可以说它完成了client的大多数功能。首先,看一下用来向eureka-server发起注册请求的register方法:

image.png

调用 AbstractJerseyEurekaHttpClient 类的register方法:

image.png

Jersey是一个Restful请求服务的框架,与常用的springmvc类似,后面会讲到在Eureka-server拦截请求的时候也用到了Jersy。

在这里调用底层类:

com.sun.jersey.api.client.Client

通过HTTP客户端发送http请求,并构建响应结果。

image.png

在Eureka-server,配置好yml文件中必需的参数后,只需要一个注解开启:

@EnableEurekaServer

查看该注解的实现方法,发现为空白注解,使用@Import:

@Import(EurekaServerMarkerConfiguration.class)

EurekaServerMarkerConfiguration类的实现中:

image.png

在这里只向spring容器中注入bean,没有任何意义。这里用到了Springboot的自动装配(这个不懂的可以看springboot零配置启动这篇文章):

image.png

发现Eureka server核心的自动配置类:EurekaServerAutoConfiguration

image.png

我们看到,在这个类上有条件注入注解:

@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)

只有在Spring容器中存在Marker这个Bean时才会实例化这个类,所以@EnableEurekaServer就相当于一个开关,起到标识的作用。

在这个配置类中定义了拦截器,同样使用Jersy拦截请求:

image.png

ApplicationResource类的addInstance方法接收请求,在对实例的信息进行验证后,向服务注册中心添加实例:

image.png

进入InstanceRegistry的register方法:

image.png

在这里做了两个功能:

1、调用handleRegistration,在方法中使用publishEvent发布了监听事件 。Spring支持事件驱动,可以监听者模式进行事件的监听,这里广播给所有监听者,收到一个服务注册的请求。

至于监听器,可以由我们自己手写实现,参数中的事件类型spring会帮我们直接注入:

@Component
public class EurekaRegisterListener {
  @EventListener
  public void registe(EurekaInstanceRegisteredEvent event){
    System.out.println(event.getInstanceInfo().getAppName());
  }
}

2、调用父类PeerAwareInstanceRegistryImpl的register方法:

image.png

① 拿到微服务的过期时间,并进行更新

② 将服务注册交给父类完成

③ 完成集群信息同步(这个会在后面说明)

调用父类AbstractInstanceRegistry的register方法,在这开始真正开始做服务注册:

先说一下在这个类中定义的Eureka-server的服务注册列表的结构:

ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry;

ConcurrentHashMap中外层的String表示服务名称;

Map中的String表示服务节点的id (也就是实例的instanceid);

Lease是一个心跳续约的对象,InstanceInfo表示实例信息。

image.png

首先,注册表根据微服务的名称或取Map,如果不存在就新建,使用putIfAbsent。

然后,从gMap(gMap就是该服务的实例列表)获取一次服务实例,判断这个微服务的节点是否存在,第一次注册的情况下一般是不存在的。

当然,也有可能会发生注册信息冲突时,这时Eureka会根据最后活跃时间来判断到底覆盖哪一个:

image.png

这段代码中,Eureka拿到存在节点的最后活跃时间,和当前注册节点的发起注册时间,进行对比。当存在的节点的最后活跃时间大于当前注册节点的时间,就说明之前存在的节点更活跃,就替换当前节点。

这里有一个思想,就是如果Eureka缓存的老节点更活跃,就说明它能够使用,而新来的服务我并不知道是否能用,那么Eureka就保守的使用了可用的老节点,从这一点也保证了可用性。

在拿到服务实例后对其进行封装:

image.png

Lease是一个心跳续约的包装类,里面存放了注册信息,最后操作时间,注册时间,过期时间,剔除时间等信息。在这里把注册实例及过期时间放到这个心跳续约对象中,再把心跳续约对象放到gmap注册表中去。之后进行改变服务状态,系统数据统计,至此一个服务注册的流程就完成了。

注册完成后,查看一下registry中的服务实例,发现我们启动的Eureka-client都已经放在里面了:

image.png

服务续约由Eureka-client端主动发起,由之前介绍过的DiscoveryClient类中的renew方法完成,主要内容仍然是发送http请求:

image.png

每隔30秒进行一次续约,调用AbstractJerseyEurekaHttpClient的sendHeartBeat方法:

image.png

在Eureka-server端,服务续约的调用链与服务注册基本相同:

InstanceRegistry # renew() ->
PeerAwareInstanceRegistry # renew()->
AbstractInstanceRegistry # renew()

主要看一下AbstractInstanceRegistry 的renew方法:

image.png

先从注册表获取该服务的实例列表(gMap),再从gMap中通过实例的id 获取具体的 要续约的实例。之后根据服务实例的InstanceStatus判断是否处于宕机状态,以及是否和之前状态相同。如果一切状态正常,最终调用Lease中的renew方法:

image.png

可以看出,其实服务续约的操作非常简单,它的本质就是修改服务的最后的更新时间。将最后更新时间改为系统当前时间加上服务的过期时间。

值得提一下的是,lastUpdateTimestamp这个变量是被volatile关键字修饰的。

image.png

之前的文章我们讲过volitaile是用来保证可见性的。那么要被谁可见呢,提前说一下,这里要被服务剔除中执行的定时任务可见,后面会具体分析。

image.png

当Eureka-server发现有的实例没有续约超过一定时间,则将该服务从注册列表剔除,该项工作由一个定时任务完成的。该任务的定义过程比较复杂,仅列出其调用过程:

EurekaServerInitializerConfiguration # start() ->
EurekaServerBootstrap # contextInitialized() ->
                      # initEurekaServerContext() ->
PeerAwareInstanceRegistryImpl # openForTraffic() ->
AbstractInstanceRegistry # postInit()

image.png

在AbstractInstanceRegistry的postInit方法中,定义EvictionTask定时任务,构建定时器启动该任务,执行任务中剔除方法 evict()。

private long evictionIntervalTimerInMs = 60 * 1000;

任务的时间被定义为60秒,即默认每分钟执行一次。

具体查看evit()剔除方法:

image.png

1、新建实例列表expiredLeases,用来存放过期的实例。

2、遍历registry注册表,对实例进行检测工作,使用isExpired方法判断实例是否过期:

image.png

解释一下各个参数的意义:

evictionTimestamp:剔除时间,当剔除节点的时候,将系统当前时间赋值给这个evictionTimestamp
additionalLeaseMs:集群同步产生的预留时间,这个时间是程序中传过来的

这里进行判断:

系统当前时间 >最后更新时间 + 过期时间 + 预留时间

当该条件成立时,认为服务过期。在Eureka中过期时间默认定义为3个心跳的时间,一个心跳是30秒,因此过期时间是90秒。

当以上两个条件之一成立时,判断该实例过期,将该过期实例放入上面创建的列表中。注意这里仅仅是将实例放入List中,并没有实际剔除。

在实际剔除任务前,需要提一下eureka的自我保护机制:

当15分钟内,心跳失败的服务大于一定比例时,会触发自我保护机制。

image.png

这个值在Eureka中被定义为85%,一旦触发自我保护机制,Eureka会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据。

image.png

参数意义:

registrySizeThreshold:根据阈值计算可以被剔除的服务数量最大值
evictionLimit:剔除后剩余最小数量
expiredLeases.size():剔除列表的数量

上面的代码中根据自我保护机制进行了判断,使用Min函数计算两者的最小值,剔除较小数量的服务实例。

举个例子,假如当前共有100个服务,那么剔除阈值为85,如果list中有60个服务,那么就会剔除该60个服务。但是如果list中有95个服务,那么只会剔除其中的85个服务,在这种情况下,又会产生一个问题,eureka-server该如何判断去剔除哪些服务,保留哪些服务呢?

image.png

这里使用了随机算法进行剔除,保证不会连续剔除某个微服务的全部实例。

最终调用internalCancel方法,实际执行剔除。

image.png

其实剔除操作的实质非常简单,就是从gMap中remove掉这个节点,并从缓存中剔除。

image.png

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

shutdown方法中调用了unregister方法:

image.png

调用AbstractJerseyEurekaHttpClient 的cancel方法:

image.png

发送http请求告诉eureka-server自己下线。

image.png

调用 AbstractInstanceRegistry 中 cancel方法:

image.png

最终还是调用了和服务剔除中一样的方法,remove掉了gMap中的实例。

image.png

在学习服务发现的源码前,先写一个测试用例:

@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/find")
public void test(String id){
    List<ServiceInstance> instances = discoveryClient.getInstances(id);
    System.out.println(instances);
}

调用DiscoveryClient 的getInstances方法,可以根据服务id获取服务实例列表:

image.png

那么这里就有一个问题了,我们还没有去调用微服务,那么服务列表是什么时候被拉取或缓存到本地的服务列表的呢?

在这里调用了CompositeDiscoveryClient 的 getInstances()方法:

image.png

中间调用过程省略:

EurekaDiscoveryClient # getInstances() ->
DiscoveryClient # getInstancesByVipAddress() ->
                # getInstancesByVipAddress() ->  //和上面不是一个方法
Applications # getInstancesByVirtualHostName()

查看Applications中的getInstancesByVirtualHostName方法:

image.png

发现一个名为virtualHostNameAppMap的Map集合中已经保存了当前所有注册到eureka的服务列表。

private final Map<String, VipIndexSupport> virtualHostNameAppMap;

也就是说,在我们没有手动去调用服务的时候,该集合里面已经有值了,说明在Eureka-server项目启动后,会自动去拉取服务,并将拉取的服务缓存起来。

那么追根溯源,来查找一下服务的发现究竟是什么时候完成的。回到DiscoveryClient这个类,在它的构造方法中定义了任务调度线程池cacheRefreshExecutor,定义完成后,调用initScheduledTask方法:

image.png

这个thread中,调用了refreshRegistry()方法:

image.png

fetchRegistry方法中,执行真正的服务列表拉取:

image.png

在fetchRegistry方法中,先判断是进行增量拉取还是全量拉取:

1、全量拉取:

当缓存为null,或里面的数据为空,或强制时,进行全量拉取,执行getAndStoreFullRegistry方法:

image.png

2、增量拉取,只拉取修改的。执行getAndUpdateDelta方法:

image.png

①②:先发送http请求,获取在eureka-server中修改或新增的集合

③:判断,若拉取的集合为null,则进行全量拉取

④:更新操作,在updateDelta方法中,根据类型进行更改

image.png

:获取一致性的hashcode值,用来校验eureka-server集合和本地是否一样

image.png

在这进行判断,若远程集合的hash值等于缓存中的hash值,不需要拉取,否则再进行拉取一次。

最后提一下,Applications中定义的以下这些变量,都是在eureka-server中准备好的,直接拉取就可以了

private final AbstractQueue<Application> applications;
private final Map<String, Application> appNameApplicationMap;
private final Map<String, VipIndexSupport> virtualHostNameAppMap;
private final Map<String, VipIndexSupport> secureVirtualHostNameAppMap;

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

服务列表的拉取并不是在服务调用的时候才拉取,而是在项目启动的时候就有定时任务去拉取了,这点在DiscoveryClient的构造方法中能够体现;

服务的实例并不是实时的Eureka-server中的数据,而是一个本地缓存的数据;

缓存更新根据实际需求分为全量拉取与增量拉取。

image.png

集群信息同步发生在Eureka-server之间,之前提到在PeerAwareInstanceRegistryImpl类中,在执行register方法注册微服务实例完成后,执行了集群信息同步方法replicateToPeers,具体分析一下该方法:

image.png

首先,遍历集群节点,用以给各个集群信息节点进行信息同步。

然后,调用replicateInstanceActionsToPeers方法,在该方法中根据具体的操作类型Action,选择分支,最终调用PeerEurekaNode的register方法:

image.png

最终发送http请求,但是与普通注册操作不同的时,这时将集群同步的标识置为true,说明注册信息是来自集群同步。

image.png

在注册过程中运行到addInstance方法时,单独注册时isReplication的值为false,集群同步时为true。通过该值,能够避免集群间出现死循环,进行循环同步的问题。

相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
71 2
|
18天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
22天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
50 12
|
1月前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
1月前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
54 3
|
2月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
63 5
|
2月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
148 5
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
76 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
62 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
66 0

推荐镜像

更多