Eureka源码分析

通过前面的学习,我们对Eureka中各个核心元素的通信行为有了较为详细的认识,为了更加深入理解它的运作和配置,接下来将结合源码来分别看看各个通信行为是如何实现的。

在阅读源码之前,先回顾之前所实现的内容,从而找到一个合适的切入口来分析。首先对于服务注册中心、服务提供者、服务消费者这三个核心元素来说,服务提供者和服务消费者都是Eureka Client,在整个运行机制中是大部分通信行为的主动发起者,而服务注册中心主要是处理请求的接收者。因此Eureka采用的是客户端发现机制,我们可以从Eureka Client着手来分析它是如何完成这些主动通信行为的。

仔细回忆一下,当我们需要将一个普通的SpringBoot应用注册到Eureka Server中,或者是从Eureka Server中获取服务列表时都进行了哪些操作?
显然只进行了两步:第一步,在项目入口类上添加@EnableDiscoveryClient注解;第二步,在application.yml配置文件中通过eureka.client.service-url.defaultZone参数来配置服务注册中心的位置。

那就顺着这个思路来阅读源码,首先查看@EnableDiscoveryClient注解的源码:

1
2
3
4
5
6
7
8
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
boolean autoRegister() default true;
}

从该注解的注释中可以知道,它主要用来开启DiscoveryClient的实例。搜索一下DiscoveryClient类,可以发现有一个类和一个接口:

既然有一个同名的接口和类,首先分析同名的接口,可以查看该接口的实现类,可以看到该接口一共有4个实现类:

里面有一个EurekaDiscoveryClient类引起了关注,查看一下该类的信息:

顺便查看一下该类的继承关系:

可以看到此处的EurekaDiscoveryClient类存在于org.springframework.cloud.netflix.eureka包中,同时发现该类实现了前面的DiscoveryClient接口,其实从名字也可以判断出它是对Eureka发现服务的封装。


聪明的你可能已经发现前面同名的DiscoveryClient接口和实现类是存在于不同的包中。其中DiscoveryClient接口存在于org.springframework.cloud.client.discovery包内,它是SpringCloud的接口,它定义了用来发现服务的常用抽象方法:

1
2
3
4
5
6
7
8
9
10
11
12
public interface DiscoveryClient extends Ordered {
int DEFAULT_ORDER = 0;

String description();
List<ServiceInstance> getInstances(String serviceId);

List<String> getServices();

default int getOrder() {
return 0;
}
}

是否注意到里面的getInstances方法,在前面我们使用它来获取服务提供者的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class HelloController {
private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

@Autowired
private DiscoveryClient client;

@GetMapping(value = "/hello")
public String index(){
List<ServiceInstance> instances = client.getInstances("hello-service");
for(int i=0;i<instances.size();i++){
logger.info("/hello,host: "+instances.get(i).getHost()+
",service_id: "+instances.get(i).getServiceId());
}
return "hello,world!";
}
}

由于这里只是一个接口,而不是一个类,因此通过该接口可以有效的屏蔽服务治理的实现细节,因此使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
Map<String, List<String>> orderedUrls = new LinkedHashMap();
String region = getRegion(clientConfig);
String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
if (availZones == null || availZones.length == 0) {
availZones = new String[]{"default"};
}

logger.debug("The availability zone for the given region {} are {}", region, availZones);
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
String zone = availZones[myZoneOffset];
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
if (serviceUrls != null) {
orderedUrls.put(zone, serviceUrls);
}

int currentOffset = myZoneOffset == availZones.length - 1 ? 0 : myZoneOffset + 1;

while(currentOffset != myZoneOffset) {
zone = availZones[currentOffset];
serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
if (serviceUrls != null) {
orderedUrls.put(zone, serviceUrls);
}

if (currentOffset == availZones.length - 1) {
currentOffset = 0;
} else {
++currentOffset;
}
}

if (orderedUrls.size() < 1) {
throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
} else {
return orderedUrls;
}
}
Region和Zone

在上述getServiceUrlsMapFromConfig方法中,可以发现客户端依次加载了两个内容:第一个是Region,第二个是Zone,其实从加载逻辑上我们就能判断出两者之间的关系。

首先调用getRegion函数,里面传入一个EurekaClientConfig对象,然后返回一个字符串类型的region,因此一个微服务应用只可以属于一个Region,如果不特别配置,默认为default。查看一下该getRegion方法的源码,如下所示:

1
2
3
4
5
6
7
8
9
public static String getRegion(EurekaClientConfig clientConfig) {
String region = clientConfig.getRegion();
if (region == null) {
region = "default";
}

region = region.trim().toLowerCase();
return region;
}

如果开发者想要自定义设置,可以通过在application.yml配置文件中新增如下配置来实现:

1
2
3
eureka:
client:
region: us-east-1

注意这个us-east-1是默认的region名称。

之后通过EurekaClientConfig#getAvailabilityZones方法来得到字符串类型数组的availZones对象,查看一下getAvailabilityZones方法(请注意这个方法存在于EurekaClientConfig接口另一个实现类EurekaClientConfigBean中)的源码,如下所示:

1
2
3
4
5
6
7
8
public String[] getAvailabilityZones(String region) {
String value = (String)this.availabilityZones.get(region);
if (value == null) {
value = "defaultZone";
}

return value.split(",");
}

因此从上面分析可知,当我们没有特别为Region配置Zone的时候,系统默认使用defaultZone,其实这也是我们之前在application.yml配置文件中新增如下配置的原因:

1
2
3
4
eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/

你也看到了这个service-url其实是一个Map对象,而这个defaultZone则是键,后面的值其实是一个字符串类型的数组。其实从getAvailabilityZones方法的返回值可以知道Zone能够设置多个,并且通过逗号分隔来配置。如果开发者想要为应用指定Zone,可以通过添加如下配置来实现:

1
2
3
eureka:
client:
availability-zones:

通过上面的分析,大家就能知道Region和Zone之间是一对多的关系。

serviceUrls

在获取了Region和Zone的信息之后,才真正开始加载Eureka Server的具体地址。继续阅读之前getServiceUrlsMapFromConfig方法的源码,里面有这六行代码:

1
2
3
4
5
6
int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
String zone = availZones[myZoneOffset];
List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
if (serviceUrls != null) {
orderedUrls.put(zone, serviceUrls);
}

可以看到这个getZoneOffset方法可以根据传入的参数按照一定的算法确定加载位于哪一个Zone配置的serviceUrls。

之后调用EurekaClientConfig对象的getEurekaServerServiceUrls方法来获取字符串类型的serviceUrls,同样可以知道这个getEurekaServerServiceUrls方法存在于EurekaClientConfig接口中,那么依然可以查看该接口的另一个实现类EurekaClientConfigBean,可以看到这是一个配置类,用来加载配置文件中的内容:

这里面有很多信息,此处先介绍我们比较关系的defaultZone,可以发现EurekaClientConfigBean类无参的构造方法会自动将defaultZone添加入之前所说的serviceUrl这个map集合中:

还记得之前所说的那个getEurekaServerServiceUrls方法么,这个EurekaClientConfigBean类中有对该方法的完整实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public List<String> getEurekaServerServiceUrls(String myZone) {
String serviceUrls = (String)this.serviceUrl.get(myZone);
if (serviceUrls == null || serviceUrls.isEmpty()) {
serviceUrls = (String)this.serviceUrl.get("defaultZone");
}
if (!StringUtils.isEmpty(serviceUrls)) {
String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
List<String> eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);
String[] var5 = serviceUrlsSplit;
int var6 = serviceUrlsSplit.length;
for(int var7 = 0; var7 < var6; ++var7) {
String eurekaServiceUrl = var5[var7];
if (!this.endsWithSlash(eurekaServiceUrl)) {
eurekaServiceUrl = eurekaServiceUrl + "/";
}
eurekaServiceUrls.add(eurekaServiceUrl.trim());
}
return eurekaServiceUrls;
} else {
return new ArrayList();
}
}

通过分析该源码可以知道该函数用于对传入的Zone进行拆分,也是就说开发者可以通过在application.yml配置文件中新增如下配置来实现配置多个Zone的目的,需要注意的是多个Zone之间需要通过逗号来进行分隔:

1
2
3
4
eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/,http://peer2:1112/eureka/

当我们在微服务应用中使用Ribbon来实现服务调用时,对于Zone的设置可以在负载均衡时实现区域亲和特性。所谓的区域亲和特性是指:Ribbon的默认策略会优先访问同客户端处于一个Zone中的服务端实例,只有当同一个Zone中没有可用的服务端实例的时候才会访问其他Zone中的实例。所以开发者可以通过Zone属性的定义,配合实际部署的物理结构,就能有效的设计出对区域性故障的容错集群。