Consul 入门教程 - 基础篇(2020 年)

大纲

Consul 的介绍

Consul 是什么

作为集群系统的灵魂,服务治理框架一直都受架构师的青睐。随着微服务思想的普及,越来越多的服务治理框架如雨后春笋般冒了出来。除了 Eureka,HashiCorp 公司的 Consul 也让诸多架构师青睐有加。Consul 是一个分布式高可用的服务网格(Service Mesh)解决方案,提供包括服务发现、配置和分段功能在内的全功能控制平面。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建完整的服务网格。简单来说,Consul 是一个分布式高可用的系统服务发现与配置工具,它跟 Eureka 的核心功能一样,但略有不同:

  • Consul 使用 Go 语言编写,以 HTTP 方式对外提供服务
  • Consul 支持多数据中心,这是它的一大特色
  • Consul 提供了可视化的 Web 界面
  • Consul 的一致性协议是 CP(Raft)
  • Consul 除了服务发现之外,还有一些别的功能,例如配置功能

Consul 的功能

Consul 提供了以服务治理为核心的多种功能以满足分布式系统的需要,它可以作为服务治理组件和配置中心。Consul 的主要功能如下,更多介绍可参考:Consul 官网Consul 项目Consul 官方文档Spring Cloud Consul 官方文档Spring Cloud Consul 中文教程

  • 服务发现:有了 Consul,服务可以通过 DNS 或者 HTTP 直接找到它所依赖的服务
  • 健康检查:Consul 提供了健康检查的机制,从简单的服务端是否返回 200 的响应代码到较为复杂的内存使用率是否低于 90%
  • K/V 存储:应用程序可以根据需要使用 Consul 的 Key/Value 存储,Consul 提供了简单易用的 HTTP 接口来满足用户的动态配置、特征标记、协调、Leader 选举等需求
  • 多数据中心:Consul 原生支持多数据中心,这意味着用户不用为了多数据中心自己做抽象

Consul 与同类产品的对比

CAP 理论,又称 CAP 定理,是由计算机科学家 Eric Brewer 于 2000 年提出的一个重要理论,描述了分布式计算系统中的三大核心属性:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。CAP 理论指出,在一个分布式系统中,不可能同时完美地满足这三大属性,最多只能同时满足其中的两个。

  • 一致性(Consistency):一致性指的是每次读取操作都能读取到最近一次写入操作的结果。换句话说,所有节点上的数据在任何时候都是一致的(强一致性),用户无论在哪个节点进行读取操作,都能获取到相同的数据。
  • 可用性(Availability):可用性指的是每个请求都能在合理的时间内得到响应,即系统始终可用。即使有部分节点故障,系统仍能继续处理请求,并返回最新可用的数据。
  • 分区容忍性(Partition Tolerance):分区容忍性指的是即使发生网络分区(节点之间的通信故障),系统依旧能够继续运行。也就是说,即使在网络分区的情况下,系统仍然能够保证其操作的正确性和可用性。

  • CA:单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
  • CP:满足一致性与分区容忍性的系统,通常性能不是特别高。
  • AP:满足可用性与分区容忍性的系统,通常可能对一致性要求低一些。

Consul 的安装

Consul 单机安装

Consul 的单机安装比较简单,官方提供了二进制可执行文件,可以在官网下载自己感兴趣的版本,Linux 系统的安装步骤如下:

  • 将已下载的 consul_l.2.0_linux_amd64.zip 解压到 /opt/consul/ 目录下
  • 添加 consul 到 PATH(环境变量)
  • 执行 consul -v,如果不报错,基本就算安装成功了

Consul 集群安装

Consul 集群默认需要至少三台 Consul 节点启动,当有多个 Consul 节点启动了,那么它们会自动组成集群。如果只是想本地开发调试,可以使用开发者模式启动 Consul,数据默认保存在内存中,重启后会丢失数据。

1
2
# 使用开发模式,启动consul
$ consul agent -dev

Consul 默认是没有 UI 管理界面的,如果需要展示 UI 管理界面,可以加上 -ui 参数进行启动,然后通过 http://127.0.0.1:8500 访问 UI 管理界面:

1
$ consul agent -dev -ui

Consul 的实用接口

Consul 对外提供了丰富的 API,有运维人员喜欢的命令行接口,也有开发人员喜欢的 HTTP 接口,常用的接口如下:

Consul 管理命令

  • consul members:查看当前 Consul 集群里所有成员的信息以及它们的状态:存活、离线、启动失败
  • consul monitor:持续打印当前 Consul 的日志信息,这个命令很有用,因为 Consul 访问量比较大,所以生产环境一般不会保存日志,如果想查看实时日志,可以使用该命令
  • consul leave:退出集群,一般会使用这个命令而不是直接杀掉 Consul 的进程

