高可用注册中心

高可用的必要性

在微服务架构这样的分布式环境中,开发者需要充分考虑故障发生的情况,所以在生产环境中必须对各个组件进行高可用部署,对于微服务如此,对于服务注册中心也一样。在前面咋们使用的都是单节点的服务注册中心,这在生产环境中显然是不合适的,因此我们需要构建高可用的服务注册中心以增强系统的可用性。

Eureka Server从设计时就考虑了高可用的问题,在Eureka的服务治理设计中,所有的节点既是服务提供方,也是服务消费方,注册中心也不例外。在前面搭建单节点的配置中,我们设置过下面两个参数,用于禁止服务注册中心自己注册自己:

1
2
3
4
eureka:
client:
register-with-eureka: false
fetch-registry: false

Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,进而达到高可用的效果。

接下来尝试搭建高可用的服务注册中心集群,这里选择在前面实现的单节点服务注册中心的基础上进行扩展,使之变成一个双节点的服务注册中心集群。

双节点服务注册中心实现

第一步,修改eureka-server服务注册中心的配置文件。修改eureka-server服务注册中心的application.yml配置文件为如下所示:

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
# 双节点的Eureka Server
spring:
application:
name: eureka-server
---
# peer1节点向peer2节点注册
spring:
profiles: peer1

eureka:
instance:
hostname: peer1
client:
service-url:
defaultZone: http://peer2:1112/eureka/
register-with-eureka: false
server:
enable-self-preservation: false

server:
port: 1111
---
# peer2节点向peer1节点注册
spring:
profiles: peer2

eureka:
instance:
hostname: peer2
client:
service-url:
defaultZone: http://peer1:1111/eureka/
register-with-eureka: false
server:
enable-self-preservation: false

server:
port: 1112

第二步,修改hosts文件。开发者需要修改windows系统下的hosts文件,在该文件中添加对peer1和peer2的转换,让上面配置的host形式的serviceUrl能在本地正确访问到。该文件所在路径为C:\Windows\System32\drivers\etc,往里面新增如下配置:

1
127.0.0.1 peer1 peer2

第三步,将项目打包成jar包。打包命令为mvn clean package,之后在target目录下生产一个jar包,然后使用如下命令来启动这两个节点:

1
2
start /min java -jar target/eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
start /min java -jar target/eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2

第四步,访问链接。之后访问peer1的注册中心http://peer1:1111/如下图所示,可以看到DS Replicas中已经有peer2节点的信息,同时细心的你可能也注意到在registered-replicas中已经存在peer2节点的eureka-server了:

同理当你访问peer2的注册中心http://peer2:1112/也可以看到DS Replicas中已经有peer1节点的信息。

这样一个简易的服务注册中心集群就搭建完成,但是里面还没有服务注册进去,将下来尝试将前面的eureka-client项目注册进去。

首先需要将eureka-client项目的application.yml配置文件中的服务注册地址修改为如下所示:

1
2
3
4
5
6
7
spring:
application:
name: hello-service
eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/,http://peer2:1112/eureka/

可以看到这里主要是对eureka.client.service-url.defaultZone的属性值做了修改,将注册中心指向了之前搭建的peer1和peer2,这一点需要注意。

接着启动eureka-client项目,通过访问http://peer1:1111/http://peer2:1112/链接,可以观察到hello-service服务同时被注册到peer1和peer2上,如下所示:

若此时断开peer1,由于hello-service服务同时也向peer2注册,因此在peer2上的其他服务依旧能访问到hello-service,进而实现了服务注册中心的高可用。

当然了,如果开发者不想使用主机名称来定义注册中心的地址,而是想使用IP地址的方式,这也是允许的,只不过需要在application.yml配置文件中增加如下配置参数:

1
2
3
eureka:
instance:
prefer-ip-address: true

请注意该值默认为false,需要开发者自己打开。

服务发现与消费

服务发现与消费介绍

通过上面的介绍和动手实践,我们已经搭建起微服务架构中的核心组件—服务注册中心(包含单节点和高可用模式)。同时还将之前的eureka-client项目进行了改造,通过简单的配置,使得该程序注册到Eureka注册中心上,成为该服务治理体系下的一个服务,且名称为hello-service。也就是说到现在为止,我们已经有了服务注册中心和服务提供者,就差一个服务消费者就形成了一个完整的提供-消费模式。这个服务消费者主要完成两个目标:发现服务和消费服务。也就是说服务的发现和消费实际上是两个行为,这两个行为需要由不同的对象来完成,其中服务的发现由Eureka客户端来完成,而服务的消费由Ribbon来完成。

Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它可以在通过客户端中配置的ribbonServerList服务端列表去轮询访问,以达到负载均衡的作用。当我们将Ribbon和Eureka一起使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnableNIWSServerList重写,扩展成从Eureka注册中心获取服务端列表。同时它也会使用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来确定服务端是否已经启动。在本篇笔者并不对Ribbon做详细介绍,开发者只需要理解它在Eureka服务发现的基础上实现了一套对服务实例的选择策略,进而实现了对服务的消费。关于Ribbon的学习,笔者会在后续文章中进行介绍。

