Eureka源码分析
Eureka源码分析
通过前面的学习,我们对Eureka中各个核心元素的通信行为有了较为详细的认识,为了更加深入理解它的运作和配置,接下来将结合源码来分别看看各个通信行为是如何实现的。
在阅读源码之前,先回顾之前所实现的内容,从而找到一个合适的切入口来分析。首先对于服务注册中心、服务提供者、服务消费者这三个核心元素来说,服务提供者和服务消费者都是Eureka Client,在整个运行机制中是大部分通信行为的主动发起者,而服务注册中心主要是处理请求的接收者。因此Eureka采用的是客户端发现机制,我们可以从Eureka Client着手来分析它是如何完成这些主动通信行为的。
仔细回忆一下,当我们需要将一个普通的SpringBoot应用注册到Eureka Server中,或者是从Eureka Server中获取服务列表时都进行了哪些操作?
显然只进行了两步:第一步,在项目入口类上添加@EnableDiscoveryClient
注解;第二步,在application.yml配置文件中通过eureka.client.service-url.defaultZone
参数来配置服务注册中心的位置。
那就顺着这个思路来阅读源码,首先查看@EnableDiscoveryClient
注解的源码:
1 | @Target({ElementType.TYPE}) |
从该注解的注释中可以知道,它主要用来开启DiscoveryClient的实例。搜索一下DiscoveryClient类,可以发现有一个类和一个接口:
既然有一个同名的接口和类,首先分析同名的接口,可以查看该接口的实现类,可以看到该接口一共有4个实现类:
里面有一个EurekaDiscoveryClient类引起了关注,查看一下该类的信息:
顺便查看一下该类的继承关系:
可以看到此处的EurekaDiscoveryClient类存在于org.springframework.cloud.netflix.eureka
包中,同时发现该类实现了前面的DiscoveryClient接口,其实从名字也可以判断出它是对Eureka发现服务的封装。
聪明的你可能已经发现前面同名的DiscoveryClient接口和实现类是存在于不同的包中。其中DiscoveryClient接口存在于org.springframework.cloud.client.discovery
包内,它是SpringCloud的接口,它定义了用来发现服务的常用抽象方法:
1 | public interface DiscoveryClient extends Ordered { |
是否注意到里面的getInstances方法,在前面我们使用它来获取服务提供者的信息:
1 | @RestController |
由于这里只是一个接口,而不是一个类,因此通过该接口可以有效的屏蔽服务治理的实现细节,因此使用SpringCloud构建的微服务应用可以很方便地切换服务治理框架,而不用修改程序代码,只需要另外添加一些针对服务治理框架的配置即可。
再来分析同名的DiscoveryClient实现类,该类存在于com.netflix.discovery
包内,同时实现了EurekaClient接口,而EurekaClient接口则继承了LookupService接口,它们都是Netflix开源包中的内容,主要定义了针对Eureka的发现服务的抽象方法,而真正实现发现服务的则是Netflix包中的com.netflix.discovery.DiscoveryClient
类,也就是这里所说同名的DiscoveryClient实现类。
可以查看一下DiscoveryClient类的继承信息:
既然这个同名的DiscoveryClient实现类是真正实现发现服务的类,那么接下来就好好分析该类的源码。
首先分析该类头部的注释,注释的内容大致为:(1)该类用于帮助与Eureka Server互相协作;(2)Eureka Client主要负责以下任务:向Eureka Server注册服务实例;向Eureka Server服务租约;当服务关闭期间,向Eureka Server取消租约;查询Eureka Server中的服务实例列表。(3)Eureka Client还需要配置一个Eureka Server的URL列表。
在具体研究Eureka Client负责完成的任务之前,先看看在哪里对Eureka Server的URL列表进行配置。其实这个配置就是eureka.client.service-url.defaultZone
,然后通过serviceUrl就可以找到该属性相关的加载属性,但是在SR5版本中它们都被@Deprecated
注解标注为不再建议使用,并使用@link
到了替代类com.netflix.discovery.endpoint.EndpointUtils
,因此我们可以在EndpointUtils类中找到那个获取serviceUrl的方法:
1 | public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { |
Region和Zone
在上述getServiceUrlsMapFromConfig方法中,可以发现客户端依次加载了两个内容:第一个是Region,第二个是Zone,其实从加载逻辑上我们就能判断出两者之间的关系。
首先调用getRegion函数,里面传入一个EurekaClientConfig对象,然后返回一个字符串类型的region,因此一个微服务应用只可以属于一个Region,如果不特别配置,默认为default。查看一下该getRegion方法的源码,如下所示:
1 | public static String getRegion(EurekaClientConfig clientConfig) { |
如果开发者想要自定义设置,可以通过在application.yml配置文件中新增如下配置来实现:
1 | eureka: |
注意这个us-east-1是默认的region名称。
之后通过EurekaClientConfig#getAvailabilityZones
方法来得到字符串类型数组的availZones对象,查看一下getAvailabilityZones方法(请注意这个方法存在于EurekaClientConfig接口另一个实现类EurekaClientConfigBean中)的源码,如下所示:
1 | public String[] getAvailabilityZones(String region) { |
因此从上面分析可知,当我们没有特别为Region配置Zone的时候,系统默认使用defaultZone,其实这也是我们之前在application.yml配置文件中新增如下配置的原因:
1 | eureka: |
你也看到了这个service-url其实是一个Map对象,而这个defaultZone则是键,后面的值其实是一个字符串类型的数组。其实从getAvailabilityZones方法的返回值可以知道Zone能够设置多个,并且通过逗号分隔来配置。如果开发者想要为应用指定Zone,可以通过添加如下配置来实现:
1 | eureka: |
通过上面的分析,大家就能知道Region和Zone之间是一对多的关系。
serviceUrls
在获取了Region和Zone的信息之后,才真正开始加载Eureka Server的具体地址。继续阅读之前getServiceUrlsMapFromConfig方法的源码,里面有这六行代码:
1 | int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); |
可以看到这个getZoneOffset方法可以根据传入的参数按照一定的算法确定加载位于哪一个Zone配置的serviceUrls。
之后调用EurekaClientConfig
对象的getEurekaServerServiceUrls方法来获取字符串类型的serviceUrls,同样可以知道这个getEurekaServerServiceUrls方法存在于EurekaClientConfig接口中,那么依然可以查看该接口的另一个实现类EurekaClientConfigBean,可以看到这是一个配置类,用来加载配置文件中的内容:
这里面有很多信息,此处先介绍我们比较关系的defaultZone,可以发现EurekaClientConfigBean类无参的构造方法会自动将defaultZone添加入之前所说的serviceUrl这个map集合中:
还记得之前所说的那个getEurekaServerServiceUrls方法么,这个EurekaClientConfigBean类中有对该方法的完整实现逻辑:
1 | public List<String> getEurekaServerServiceUrls(String myZone) { |
通过分析该源码可以知道该函数用于对传入的Zone进行拆分,也是就说开发者可以通过在application.yml配置文件中新增如下配置来实现配置多个Zone的目的,需要注意的是多个Zone之间需要通过逗号来进行分隔:
1 | eureka: |
当我们在微服务应用中使用Ribbon来实现服务调用时,对于Zone的设置可以在负载均衡时实现区域亲和特性。所谓的区域亲和特性是指:Ribbon的默认策略会优先访问同客户端处于一个Zone中的服务端实例,只有当同一个Zone中没有可用的服务端实例的时候才会访问其他Zone中的实例。所以开发者可以通过Zone属性的定义,配合实际部署的物理结构,就能有效的设计出对区域性故障的容错集群。