Consul 对外服务接口

  • /v1/agent/members:列出集群内的所有成员及其信息
  • /v1/status/leader:显示当前集群 leader
  • /v1/catalog/services:显示当前注册的服务
  • /v1/kv/key:显示当前 Key 对应的 Value

Spring Cloud Consul 基础使用

Spring Cloud Consul 介绍

Spring Cloud Consul 通过自动配置、对 Spring Environment 绑定和其他惯用的 Spring 模块,为 Spring Boot 应用程序提供了 Consul 集成。只需要一些简单注解,就可以快速启用和配置 Consul,并用它来构建大型分布式系统。Spring Cloud Consul 作为 Spring Cloud 与 Consul 之间的桥梁,对二者都有良好的支持,其特性如下:

  • 服务注册发现,实例可以向 Consul 注册服务,客户端可以使用 Spring Bean 来发现服务提供方
  • 支持 Ribbon 的客户端负载
  • 支持 Zuul 服务网关
  • 分布式配置中心,使用的是 Consul 的 K/V 存储
  • 控制总线,使用的是 Consul Events

Spring Cloud Consul 入门案例

Spring Cloud Consul 提供了 bus、config、discovery 等模块,项目中可以根据具体的需要选择对应的模块。下面将演示如何使用 config、discovery 模块,点击下载完整的案例代码。

1. 版本说明

在下面的的教程中,使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3。

2. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

3. 创建 Consul Provider 工程

创建 Consul Provider 的 Maven 工程,配置工程里的 pom.xml 文件,引入 spring-cloud-starter-consul-discovery,将服务实例注册到 Consul:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

创建 Consul Provider 的主启动类,这里可以缺省添加 @EnableDiscoveryClient 注解:

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}

创建 Consul Provider 的测试控制类,值得注意的是,在不引入 spring-boot-starter-actuator 依赖的情况下,必须手动创建 /actuator/health 接口,这是新版 Spring Cloud Consul 的默认注册健康检查接口,否则 Consul 会认为服务不可用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class ProviderController {

@Value("${server.port}")
private String port;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/provider/sayHello")
public String sayHello(String name) {
return "from port " + port + ": hello " + name;
}
}

添加 Consul Provider 需要的 application.yml 配置文件到工程中:

1
2
3
4
5
6
7
8
9
10
server:
port: 9001

spring:
application:
name: consul-provider
cloud:
consul:
host: 127.0.0.1 # consul 地址
port: 8500 # consul 端口

4. 创建 Consul Consumer 工程

创建 Consul Consumer 的 Maven 工程,配置工程里的 pom.xml 文件,引入 spring-cloud-starter-consul-discovery,将服务实例注册到 Consul,同时引入 Feign:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

创建 Consul Consumer 的主启动类,这里可以缺省添加 @EnableDiscoveryClient 注解:

1
2
3
4
5
6
7
8
9
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

创建 Consul Consumer 的服务接口类,用于调用 Provider 服务:

1
2
3
4
5
6
@FeignClient(value = "consul-provider")
public interface HelloService {

@RequestMapping(value = "/provider/sayHello", method = RequestMethod.GET)
public String sayHello(@RequestParam("name") String name);
}

创建 Consul Consumer 的测试控制类,在不引入 spring-boot-starter-actuator 依赖的情况下,必须手动创建 /actuator/health 接口,这是新版 Spring Cloud Consul 的默认注册健康检查接口,否则 Consul 会认为服务不可用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class ConsumerController {

@Autowired
private HelloService helloService;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/consumer/sayHello")
public String sayHello(String name) {
return helloService.sayHello(name);
}
}

添加 Consul Consumer 需要的 application.yml 配置文件到工程中:

1
2
3
4
5
6
7
8
9
10
server:
port: 9002

spring:
application:
name: consul-consumer
cloud:
consul:
host: 127.0.0.1 # consul 地址
port: 8500 # consul 端口

5. 创建 Consul Config 工程

创建 Consul Config 的 Maven 工程,配置工程里的 pom.xml 文件,引入 spring-cloud-starter-consul-config;这里的 Consul Config 工程与上面的 Provider、Consumer 工程没有任何关系,作用是用来单独演示 Consul 的 Config 功能(配置中心):

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>

创建 Consul Config 的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RefreshScope
@RestController
@RequestMapping("/config")
public class ConfigController {

@Value("${foo.bar.name}")
private String name;

@GetMapping("/getName")
public String getName() {
return name;
}
}

