14.6 在运行时刷新配置属性

14.6 在运行时刷新配置属性

在编写本章的时候,我正在一架飞机上,因为维护问题,飞机被重新拉回了登机口。情况并不严重,你正在读本章的内容,就说明机械工程师的工作完成得还是很令人满意的。即便如此,关于飞机维护,最有意思的事情是它要求飞机必须要在地面上。如果飞机正在飞行,那么能做的事情就太少了。

相比之下,在《星球大战》(Star Wars)电影中,如果Luke Skywalker或PoeDameron的X翼战机需要维护,舰载机械机器人(mech droid)就可以派上用场了,即使X翼战机正在作战,它也可以开展工作。

传统上,应用程序维护,包括配置更改,都需要重新部署或至少重新启动应用。可以说,由于缺少一个“机械机器人”来调整哪怕是最小的配置属性,因此我们每次都需要将应用程序拉回“登机口”。这对云原生应用来说是不可接受的。我们希望能够动态地更改配置属性,而不需要关闭应用程序。

幸运的是,Spring Cloud Config Server能够刷新正在运行的应用程序的配置属性,而不需要停机。一旦变更推送到支撑的Git仓库或Vault私密仓库,应用中的每个微服务就都可以立即通过以下两种方式的某一种进行刷新。

  • 手动刷新:Config Server客户端启用一个特殊的“/actuator/refresh”端点,对每个服务的这个端点发送HTTP POST请求将会强制配置客户端从Config Server的后端检索最新的配置。
  • 自动刷新:Git仓库上的提交hook会触发所有Config Server客户端服务的刷新操作。这涉及Spring Cloud的另一个项目,名为Spring Cloud Bus,它能够用于Config Server及其客户端之间的通信。

每种方案都有其优点和缺点。手动刷新能够更精确地控制服务何时更新最新配置,但是它需要向每个微服务实例发送一个HTTP请求。自动更新能够让应用中的每个微服务即时使用最新的配置,但它是由配置仓库的提交自动触发的,对于有些项目来说过于危险。

我们接下来看一下这两种方案,然后你就可以自行选择哪种方式更适合你的项目了。

14.6.1 手动刷新配置属性

在第16章中,我们将会介绍Spring Boot Actuator。它是Spring Boot的基本元素之一,能够探查应用运行时的状况并且允许对运行时进行一些有限的操作,比如修改日志级别。现在先看一个特殊的Actuator特性,只有配置为Spring CloudConfig Server客户端的应用,这个特性才有效。

当我们将应用设置为Config Server客户端的时候,自动配置功能会配置一个特殊的Actuator端点,用来刷新配置属性。为了使用该端点,在项目的构建文件中除了Config Client依赖,我们还需要添加Actuator starter依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

我们可以猜到,这项依赖也可以在Spring Initializr中通过选中Actuator复选框添加进来。

在Config Server客户端应用中添加Actuator之后,我们可以在任意时间发送HTTPPOST请求到“/actuator/refresh”,通知它从后端仓库刷新配置属性。

我们看一下它是如何实现的。假设我们有一个带有@ConfigurationProperties注解的类,名为GreetingProps:

1
2
3
4
5
6
7
8
9
10
11
@ConfigurationProperties(prefix="greeting")
@Component
public class GreetingProps {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

另外,我们可以编写一个控制器类。GreetingProps会注入其中,当它在处理GET请求时,返回message属性的值:

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class GreetingController {
private final GreetingProps props;
public GreetingController(GreetingProps props) {
this.props = props;
}
@GetMapping("/hello")
public String message() {
return props.getMessage();
}
}

在我们的Git配置仓库中有一个application.yml文件,含有如下的属性设置:

1
2
greeting:
message: Hello World!

Config Server和这个简单的hello-world配置客户端运行起来之后,我们对“/hello”发送HTTP GET请求,将会产生如下的响应:

1
2
$ curl localhost:8080/hello
Hello World!

现在,我们对Config Server和hello-world都不进行重启,而是修改application.yml文件并推送至后端Git仓库,这样greeting.message属性将会变成如下形式:

1
2
greeting:
message: Hiya folks!

即便在Git中配置已经发生变化,如果我们发送GET请求到hello-world应用,得到的结果依然是“Hello World!”响应。但是,我们可以对刷新端点发送一个POST请求,强制使其刷新:

1
2
$ curl localhost:53419/actuator/refresh -X POST
["config.client.version","greeting.message"]

注意,响应中包含一个JSON数组,列出了发生变更的属性名。这个数组包含greeting.message属性,还包含config.client.version属性(当前配置对应的Git提交的哈希值)的变化。因为现在的配置基于一个新的Git提交,所以每当后端的配置仓库有变化时,这个值都会跟着变化。

POST请求的响应告诉我们greeting.message已经发生变化了。但是,真正的证据还是要靠再次向“/hello”路径发送GET请求:

1
2
$ curl localhost:8080/hello
Hiya folks!

无须重启应用,甚至无须重启Config Server,应用现在就能向我们提供greeting.message属性的全新值。

如果我们能够完全控制何时对配置属性进行更新,那么“/actuator/refresh”端点是很不错的选择。如果我们的应用由多个微服务组成(可能每个服务都有多个实例),那么将配置传播到所有服务可能是一项非常乏味的工作。接下来,我们看一下如何一次性地将配置变更自动用到所有服务上。

14.6.2 自动刷新配置属性

Config Server能够借助名为Spring Cloud Bus的Spring Cloud项目将配置变更自动通知到每个客户端,作为手动刷新应用中每个Config Server客户端属性的替代方案。图14.7阐述了它是如何运行的。

image-20211023215219588

图14.7 Config Server与Spring Cloud Bus能够对应用广播变更(应用会在属性发生变化的时候,自动刷新它们的属性)

可以简要概括图14.7中的属性刷新流程。

  • 在配置Git仓库上创建一个webhook,当Git仓库有任何变化(比如所有的推送)时,都会通知Config Server。很多的Git实现都支持webhook,比如GitHub、GitLab、Bitbucket和Gogs。
  • Config Server会对webhook POST请求做出响应,借助某种消息代理以消息的方式广播该变更。
  • 每个Config Server客户端应用订阅该通知,对通知消息做出响应,也就是会使用Config Server中的新属性值刷新它们的环境。

这样做的结果就是,在配置属性变更推送到后端的Git仓库之后,所有的ConfigServer客户端应用能够立即获取最新的配置属性值。

在使用Config Server的自动属性刷新功能时,会有多个部件在发挥作用。我们回顾一下要做的变更,这样对需要做的事情会有一个整体的了解。