接下来将通过一个简单的例子,来看看如何在Eureka的服务治理体系下如何实现服务的发现与消费。

服务发现与消费实现

第一步,启动服务注册中心。在进行服务发现与消费之前,需要启动之前的服务注册中心eureka-server,为了测试方便,我这里就直接启动一个单节点项目,启动结果如下所示:

第二步,启动多个hello-service服务。为了验证Ribbon的客户端负载均衡功能,这里通过java -jar命令来启动两个不同端口的hello-service服务,当然了前提是先将hello-service服务打包为jar包(注意此处的hello-service就是eureka-client项目):

1
2
start /min java -jar target/eureka-client-0.0.1-SNAPSHOT.jar --server.port=8081
start /min java -jar target/eureka-client-0.0.1-SNAPSHOT.jar --server.port=8082

第三步,确认服务已经注册。在启动两个hello-service服务之后,访问服务注册中心,可以从Eureka的信息面板中看到名为HELLO-SERVICE的服务中出现了两个实例单元,这两个实例就是前面使用命令行启动的8081和8082端口的服务:

第四步,创建消费者。服务注册中心和服务注册都有了,接下来开始创建消费者。按照图示操作,新建一个SpringBoot工程,名称为ribbon-consumer,并在pom.xml文件中添加如下依赖:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.envy</groupId>
<artifactId>ribbon-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ribbon-consumer</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR7</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

其实这个依赖和之前的hello-service服务没有任何区别,因为spring-cloud-starter-netflix-eureka-client中已经包含了spring-cloud-starter-netflix-ribbon,开发者可以在pom.xml同级目录下使用mvn dependency:tree命令来分析项目jar包的引用关系:

第五步,配置启动类。在这一步中,开发者需要做两件事情:标识ribbon-consumer为客户端应用和提供RestTemplate的Bean。首先完成第一小步,在RibbonConsumerApplication这个项目入口类上添加@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,以获得服务发现的能力。接下来完成第二小步,提供RestTemplate的Bean。RestTemplate可以帮助我们发起一个GET或者POST请求,这个笔者会在后续进行介绍,这里只需要提供一个RestTemplate的Spring Bean实例即可,同时在上面添加@LoadBalanced注解来开启客户端负载均衡,相应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
@EnableDiscoveryClient
public class RibbonConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class, args);
}
}

第六步,创建controller。在项目目录中新建一个controller包,并在里面新建一个ConsumerController类,注意该类需要提供一个/ribbon-consumer接口,用于获取服务信息。其实这里所谓的/ribbon-consumer接口就是一个方法的访问链接是/ribbon-consumer罢了,而这个方法名称随意,此处命名为helloConsumer,然后在该方法中可以导入之前创建的RestTemplate,通过它来实现对HELLO-SERVICE服务提供的/hello接口进行调用。可以看到这里访问的地址是服务名HELLO-SERVICE,而不是一个具体的地址,在服务治理框架中,这是一个非常重要的特性,也符合本文开头对于服务治理的解释:

1
2
3
4
5
6
7
8
9
10
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;

@GetMapping(value = "/ribbon-consumer")
public String helloConsumer(){
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
}
}

第七步,配置服务注册中心位置。接下来就是在ribbon-consumer项目中配置Eureka服务注册中心的地址,主要这个地址需要和前面的HELLO-SERVICE保持一致,否则是无法发现服务的,同时设置该消费者的端口为9000或者其他,但是不能与之前使用的端口产生冲突即可:

1
2
3
4
5
6
7
8
9
spring:
application:
name: ribbon-consumer
server:
port: 9000
eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/

第八步,启动ribbon-consumer服务。在完成了上述配置后,接下来启动ribbon-consumer服务,然后查看Eureka注册中心的信息面板,可以看到除了之前的HELLO-SERVICE之外,刚才实现的RIBBON-COMSUMER服务也出现了:

第九步,发起请求。接下来开发者可以在浏览器地址栏中输入http://peer1:9000/ribbon-consumer/,可以看到页面返回hello,world!:

同时开发者也可以在ribbon-consumer应用的控制台中看到如下信息:

可以看到Ribbon输出了当前客户端维护的HELLO-SERVICE的服务列表情况,其中包含了各个实例的位置,Ribbon就是按照此信息进行轮询访问,以实现基于客户端的负载均衡。此外还输出了一些其他有用的信息,如对各个实例的请求总数量、第一次连接信息、上一次连接信息、总的请求失败数量等。

那么现在有人可能要问题,这个hello,world!究竟是哪个HELLO-SERVICE提供的呢?前面提供了8081和8082这两个HELLO-SERVICE服务,其实开发者可以多次发起/ribbon-consumer请求并通过观察启动的两个HELLO-SERVICE服务的控制台,可以看到两个控制台会交替输出下面的日志:

1
/hello,host: 192.168.73.1,service_id: HELLO-SERVICE

这个是之前我们在HelloController中对服务信息的输出配置,开发者可以通过判断当前ribbon-consumer对HELLO-SERVICE的调用来判断Ribbon是否进行了负载均衡。