添加 Consul Config 需要的 application.yml 配置文件到工程中:

1
2
3
4
5
6
7
8
9
10
server:
port: 9003

spring:
application:
name: consul-config
cloud:
consul:
host: 127.0.0.1 # consul 地址
port: 8500 # consul 端口

6. 测试结果

  1. 启动本地的 consul 服务器
  2. 依次启动 consul-provider、consul-consumer 应用
  3. 访问 consul 的管理界面:http://127.0.0.1:8500,如果各个服务的 Health Checks 显示绿色的对勾,即表示服务注册成功,如下图所示:
    config-ui-services
  4. 访问 http://127.0.0.1:9002/consumer/sayHello?name=Peter,如果返回 from port 9001: hello Peter,说明 consul-provider、consul-consumer 应用一切运行成功
  5. 访问 http://127.0.0.1:8500/ui/dc1/kv,点击页面上的 create 按钮,在 Key or folder 栏输入 config/consul-config/foo.bar.namevalue 栏输入 book,然后点击 save 按钮保存,如下图所示:
    config-ui-config
  6. 启动 consul-config 应用,访问 http://127.0.0.1:9003/config/getName,如果接口返回 book,说明成功访问到 Consul Config 的 Key/Value 存储

Spring Cloud Consul 进阶使用

Spring Cloud Consul 模块介绍

Spring Cloud Consul 是在 ecwid 的 consul-api 的基础上又封装了一层功能,使其跟现有 Spring Cloud 组件融合,达到开箱即用的目的。围绕着 Consul 的核心功能,Spring Cloud Consul 也提供了相应的功能模块与之匹配,其中 Consul 的事件功能比较弱化,应用比较多的是服务治理和配置功能,各个模块的介绍如下:

  • spring-cloud-consul-binder:对 Consul 的事件功能封装
  • spring-cloud-consul-config:对 Consul 的配置功能封装
  • spring-cloud-consul-core:基础配置和健康检查模块
  • spring-cloud-consul-discovery:对 Consul 服务治理功能封装

Spring Cloud Consul Discovery

基础配置参数

服务启动时,会通过 ConsulServiceRegistry.register() 向 Consul 注册自身的服务。服务注册时,会告诉 Consul 以下信息:

  • ID:服务 ID,默认是服务名 + 端口号
  • Name:服务名,默认是应用名称
  • Tags:给服务打的标签,默认是 [secure=false]
  • Address:服务地址,默认是本机 IP
  • Port:服务端口,默认是服务的 Web 端口
  • Check:健康检查信息,包括 Interval(健康检查间隔)和 HTTP(健康检查地址)

一般情况下,不需要显式提供上述信息,Spring Cloud Consul 会有默认值,但是在一些特殊业务场景中,可能就需要定制上述服务了。Consul Discovery 的常见配置如下:

consul-discovery-setting

Tags 栏会有一个 secure=false,这个是 Spring Cloud Consul 默认加上的,它取自配置 spring.cloud.consul.discovery.scheme,默认值是 http。如果服务提供的是 https 的服务时,需要配置该值为 https,它的作用是告诉服务消费者调用服务方接口时需要哪种协议。配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 自定义健康检测接口
*/
@RestController
public class ProviderController {

@GetMapping("/health")
public String health() {
return "SUCCESS";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 9001

spring:
application:
name: consul-provider
cloud:
consul:
host: 127.0.0.1 # consul 启动地址
port: 8500 # consul 启动端口
discovery:
prefer-ip-address: true # 优先使用 IP 注册
ip-address: 127.0.0.1 # 若部署在 Docker 中,指定宿主机 IP
port: 9001 # 若部署在 Docker 中,指定宿主机端口
health-check-interval: 20s # 健康检查间隔时间为 20s
health-check-path: /health # 自定义健康检查路径
tags: ${LANG},test # 指定服务的标签, 用逗号隔开

服务发现使用案例

Spring Cloud Consul 提供了两种方式的服务发现功能:Ribbon 和 DiscoveryClient。如果客户端使用了 Feign,或者使用了 RestTemplate + @LoadBalancerd 注解,那么默认使用的是 ConsulServerList 提供的服务发现逻辑。如果客户端只想独立使用服务发现功能,那么可以直接使用 DiscoveryClient。上面提到了使用 Consul 的 Tags 功能将服务分组,下面就用上面说的两种方式分别调用服务提供者的接口,点击下载完整的案例代码,由于篇幅有限,以下只给出核心代码和配置。

1. 创建 Consul Provider Tag One 工程

创建 Consul Provider Tag One 的主启动类:

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderOneApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderOneApplication.class, args);
}
}