  • 我们需要有一个消息代理,用来处理Config Server及其客户端之间的消息传递,可以选择RabbitMQ或Kafka。
  • 在后端Git仓库上需要创建一个webhook,将各种变更通知给ConfigServer。
  • Config Server需要启用Config Server监控依赖(提供了处理Git仓库webhook请求的端点)以及RabbitMQ或Kafka的Spring Cloud Stream依赖(用于发布属性变更消息给代理)。
  • 除非消息代理在本地按照默认设置运行,否则,我们要在Config Server及其所有的客户端上配置连接至代理的详细信息。
  • 每个Config Server的客户端应用需要Spring Cloud Bus依赖。

假设预先需要的消息代理(不管是RabbitMQ、Kafka,还是你选择的其他方案)已经处于运行状态,并且为传送属性变更消息做好了准备,我们首先从将属性变更应用于Config Server开始,让它处理webhook的更新请求。

创建webhook

很多Git服务都支持创建webhook,从而能够将Git仓库的变更信息通知给应用,这些变更包括推送。不同实现之间创建webhook的操作有所差异,我们很难对它们一一描述。在这里,我会介绍如何为Gogs仓库创建webhook。

我选择Gogs的原因在于它非常易于在本地运行,并且支持将webhook POST用到本地运行的应用上(对于GitHub来说,这非常难以实现)。同时,在Gogs上创建webhook的过程与GitHub几乎完全相同,因此描述Gogs的过程能够间接让你知道为GitHub创建webhook都需要哪些步骤。

首先,在Web浏览器中访问配置仓库并点击Settings链接,如图14.8所示。(GitHub上Settings链接的位置略有差异,但是它们的外观很相似。)

image-20211023215249586

图14.8 在Gogs或GitHub上点击Settings开始创建webhook

这会将我们带到仓库的设置页面,在左侧包含了一个设置分类的菜单。在菜单中选择Webhooks,将会出现如图14.9所示的页面。

image-20211023215304730

图14.9 Webhooks页面中的Add Webhook按钮会打开创建webhook的表单

在Webhooks设置页面,点击Add Webhook按钮,在Gogs中会生成一个下拉列表,用来选择不同类型的webhook。选择Gogs选项,如图14.9所示。这样,我们会看到一个创建新webhook的表单,如图14.10所示[^1]。

Add Webhook表单有多个输入域,重要的是Payload URL和Content Type。我们马上将会配置Config Server来处理webhook的POST请求。在实现该功能的时候,Config Server将会在“/monitor”路径下处理webhook请求。因此,我们需要将Payload URL输入域设置成引用Config Server的“/monitor”端点的URL。因为我是在一个Docker容器中运行Gogs的,所以在图14.10中将URL设置成http://host.docker.internal:8888/monitor,它的域名为host.docker.internal。这个域名让Gog服务器能够跨越容器的边界访问宿主机器上的Config Server[^2]。

image-20211023215423231

图14.10 创建webhook时需要指定Config Server的“/monitor”URL和JSON载荷

我还将Content Type输入域设置成了application/json。这一点非常重要,因为Config Server的“/monitor”端点并不支持Content Type的另一个选项application/x-www- form-urlencoded。

如果设置Secret输入域,就可以在webhook POST请求中新增一个名为X-Gogs-Signature(在GitHub中名为X-Hub-Signature)的头信息,包含给定私密信息的HMAC-SHA256摘要(在GitHub中是HMAC-SHA1)。此时,Config Server的“/monitor”端点并不识别这个签名头信息,因此我们可以将这个输入域设置为空。

最后,我们只关心配置仓库的推送请求,另外,我们当然希望这个webhook处于活跃状态,所以需要确保Just the push event单选框和Active复选框处于选中状态。点击表单底部的Add Webhook按钮,webhook就创建完成了。每当仓库有推送的时候,就会向Config Server发送POST请求。

现在,我们必须要启用Config Server的“/monitor”端点来处理这些请求。

在Config Server中处理webhook更新

要启用Config Server的“/monitor”端点非常简单,我们只需添加spring-cloud-config-monitor依赖到Config Server的配置文件即可。在Maven的pom.xml文件中,如下的依赖就会完成该项工作:

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

这项依赖添加完成之后,自动配置功能会发挥作用,从而启用“/monitor”端点。但是,除非Config Server本身有广播变更通知的方法,否则不会带来任何好处。为了实现这一点,我们需要添加对Spring Cloud Stream的依赖。

Spring Cloud Stream是另一个Spring Cloud项目。借助它,我们能够创建通过底层绑定机制通信的服务,这种通信机制可能是RabbitMQ或Kafka。服务在编写的时候并不会关心如何使用这些通信机制,只是接受流中的数据,对其进行处理,并返回到流中,由下游的服务继续处理。

“/monitor”端点使用Spring Cloud Stream发布通知消息给参与的ConfigServer客户端。为了避免硬编码特定的消息实现,监控器会作为Spring CloudStream的源,发布消息到流中并让底层的绑定机制处理消息发送的特定功能。

如果使用RabbitMQ,就需要将Spring Cloud Stream RabbitMQ绑定依赖添加到Config Server的构建文件中:

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

如果你更喜欢Kafka,那么需要添加如下的Spring Cloud Stream Kafka依赖:

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

依赖准备就绪之后,Config Server几乎就可以参与属性自动刷新功能了。实际上,如果RabbitMQ或Kafka在本地运行并且使用默认配置,Config Server就已经可以运行了。如果消息代理在其他地方运行,而不是在localhost,或者使用了非默认端口,又或者我们修改了访问代理的凭证信息,就需要在Config Server本身的配置中添加一些属性了。

如果采用RabbitMQ绑定,那么application.yml中的如下条目可以用来重写默认值:

1
2
3
4
5
6
spring:
rabbitmq:
host: rabbit.tacocloud.com
port: 5672
username: tacocloud
password: s3cr3t

虽然我们在这里设置了所有的属性,但是在你的RabbitMQ代理中只需要设置与默认值不同的属性即可。

如果使用Kafka,可以使用类似的属性:

1
2
3
4
5
6
spring:
kafka:
bootstrap-servers:
- kafka.tacocloud.com:9092
- kafka.tacocloud.com:9093
- kafka.tacocloud.com:9094

你会发现,这些属性来源于第8章我们学习Kafka消息时的配置。实际上,配置自动刷新功能的RabbitMQ和Kafka后端与在Spring中使用代理的其他场景非常相似。

创建Gogs的通知提取器

对于每个Git实现来说,webhook POST请求所携带的内容会有所不同。所以,对于“/monitor”端点来说,很重要的一点就是在处理webhook POST请求时能够理解不同的数据格式。在幕后,“/monitor”端点会有一组组件来检查POST请求,试图弄清楚请求来自哪种Git服务器,然后将请求数据映射为通用的通知类型,并发送至每个客户端。

Config Server对多个流行的Git实现提供了开箱即用的支持,比如GitHub、GitLab和Bitbucket。如果你使用其中的某一个实现,那么不需要任何额外的操作。在我编写本书的时候,Gogs还没有得到官方支持[^3]。因此,使用Gogs作为Git实现的话,我们需要在项目中提供一个Gogs的通知提取器。

程序清单14.1为Taco Cloud集成Gogs时我所使用的通知提取器。

程序清单14.1 Gogs的通知提取器实现

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
package tacos.gogs;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.cloud.config.monitor.PropertyPathNotification;
import
org.springframework.cloud.config.monitor.PropertyPathNotificationExtractor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 300)
public class GogsPropertyPathNotificationExtractor
implements PropertyPathNotificationExtractor {
@Override
public PropertyPathNotification extract(
MultiValueMap<String, String> headers,
Map<String, Object> request) {
if ("push".equals(headers.getFirst("X-Gogs-Event"))) {
if (request.get("commits") instanceof Collection) {
Set<String> paths = new HashSet<>();
@SuppressWarnings("unchecked")
Collection<Map<String, Object>> commits =
(Collection<Map<String, Object>>) request
.get("commits");
for (Map<String, Object> commit : commits) {
addAllPaths(paths, commit, "added");
addAllPaths(paths, commit, "removed");
addAllPaths(paths, commit, "modified");
}
if (!paths.isEmpty()) {
return new PropertyPathNotification(
paths.toArray(new String[^0]));
}
}
}
return null;
}
private void addAllPaths(Set<String> paths,
Map<String, Object> commit,
String name) {
@SuppressWarnings("unchecked")
Collection<String> files =
(Collection<String>) commit.get(name);
if (files != null) {
paths.addAll(files);
}
}
}