创建 Consul Provider Tag One 的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ProviderOneController {

@Value("${server.port}")
private String port;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/provider/sayHello/{name}")
public String sayHello(@PathVariable("name") String name) {
return "from port " + port + ": hello " + name;
}
}

创建 Consul Provider Tag One 的 application.yml 配置文件,加入 tags 属性:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9001

spring:
application:
name: consul-provider
cloud:
consul:
host: 127.0.0.1 # consul 启动地址
port: 8500 # consul 启动端口
discovery:
tags: tag1
2. 创建 Consul Provider Tag Two 工程

创建 Consul Provider Tag Two 的主启动类:

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@SpringBootApplication
public class ProviderTwoApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderTwoApplication.class, args);
}
}

创建 Consul Provider Tag Two 的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class ProviderTwoController {

@Value("${server.port}")
private String port;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/provider/sayHello/{name}")
public String sayHello(@PathVariable("name") String name) {
return "from port " + port + ": hello " + name;
}
}

创建 Consul Provider Tag Two 的 application.yml 配置文件,加入 tags 属性:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9002

spring:
application:
name: consul-provider
cloud:
consul:
host: 127.0.0.1 # consul 启动地址
port: 8500 # consul 启动端口
discovery:
tags: tag2
3. 创建 Consul Consumer Ribbon 工程

创建 Consul Consumer Ribbon 的主启动类:

1
2
3
4
5
6
7
8
9
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerRibbonApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumerRibbonApplication.class, args);
}
}

创建 Consul Consumer Ribbon 的服务接口类,用于调用 Provider 服务:

1
2
3
4
5
6
@FeignClient("consul-provider")
public interface ProviderService {

@RequestMapping(value = "/provider/sayHello/{name}", method = RequestMethod.GET)
public String sayHello(@PathVariable("name") String name);
}

创建 Consul Consumer Ribbon 的基础配置类,声明 RestTemplate 的 Bean 对象:

1
2
3
4
5
6
7
8
9
@Configuration
public class CommonConfiguration {

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

创建 Consul Consumer Ribbon 的测试控制类,建立两个 REST 接口,一个通过 Feign 的方式访问 Provider,另一个通过 RestTemplate 的方式访问 Provider:

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
@RestController
public class ConsumerRibbonController {

private static final String URL = "http://consul-provider";

@Autowired
private RestTemplate restTemplate;

@Autowired
private ProviderService providerService;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/consumer/sayHelloOne/{name}")
public String sayHelloOne(@PathVariable("name") String name) {
return providerService.sayHello(name);
}

@GetMapping("/consumer/sayHelloTwo/{name}")
public String sayHelloTwo(@PathVariable("name") String name) {
return restTemplate.getForObject(URL + "/provider/sayHello/" + name, String.class);
}
}

创建 Consul Consumer Ribbon 的 application.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9003

spring:
application:
name: consul-consumer-ribbon
cloud:
consul:
host: 127.0.0.1 # consul 启动地址
port: 8500 # consul 启动端口
discovery:
server-list-query-tags:
consul-provider: tag1 # 在调用 consul-provider 服务时,使用 tag1 对应的服务实例
4. 创建 Consul Consumer Discovery Client 工程

创建 Consul Consumer Discovery Client 的主启动类:

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@SpringBootApplication
public class DiscoveryClientApplication {

public static void main(String[] args) {
SpringApplication.run(DiscoveryClientApplication.class, args);
}
}

创建 Consul Consumer Discovery Client 的测试控制类,使用 DiscoveryClient 注入的方式,手动去 Consul 中获取服务列表;这里需要说明的是,ConsulDiscoveryClient 中不支持根据自定义 Tags 获取服务提供者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class TestController {

@Autowired
private DiscoveryClient discoveryClient;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/getServer/{serviceId}")
public List<ServiceInstance> getServer(@PathVariable("serviceId") String serviceId) {
return discoveryClient.getInstances(serviceId);
}
}

创建 Consul Consumer Discovery Client 的 application.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
server:
port: 9004

spring:
application:
name: consul-consumer-discovery-client
cloud:
consul:
host: 127.0.0.1 # consul 启动地址
port: 8500 # consul 启动端口
5. 测试结果
  1. 启动本地的 Consul 服务器,打开 Consul 的管理界面,查看服务的注册情况,如下图所示:
    consul-discovery-result-1
  2. 依次启动 consul-provider-tag-one、consul-provider-tag-two、consul-consumer-discovery-client、consul-consumer-ribbon 应用
  3. 请求 consul-consumer-ribbon 应用的 Feign 接口,访问 http://127.0.0.1:9003/consumer/sayHelloOne/Jim,查看返回的内容是否为 from port 9001: hello Jim
  4. 请求 consul-consumer-ribbon 应用的 RestTemplate 接口,访问 http://127.0.0.1:9003/consumer/sayHelloTwo/Peter,查看返回的内容是否为 from port 9001: hello Peter
  5. 请求 consul-consumer-discovery-client 应用的接口,访问 http://127.0.0.1:9004/getServer/consul-provider,查看返回的服务提供者信息是否只有 consul-provider 提供者,如下图所示:
    consul-discovery-result-2

Spring Cloud Consul Config

上面的示例演示了 Spring Cloud Consul Config 获取和刷新配置的简单用法。Spring Cloud Consul Config 与 Consul 是通过 HTTP 进行交互的,那配置刷新是如何做到的呢?另外,示例中只有一条配置,可是实际工作中的配置可能有成百上千条,难道配置信息需要一个个在 Consul 的管理页面中添加吗?

配置中心使用案例

Consul 只支持用 K/V 的方式进行配置,那怎么让 Consul 支持同时配置多条的方式呢?难道要给 Consul 增加一个导入功能吗?其实 K/V 不仅可以代表一条配置,还可以代表一个应用的配置,将应用名作为 Key,Value 中用来存放它所有的配置,这样就可以达到同时配置多条的结果。Spring Cloud Consul Config 就是这样,通过将 yml 或者 properties 放在 Value 中来实现配置的批量操作。下面的示例将演示使用 Consul 的配置功能,将整个应用的配置以 yml 的方式存储在 Consul 中,以此实现类似 Spring Cloud Config 的配置中心功能,点击下载完整的案例代码。

创建 Consul Config Customize 工程,配置工程里的 pom.xml 文件:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>

创建 Consul Config Customize 的主启动类:

1
2
3
4
5
6
7
@SpringBootApplication
public class ConfigApplication {

public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}

创建 Consul Config Customize 的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RefreshScope
@RestController
@RequestMapping("/config")
public class TestController {

@Value("${foo.bar.name}")
private String name;

@GetMapping("/getName")
public String getName() {
return name;
}
}

创建 Consul Config Customize 的 application.yml 配置文件:

1
2
3
4
5
spring:
application:
name: consul-config-customize
profiles:
active: dev

创建 Consul Config Customize 的 bootstrap.yml 配置文件,增加自定义的配置属性:

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
consul:
config:
host: 127.0.0.1 # Consul 启动地址
port: 8500 # Consul 启动端口
format: yaml # Consul 中 Value 配置格式为 yaml
prefix: configuration # Consul 中的配置文件目录,默认为 config
default-context: app # 去该目录下查找缺省配置,默认为 application
profile-separator: ':' # profiles 配置的分隔符,默认为 ','
data-key: data # 如果指定配置格式为 yaml 或者 properties,则需要该值作为 key,默认为 data

测试结果:

  1. 启动本地的 Consul 服务器
  2. 访问 http://127.0.0.1:8500/ui/dc1/kv 页面,添加 key 为:configuration/consul-config-customize:dev/data,value 为:
1
2
3
4
5
server:
port: 9002
foo:
bar:
name: book-dev
  1. 访问 http://127.0.0.1:8500/ui/dc1/kv 页面,添加 key 为:configuration/consul-config-customize:test/data,value 为:
1
2
3
4
5
server:
port: 9003
foo:
bar:
name: book-test
  1. 启动 consul-config-customize 应用,查看启动的端口号;访问 http://127.0.0.1:9002/config/getName,查看接口返回的内容
  2. 更改 application.yml 中的配置为 spring.profiles.active=test
  3. 重新启动 consul-config-customize 应用,查看启动的端口号;访问 http://127.0.0.1:9003/config/getName,查看接口返回的内容

配置实时刷新原理

Spring Cloud Consul 是通过 HTTP 的方式跟 Consul 交互,那配置是如何实时生效的呢?答案其实很简单,那就是配置并没有实时生效。org.springframework.cloud.consul.config.ConfigWatch 中有一个定时方法 watchConfigKeyValues(),它默认每秒执行一次(可以通过 spring.cloud.consul.config.watch.delay 自定义执行的时间间隔),去 Consul 中获取最新的配置信息,一旦配置发生改变,Spring 通过 ApplicationEventPublisher 重新刷新配置。Consul Config 组件就是通过这种方式,达到配置 “实时生效” 的目的。那客户端如何得知配置被更新过了呢,答案在 Consul 返回的数据里。Consul 会给每一项配置加一个 consulIndex 属性,类似于版本号,如果配置更新,它就会自增。Spring Cloud Consul Config 就是通过缓存 consulIndex 来判断配置是否发生改变。

Spring Cloud Consul 功能重写

Spring Cloud Consul 提供了很多方便实用的功能,但是面对五花八门的需求,还是希望可以重写它的原有逻辑。

重写 ConsulDiscoveryClient (支持 Tag)

ConsulDiscoveryClient 并不支持根据自定义 Tag 获取服务,一般来说,ConsulServerList 和 ConsulDiscoveryClient 虽然面对的需求不同,但是实现的功能都是一样的,那就是根据条件查找服务。可能 Spring 认为 ConsulDiscoveryClient 是为一些框架型的功能准备的,用户完全可以拿到服务列表后自行筛选所需的数据。下面的示例将重写 ConsulDiscoveryClient 的功能,让其支持根据自定义 Tag 获取服务。首先创建三个工程,分别是:consul-provider-tag-one、consul-provider-tag-two、consul-consumer-override,其中前两个工程与上面的服务发现案例里的配置和代码完全一致,这里不再累述,点击下载完整的案例代码。

Consul Consumer Override 工程里的 MyConsulDiscoveryClient 类:

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
66
67
68
69
70
71
public class MyConsulDiscoveryClient implements DiscoveryClient {

private final ConsulClient client;
private final ConsulDiscoveryProperties properties;

public MyConsulDiscoveryClient(ConsulClient client, ConsulDiscoveryProperties properties) {
this.client = client;
this.properties = properties;
}

@Override
public String description() {
return "Spring Cloud Consul Discovery Client";
}

@Override
public List<ServiceInstance> getInstances(final String serviceId) {
return getInstances(serviceId, QueryParams.DEFAULT);
}

public List<ServiceInstance> getInstances(final String serviceId, final QueryParams queryParams) {
List<ServiceInstance> instances = new ArrayList<>();
addInstancesToList(instances, serviceId, queryParams);
return instances;
}

private void addInstancesToList(List<ServiceInstance> instances, String serviceId, QueryParams queryParams) {
String aclToken = properties.getAclToken();
Response<List<HealthService>> services;
if (StringUtils.hasText(aclToken)) {
// 这里由获取默认tag改为获取指定tag
services = client.getHealthServices(serviceId, getTag(serviceId), this.properties.isQueryPassing(), queryParams, aclToken);
} else {
// 这里由获取默认tag改为获取指定tag
services = client.getHealthServices(serviceId, getTag(serviceId), this.properties.isQueryPassing(), queryParams);
}
for (HealthService service : services.getValue()) {
String host = ConsulServerUtils.findHost(service);
Map<String, String> metadata = ConsulServerUtils.getMetadata(service);
boolean secure = false;
if (metadata.containsKey("secure")) {
secure = Boolean.parseBoolean(metadata.get("secure"));
}
instances.add(new DefaultServiceInstance(serviceId, host, service.getService().getPort(), secure, metadata));
}
}

public List<ServiceInstance> getAllInstances() {
List<ServiceInstance> instances = new ArrayList<>();
Response<Map<String, List<String>>> services = client.getCatalogServices(QueryParams.DEFAULT);
for (String serviceId : services.getValue().keySet()) {
addInstancesToList(instances, serviceId, QueryParams.DEFAULT);
}
return instances;
}

@Override
public List<String> getServices() {
String aclToken = properties.getAclToken();
if (StringUtils.hasText(aclToken)) {
return new ArrayList<>(client.getCatalogServices(QueryParams.DEFAULT, aclToken).getValue().keySet());
} else {
return new ArrayList<>(client.getCatalogServices(QueryParams.DEFAULT).getValue().keySet());
}
}

// 获取tag的方法,该方法在 ConsulServerList 中已存在
protected String getTag(String serviceId) {
return this.properties.getQueryTagForService(serviceId);
}
}

Consul Consumer Override 工程里的配置类:

1
2
3
4
5
6
7
8
9
@Configuration
public class CommonConfiguration {

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) // 保证优先被Spring加载
public MyConsulDiscoveryClient discoveryClient(ConsulClient client, ConsulDiscoveryProperties properties) {
return new MyConsulDiscoveryClient(client, properties);
}
}

Consul Consumer Override 工程里的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class TestController {

@Autowired
private DiscoveryClient discoveryClient;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/getServer/{serviceId}")
public List<ServiceInstance> getServer(@PathVariable("serviceId") String serviceId) {
return discoveryClient.getInstances(serviceId);
}
}

Consul Consumer Override 工程里的 application.yml 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9004

spring:
application:
name: consul-consumer-discovery-client
cloud:
consul:
host: 127.0.0.1 # consul 启动地址
port: 8500 # consul 启动端口
discovery:
server-list-query-tags:
consul-provider: tag1 # 在调用 consul-provider 服务时,使用 tag1 对应的服务实例

测试结果:

  1. 启动本地的 Consul 服务器
  2. 依次启动 consul-provider-tag-one、consul-provider-tag-two、consul-consumer-override 应用
  3. 访问 http://127.0.0.1:9004/getServer/consul-provider,查看接口返回的信息,看看是否只返回了 tag1 对应的服务实例

重写 ConsulServerList

原理分析

单纯的自定义实现 ServerList 的接口并不能达到重写 ConsulServerList 的目的,是因为 ConsulServerList 的 serviceId 属性为 null 时会导致启动报错。这个 serviceId 属性表示服务提供者的名称,但是却作为 ConsulServerList 的成员变量。由此可以联想到,Spring Cloud Consul 为每个服务提供者都创建了一个 ConsulServerList 实例,这是为了支持 Ribbon 的服务配置个性化。Ribbon 支持对某一个服务单独配置负载,比如负载算法,是否重试等,当然也包括服务发现逻辑,为每一个服务实例化一个服务发现逻辑,可以最大化地将自由交给实现方。特别注意,ConsulServerList 并不是在 Spring 启动的时候初始化,而是在服务调用时通过 Ribbon 进行初始化,具体的初始化流程如下:

  • Feign 通过 serviceId 去 Ribbon 中获取服务端配置
  • Ribbon 根据 serviceId 去缓存中找是否存在这个名称的 AnnotationConfigApplicationContext 实例,如果有就立即返回;如果没有就创建一个,而创建 AnnotationConfigApplicationContext 的过程,就是 ConsulServerList 初始化的过程
  • AnnotationConfigApplicationContext 跟 ConsulServerList 是通过 @RibbonClient 注解关联在一起的,具体可以参考 RibbonClientConfigurationRegistrar 源码

所以重写 ConsulServerList 的过程比较麻烦,要么重新写一套类似的 spring-cloud-consul-discovery 源码,要么就从源头的 RibbonClientConfiguration 开始直到 ConsulServerList 均改成自己的实现。

重写示例

下面的示例将使用第二种方式重写 ConsulServerList,首先创建三个工程,分别是:consul-provider-tag-one、consul-provider-tag-two、consul-consumer-override,其中前两个工程与上面的服务发现案例里的配置和代码完全一致,这里不再累述,点击下载完整的案例代码。值得一提的是,在 Consul Consumer Override 工程里新增 MyConsulServerList、MyConsulRibbonClientConfiguration、MyRibbonConsulAutoConfiguration 类,需要保证 MyConsulRibbonClientConfiguration 类不能与被 @ConponentScan 修饰的主类放在同一个包或其子包下,否则会导致 IClientConfig 的 Bean 无法注入。

Consul Consumer Override 工程里的 MyConsulServerList 类:

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
public class MyConsulServerList extends AbstractServerList<ConsulServer> {

private String serviceId;
private final ConsulClient client;
private final ConsulDiscoveryProperties properties;
private static final Logger logger = LoggerFactory.getLogger(MyConsulServerList.class);

public MyConsulServerList(ConsulClient client, ConsulDiscoveryProperties properties) {
this.client = client;
this.properties = properties;
}

/**
* 打印一句提示
*/
private List<ConsulServer> getServers() {
if (this.client == null) {
return Collections.emptyList();
}

logger.info("===== 自定义服务发现 =====");

String tag = getTag(); // null is ok
Response<List<HealthService>> response = this.client.getHealthServices(
this.serviceId, tag, this.properties.isQueryPassing(),
createQueryParamsForClientRequest(), this.properties.getAclToken());
if (response.getValue() == null || response.getValue().isEmpty()) {
return Collections.emptyList();
}
return transformResponse(response.getValue());
}

// 省略其他代码 ....
}