GogsPropertyPathNotificationExtractor如何运行的细节与我们的讨论没有太大关系,并且在Spring Cloud Config Server内置对Gogs的支持之后,就更加无关紧要了。所以,我不会对它进行过多的介绍,将它放在这里只是为了让你在使用Gogs的时候,可以作为参考。

在Config Server的客户端中启用自动刷新

在Config Server客户端启用属性的自动刷新比Config Server本身会更加简单。我们需要添加一项依赖:

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

这样会添加AMQP(如RabbitMQ)Spring Cloud Bus starter到构建文件中。

如果使用Kafka,就需要添加如下的依赖:

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

对应的Spring Cloud Bus starter准备就绪之后,启动应用的时候,自动配置功能就会发挥作用,应用会自动将自己绑定到本地运行的RabbitMQ代理或Kafka集群上。如果你的RabbitMQ或Kafka在其他地方运行,那么我们需要在每个客户端应用上像Config Server本身那样配置它们的详细信息。

Config Server及其客户端都配置成了支持自动刷新。将它们启动起来,并对application.yml做一下修改(任意修改都可以),当将该文件提交至Git仓库的时候,我们会立即看到它在客户端应用中生效。

[^1]: GitHub没有可选webhook的下拉列表。在点击Add Webhook按钮之后,会直接出现创建webhook的表单。
[^2]: 在Docker容器中。localhost指的是容器本身,而不是Docker宿主机。
[^3]: 作者给Config Server项目提交了一个支持Gogs的pull request。在它合并进去之后,本书的这个章节就没有必要关注了。目前,作者的这个pull request经修改后,已经合并到了Config Server中。——译者注