Consul Consumer Override 工程里的 MyConsulRibbonClientConfiguration 类:

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
@Configuration
public class MyConsulRibbonClientConfiguration {

@Autowired
private ConsulClient client;

private String serviceId = "client";

protected static final String VALUE_NOT_SET = "__not__set__";

protected static final String DEFAULT_NAMESPACE = "ribbon";

public MyConsulRibbonClientConfiguration() {
}

public MyConsulRibbonClientConfiguration(String serviceId) {
this.serviceId = serviceId;
}

/**
* 将ServerList生效的实现改为MyServerList
*
* @param config
* @param properties
* @return
*/
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config, ConsulDiscoveryProperties properties) {
MyConsulServerList serverList = new MyConsulServerList(client, properties);
serverList.initWithNiwsConfig(config);
return serverList;
}

// 省略其他代码 ....
}

Consul Consumer Override 工程里的 MyRibbonConsulAutoConfiguration 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 该类主要是将原有入口取代, 因此它的生效逻辑刚好跟 RibbonConsulAutoConfiguration 相反
* 当 spring.cloud.consul.ribbon.enabled 为 false 时, 这里重写的逻辑生效
*/
@Configuration
@ConditionalOnConsulEnabled
@ConditionalOnBean(SpringClientFactory.class)
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@ConditionalOnExpression("${spring.cloud.consul.ribbon.enabled:true}==false")
@RibbonClients(defaultConfiguration = MyConsulRibbonClientConfiguration.class)
public class MyRibbonConsulAutoConfiguration {

}

在 Consul Consumer Override 工程里里创建 /src/main/resources/META-INF/spring.factories 配置文件,添加上面的自动配置类:

1
2
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.springcloud.override.consul.MyRibbonConsulAutoConfiguration

Consul Consumer Override 工程里的启动主类:

1
2
3
4
5
6
7
8
9
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerOverrideApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumerOverrideApplication.class, args);
}
}

Consul Consumer Override 工程里的服务接口类,用于调用 Provider 服务:

1
2
3
4
5
6
@FeignClient("consul-provider")
public interface ProviderService {

@RequestMapping(value = "/provider/sayHello/{name}", method = RequestMethod.GET)
public String sayHello(@PathVariable("name") String name);
}

Consul Consumer Override 工程里的测试控制类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class TestController {

@Autowired
private DiscoveryClient discoveryClient;

@Autowired
private ProviderService providerService;

@GetMapping("/actuator/health")
public String health() {
return "SUCCESS";
}

@GetMapping("/sayHello/{name}")
public String getServer(@PathVariable("name") String name) {
return providerService.sayHello(name);
}
}

Consul Consumer Override 工程里的 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9004

spring:
application:
name: consul-consumer-discovery-client
cloud:
consul:
host: 127.0.0.1 # consul 启动地址
port: 8500 # consul 启动端口
ribbon:
enabled: false # 此处配置很重要,为 true 时走原有逻辑, 为 false 时走重写逻辑

测试结果:

  1. 启动本地的 Consul 服务器
  2. 依次启动 consul-provider-tag-one、consul-provider-tag-two、consul-consumer-override 应用
  3. 访问 http://127.0.0.1:9004/sayHello/Peter,查看接口是否正常返回内容,控制台输出的日志信息如下:
1
2
3
4
c.netflix.loadbalancer.BaseLoadBalancer  : Client: consul-provider instantiated a LoadBalancer: DynamicServerListLoadBalancer:NFLoadBalancer:name=consul ...
c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
c.s.override.consul.MyConsulServerList : ===== 自定义服务发现 =====
c.netflix.config.ChainedDynamicProperty : Flipping property: consul-provider.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
  1. 多次访问 http://127.0.0.1:9004/sayHello/Peter,通过接口返回的服务方端口号,看看客户端是不是默认以轮询的方式调用服务方的接口

Spring Cloud Consul 使用的坑

异常信息不完整

开发者偶尔会遇到 Spring Cloud Consul 打印的异常堆栈中,message 为 null 的情况,导致排查问题异常困难,这是因为 Spring Cloud Consul 对 consul-api 自定义的 OperationException 异常没有做特殊处理导致的。当 Consul 的 HTTP 响应代码为非 200 时,consul-api 会抛出 OperationException;而 Spring Cloud Consul 在调用 consul-api 接口时,有些代码会简单地使用 Exception 捕获异常,然后打印 Log 日志,导致 OperationException 的属性丢失。例如在 ConsulCatalogWatch、ConsulHealthIndicator 中均使用这种处理方式。

Consul Api 的兼容问题

Consul 在 1.0.0 版本后,将一些接口(/agent/check/pass,/agent/service/deregister)由 GET 方法改成 PUT,这个 bug 在 consul-api 的 1.3.0 版本才得到解决,对应到 Spring Cloud Consul 已经是 2.0.0.M1 版本了。