简介
Joris Kuipers 讨论了如何在 Spring Boot 应用中实现动态的运行时配置更新,以避免重启。他介绍了 Spring Cloud Context、@RefreshScope 注解和 refresh Actuator 端点的用法,并演示了这些机制如何实时生效配置更改(包括 ConfigurationProperties 和 @Value 字段)。演讲包含使用文件配置和外部配置后端(如 Spring Cloud Consul)的现场演示,并涵盖了使用 Kubernetes ConfigMap Watcher 等高级主题以及在刷新过程中出现验证和绑定错误的常见陷阱。
目录
-
引言:Panta rhei 与动态配置的需求
-
启动时配置的问题
-
Spring Cloud Context 与配置重载
-
演示:配置属性与手动刷新端点
-
引入 @RefreshScope
-
@RefreshScope 的工作原理(目标代理)
-
通过
/env端点进行临时更新 -
使用外部后端实现自动刷新
-
演示:使用 Spring Cloud Consul 自动刷新
-
陷阱:绑定和验证失败
-
Kubernetes 动态配置与 ConfigMap Watcher
-
最终考量与最佳实践
-
总结与问答
引言:Panta rhei 与动态配置的需求
好的,欢迎大家。我们开始吧。我知道还有人正在进来,但我希望能利用好这段时间,因为我们今天要讨论的主题是 Spring 动态配置或刷新。
我给大家看的这张幻灯片上是一点荷兰艺术,实际上这是 MC Escher 的作品,上面还有一句希腊语:“Panta rhei”。它的意思是万物流转,即没有事物能保持不变,对吧?一个东西会随着时间而变化,这对于配置来说也是如此。
每个应用都有某种配置。它允许你拥有一个构建版本,比如你的 fat JAR 或容器镜像,但你可以通过更改与应用本身分离的配置来改变它的行为方式。
我们都了解这一点,这样做的一个好理由是拥有环境特定的配置,对吧?你连接生产数据库的 JDBC URL 很可能与暂存环境的不同,诸如此类的例子有很多。
但显然,你也可能需要在环境内部进行更改。也许我的数据库凭据已更新,我需要轮换凭据;或者我想更改缓存的一些配置设置,或者是我希望更新的功能标志。这正是我们这次演讲要讨论的主题:在一个正在运行的应用、一个环境中进行的更改。
启动时配置的问题
因为重要的是要认识到,Spring 中的配置是在启动时读取的。为此,Spring 有一个“环境”的概念,基本上就是你的应用上下文,它有一个属性源列表。属性可以来自不同地方,例如类路径、文件系统或外部配置,我稍后会展示一些示例。
由于它是在启动时读取的,默认情况下,更改是不可见的。你可以尝试更改文件系统中的配置文件,但你的应用不会知道,也不会关心,直到你重新启动整个应用。
这意味着我们通常的工作方式是:要使配置更改生效,你必须重启应用,但这并非总是件好事,因为你会损失很多东西。显然,你会损失可用性,你的应用需要停机再启动。因此,如果你想保持可用,就必须运行多个实例并进行滚动重启。
你会丢失应用运行期间可能积累的所有内存中状态,例如内存缓存。你必须重启时,所有这些都会丢失。
但我们在主题演讲中也看到,你实际上会丢失 JVM 累积的许多状态。JIT 编译器为你所做的一切都消失了,这可能会产生非常显著的影响。
我们再次在主题演讲中看到一些幻灯片显示,你的应用可能需要 30 秒以上才能预热。实际上,我认为这还是乐观的。那仅限于你的应用经常做同样的事情,JIT 立即介入的情况。
我有一些集成服务,应用可能需要数小时才能恢复到重启前那样快速地进行涉及 XML 处理的远程调用。所有这些都消失了。
如果我告诉你有一个更好的方法呢?你实际上不必仅仅为了获取一些配置更改而重启你的应用。
顺便说一句,这是上个世纪一部电影中的场景。他们做了一次重载,而我要谈的正是配置重载(config reloads),这是我们今天的主题。
Spring Cloud Context 与配置重载
那么,在 Spring 中进行配置重载意味着什么?事实证明,有一个名为 Spring Cloud Context 的项目。Spring Cloud 就像是一个项目伞,但在其核心有一个公共模块,其中包括 Spring Cloud Context。它提供了动态重载配置的支持。
它提供了一些功能。其中一个很多人不知道的功能是,你可以使 Environment Actuator 端点(一个标准的 Boot 端点,用于查看带有所有属性源的环境)变为可写。
因此,你实际上可以 POST 配置更新。这通常不是你希望在生产环境中使用的方式,因为它只是一个临时更改,重启应用后就会消失。但在某些暂存环境的用例中很有用。
如果你这样做,任何使用 @ConfigurationProperties 标注的 Bean 都会被重新绑定。对象仍然是同一个,但它会获得新的值,并在应用运行时可见。
你也可以用它来更改日志级别。这不是一个非常重要的用例,因为 Actuator 已经提供了一个专门的端点来更新日志,但这很有趣。
因为当你在静态配置中进行更改时,也可以这样做。你可以在文件系统、外部属性等中更改配置,然后你可以告诉你的应用,"我希望你刷新(refresh)"。这通常是你使用这些功能的方式。
它的作用与 POST 到 Environment 端点相同,它将确保配置属性被重新绑定,但它也做了一些其他的事情。
演示:配置属性与手动刷新端点
我将通过一个简短的演示来解释这一点。我准备了一个应用,它是一个非常简单的 Spring MVC 应用。
它有两个配置属性类,这是一个使用 @ConfigurationProperties 标注的类示例。如你所见,它有一个名为 mutable-config 的前缀,然后有一个名为 key 的属性,它有 getter 和 setter,因此是可变的。
我们还有一个不可变版本。它有构造函数和 getter,但没有 setter。这两个东西都被注入到了我的控制器中。
你可以看到我有一个 REST Controller,不,实际上是一个标准的 Spring MVC Controller。它获取了这两个配置类,也注入了一个 @Value 来展示我们也可以这样做。然后这三个配置源被放入一个对象中并进行渲染。我想这个应用应该已经在运行了。
让我来展示一下。如果我现在转到浏览器,我们可以刷新,这就是我们得到的结果。这是一个设计精美的应用(因为我是后端开发者),我们可以看到基本键、可变配置键,并看到一些值。这些值都被命名为 blah blah blah from file system application properties。
原因是我们回到应用中,可以看到在类路径上的 application.properties 旁边,我有一个 application.properties,它包含了我们刚刚看到的应用中渲染的配置。
但现在,这个应用没有启用任何重载功能,它没有使用 Spring Cloud 或任何东西。所以,我可以在这里进行更改,我可以写 updated,然后我可以尝试刷新,但显然什么都不会发生,对吧?我需要重启才能看到这个更改。
那么,我们能做些什么来让它实际看到我对这个文件所做的更改呢?如果我们查看 POM,你会看到我注释掉了一个名为 Spring Cloud Starter 的依赖项。我现在要启用它。
现在当我重启我的应用时,它仍然可以工作,但现在当我查看 Actuator 的概览时,它会显示这里有一个之前没有的 refresh 端点。我可以通过调用这个刷新端点来确保我的应用更新其配置。
让我们试一试。首先看看我当前的配置是什么:它仍然是一样的。但现在我要进入我的文件配置 application.properties 并更新它,写上 it's updated。
仅仅更改文件本身还没有做任何事情,对吧?我可以刷新,它仍然是一样的。我需要主动调用这个刷新端点来触发我的应用说:“我希望你重新读取你的配置状态。”
为此,我们可以向该刷新端点 POST 请求。有趣的是,我们可以看到对这个请求的响应。响应是 200 OK,但它也告诉我哪些内容发生了变化。你可以看到它捕捉到我更改了 mutable-key,但这是唯一被返回的更改。
在我的应用日志中,我可以看到类似的情况。你可以看到它刷新了 mutable-config-key 的键。现在当我回到浏览器并刷新时,我确实看到了新值。
请注意这个东西,它显示 “调用次数”。这只是我控制器中的一个原子整数计数器,所以每当我进行刷新时,它就会增加一。你可以看到它在增加。这证明了我没有实际重启任何东西,这仍然是同一个控制器。它从 6 变成了 7,它没有重置为 1,所以如果你需要证据,这就是证明这确实有效。
让我尝试一些别的东西。我们还有一个不可变的配置键,所以让我更新它。我进入我的 application.properties,我要更新这一个,同时,我们也更新基本键,对吧?我现在更新了所有三个。
我们首先要调用刷新端点,当然。然后我们进入浏览器,按下刷新,什么都没有,对吧?我没有在这里看到我的更改。
原因在于 Spring 实际上已经更新了环境。让我先给你看看。如果我们转到 localhost:8080/actuator/env,我可以在这里看到当前环境。如果我搜索不可变的东西,我确实看到我的值现在是 immutable config updated。
问题是,这个东西被注入到了我的控制器中。如果你回到控制器,你会看到它首先有一个直接注入的 basic-key 字段。所以,如果我不重启控制器本身,我就无法看到基本键的任何更改,因为我没有从环境中提取配置,而是在启动时创建时就注入了它。
对于不可变配置,情况基本相同:这个对象是不可变的,所以它不能被重新绑定。当然,如果这是你唯一能做的事情,那这将是一个巨大的限制。
引入 @RefreshScope
然而,幸运的是,有一个更好的方法。我们可以使用 Spring Cloud 提供的一个新注解,称为 @RefreshScope。当我们这样做时,我们实际上能够重新加载所有配置。
我将快速演示,然后解释它的工作原理。如果我们回到控制器,它现在被 @RefreshScope 标注了,对吧?同样,我也在不可变配置中做了。所以不可变配置现在也用 @RefreshScope 标注了。
我的应用仍在运行。在我重启它之前,我想快速向你展示一件事。你可能熟悉 Actuator 中还有一个 configprops 端点,你可以在其中看到配置属性 Bean 以及绑定到它们的值。
所以在这里我可以查找我的不可变配置属性,我可以看到这些值。由于信息量很大,有点难看清,但实际上你也可以只查看特定的属性。你可以在 URL 中看到我只请求了不可变配置的配置。
请记住这一点,它现在正如你所期望的那样工作。但现在我要重启我的应用。我要接收刷新范围的内容。
我的当前值是这样,对吧?我已经将它们都重置为默认值,所以如果我回到我的应用并刷新,这正是我们应该看到的。好了,调用次数是 1。让我增加几次,现在是 8,对吧?正如你所见。
让我们再次更改之前未被拾取的一项。实际上,让我们同时更改这两项。我进入这里,我要写 basic from fsps update,对不可变的那一项也做同样的更改,写 update。进入这个文件,调用刷新端点。
现在我们再次看到有两个键发生了变化,这之前就已经奏效了。但这次,如果我进去,我确实立即看到了新值,但也要注意,我的调用次数已经重置为 1。
@RefreshScope 的工作原理(目标代理)
这已经解释了正在发生的事情。当你有一个刷新范围时,Spring 会创建一个代理(proxy),它充当你的单例并被注入。在这个代理后面是你的真实对象,它有自己的状态和配置。
现在当我们调用刷新时,对于所有刷新范围的代理,它们都将获得一个新目标。所以 Spring 基本上只是丢弃了代理背后的旧实例,并创建了新的实例。它为所有刷新范围的 Bean 执行此操作,对吧?
所以它并不智能,它不会找出哪个 Bean 使用了哪个配置,然后说“哦,只有这个配置更改了,我只需要做这个”。它只是全面地重新创建所有具有刷新范围的 Bean。但好的方面是,由于代理的存在,它提供了一个稳定的引用。
[听众提问]
是的,问题是:“这是否也适用于 @PreDestroy 这样的生命周期注解?”实际上是适用的,我可以向你展示。
因为如果我们进入我的控制器,我为演示添加了一个名为 preDestroy 的方法。所以无论何时调用它,我都只记录 goodbye。如果我们查看应用的输出,我们确实会看到这一点,对吧?你可以看到 preDestroy 方法被调用了。如果我有一个 @PostConstruct,它也会为新的实例被调用。
所以你可以看到这真的只是一个自定义的生命周期,对吧?它不是一个单例,它只在一个刷新范围内,因此是 Refresh Scope。
还记得我给你们看过我们可以在这里查看配置属性吗?这是旧的配置。现在让我们刷新,看看会发生什么。
这是它默认显示的不可变配置属性的配置属性。它看起来很奇怪。它有各种我没有配置的属性,它有一个 pre-filtered,它有一个 targetSource。这是因为这个东西现在显示的是代理。它没有显示背后的配置属性,你第一次看到可能会感到困惑。
它实际上是有道理的。好消息是,当我们转到配置属性时,在这个顶级的刷新范围代理旁边,你实际上可以看到另一个,因为在这里你可以看到我们有一个名为 scopedTarget.immutableConfig 的 Bean,它实际上是支持对象,它仍然拥有我的常规键和常规值,对吧?所以你仍然可以在这里找到当前的配置。
Ephemeral Updates with the /env Endpoint
在继续演示之前,再展示一件事。我在一张幻灯片中向你们展示了我们可以通过 POST 请求到 Environment 端点来进行实时更新。
如果我们进入我的 HTTP 文件,你可以看到这里有另一个请求。我在这里配置了一个请求,我正在向 Actuator 的 env 端点 POST 请求,我要将我的可变键更改为以 from the environment endpoints 结尾的东西。
我们可以看到这个值确实来自这里,我可以再次调用它。我们将得到一个响应,显示哪些键已更改。如果我现在进入应用并进行刷新,我们确实可以看到它获取了一个新值。
然而,这个值只是来自一个 POST 请求,对吧?如果我重启我的应用,这个值就会消失,它不是持久化的。
我真的想不出这在生产环境中的好用例,对吧?你绝不希望看到没有持久配置源支持的配置,并且它会在重启时发生变化。
但对于某些用例,例如暂存环境中,你引入了一个功能开关,然后你的测试人员过来对你说:“嘿,你知道吗,我想测试启用这些功能开关和禁用这个开关的组合会发生什么?你能让应用重启吗?”
因为测试人员通常没有能力自己做这种事情,他们需要进入 Kubernetes 重启应用之类的。但现在你可以给他们提供一个端点,说:“你可以这样做。”当然,前提是你确保所有相关的代码要么是从可变配置属性中读取,要么是用 refresh scope 标注的,对吧?我认为这是一个非常好的用例。
顺便说一句,要让它工作,你必须明确启用它,因为这是一个危险的功能。所以你可以在这里看到我将管理端点的 post-enabled 设置为 true,这就是它工作的原因。出于好的理由,它默认不工作。
当我执行这些操作时,幕后发生的事情是,当我 POST 到刷新端点时,会触发一个事件,有一个监听器会实际重新绑定这些配置属性。它还会重新配置日志级别。
但重要的是要认识到,正如我已经提到的,@RefreshScope Bean 是这些代理,无论何时你调用刷新,它们都会获得一个新目标,对吧?
这不是一个智能机制,它不会确定哪个 Bean 使用了哪个配置。它只是全面地刷新所有刷新范围的 Bean,这就是它的工作原理。
自动刷新与外部后端
这很好,但正如你在我的演示中看到的,我现在每次进行更改时都需要调用这个刷新端点。这不是你通常想要的。你想要的是自动刷新,对吧?我有配置,我进行了更改,咻,它在应用中可见。
当然,好消息是你可以做到,而且这实际上得到了许多配置项目的支持。Spring Cloud 实际上有很多支持外部化配置的项目。
今天早上的主题演讲中,Josh 演示了 Config Server,它默认由 Git 仓库支持,但它支持许多不同的后端。我将演示一个名为 Spring Cloud Consul 的东西,因为它易于演示。HashiCorp 也有 Vault,它是一个成熟的密钥管理器。
我将演示 Kubernetes 的支持。我想在这里为 Spring Cloud AWS 团队点赞。他们做得非常出色,为 AWS Parameter Store、AWS Secrets Manager、使用 S3 进行配置等提供了支持,所有这些也支持重载。
这通常的工作方式是,会进行一些轮询,或者如果你幸运的话,会有一个更高效的回调机制,告诉你的应用:“啊,有东西变了。”
使用这些功能有一些很好的理由,例如你可以在微服务之间共享配置等等。这不是深入探讨这些内容的演讲,但这些绝对值得你去了解。
可选地,这些应用可以使用一个名为 Spring Cloud Bus 的项目。Spring Cloud Bus 是一个抽象,它使用 AMQP(通常是 RabbitMQ)或 Kafka,可以广播这些事件。
我可以看看有多少人熟悉 Spring Cloud Bus 这个名字吗?就像我猜的那样,大概一半。这也是我的经验,所以这次演示中我不会深入介绍它。但请注意,有这种技术可以让你(尤其当你有很多应用实例时)通过发布-订阅机制来发布这些事件。
演示:使用 Spring Cloud Consul 自动刷新
我将演示刷新功能,为此我将使用一个名为 Spring Cloud Consul 的东西。
我现在有一个应用,它与我刚刚演示的应用非常相似,但它依赖于一个名为 spring-cloud-starter-consul-config 的东西。我正在本地主机上以 Docker 镜像运行 Consul,所以 Consul 已经准备好了。
这里运行着 Consul,Consul 是一种服务发现机制、一个服务注册中心,但它也有一个键值存储,你可以用它来存储配置。这就是我们要使用的。如你所见,它目前是空的。
我将创建一个新的配置键。默认情况下,从这里获取配置的 Spring 应用的配置需要以一个名为 config 的虚拟文件夹开头,然后你可以输入一个键。
所以我们现在这里是 config(只是一个前缀),然后是我的应用名称(我的应用名为 consul,所以它会获取 consul 文件夹下的配置),然后是我的属性名称,也就是 basic-key。
我将给它一个值。我要写一些类似 value from consul 的东西,这样我们就可以看到它确实来自这里。保存。现在启动我的应用。
我们可以看到控制器仍然在做同样的事情,它有基本键,它有两个配置属性,并且被标记为 refresh scope,所以它会获取所有的更改。
我们现在看到的第一件事是,我们看到了 value from consul,对吧?所以在启动时,它看到了我的更改。当然,这与动态刷新还没有任何关系,它只是表明我们可以在启动时加载配置。
但现在我可以回去说我想更改这个东西。我们转到 Consul,进入这里,获取基本键,value from consul updated。保存。
现在我不会调用刷新端点或任何东西。我只是回到我的应用,进行刷新,你可以看到它已经更新了,对吧?
魔法?实际上不是魔法。发生的是长轮询(long polling)。应用现在发出一个请求,然后 Consul 保持响应开启,直到确实发生更改。
你可以看到它再次获取了这里的刷新键,但我们不必调用刷新端点。这是配置中的一个更改。
例如,我的 mutable-config-key 仍然是从类路径中读取的,这是我设置的一个默认值。我也可以覆盖它。我可以写 from consul 或类似的东西,以确保你们能看到它来自这里。保存。回到应用,刷新,你可以看到它也获取了那个值,对吧?所以我也能够引入新值来覆盖优先级较低配置源的值。
陷阱:绑定和验证失败
这很好,但有一些陷阱。我想给你们展示一些重要的东西。我在我的可变配置属性对象上使用了 @Validated 标注。
这是一个标准的 Spring Boot 功能,你可以说“我想对我的配置执行验证”。因此,如果启动时出了问题,我的应用将无法启动,并会打印出一条说明问题的错误信息。
我这里有两件事:这是一个 int,所以显然我只能将数字绑定到它;这个东西有一个约束,要求字符串至少有四个字符才能工作。
首先看看如果我输入一个少于四个字符的值,会发生什么。我再次进入 Consul,获取那个键,然后我将其更改为 abc,显然只有三个字符。保存。
现在,在向你展示其他内容之前,我先给你看看我的应用现在会做什么。如果你在这里刷新,你会看到它只显示 abc。但我们有验证,对吧?
现在看看我的应用。如果我们查看控制台,哦,这里发生了一些事情。你可以看到这个东西现在每秒都在吐出错误,它告诉我创建可变配置属性时出现错误,因为它无法绑定。
但这是一个验证错误,通常在启动期间会阻止你的应用启动。但这个应用已经在运行了,所以它仍然会绑定。
让我先修正它,然后再给你们看看其他东西。我们通过添加一个额外的字母使它再次成为有效的东西。保存。我们将看到新值,并且会看到这个东西停止输出所有这些警告,并再次告诉我键已更改。
我们恢复了正常。我还有这个数字。让我查找确切的值:mutable-config-number。让我们创建一个键。我将创建一个新键,将其初始值设置为一个有效值,例如 one。保存。转到这里,刷新,我们看到了 1,符合预期。
现在让我们回到 Consul,并说这个数字将是 abc。这不同,对吧?这不是验证问题,这个东西无法绑定。
事实证明,它不会是 1,因为 1 是旧值,而且它是一个即将消失的 Refresh Scope Bean。所以因为它实际上无法绑定,它只会是默认值。int 的默认值是 0,没错。
如果我们保存,我们去这里,刷新。哦,有意思,没想到会发生这种情况。好的,它是 1。我稍后会弄清楚为什么会这样,但你确实看到这次它根本无法绑定,而这是不同的行为,对吧?
所以测试这些东西很重要。你可以看到测试有多重要,以至于我甚至弄错了。因为它在运行时没有像我们期望的那样做。
现在,你如何确保捕捉到这种情况?想象一下你在生产环境中运行,你启用了这些功能,有人输入了一个错误的值,你的应用开始疯狂记录日志,它现在运行在错误的配置上,但没有人注意到。这显然是一个真正的问题。
所以,要知道 Spring Cloud 还会给我提供一个额外的健康指标。如果我们转到 actuator/health,我启用了详细信息记录。
你可以看到有一个 Consul,有基础指标,但如果我向下滚动一点,你会看到现在还有一个 Refresh Scope 指标。Refresh Scope 指标实际上显示为 DOWN,因为它在绑定时遇到了问题。
所以你可以在这里看到存在问题。在这种情况下,因为默认情况下所有健康指标都由 Health 端点使用,所以我的整个应用状态现在是 DOWN。不一定非得这样。
你实际上可以更改设置,说:“我仍然希望 Refresh Scope 指标存在,但我不希望它用于默认的健康检查。”但你总是可以单独查询它。
所以我可以转到 /refreshscope,只查看健康指标状态,对吧?你可以把它放在一个组里,Spring Boot 在这方面非常灵活。
但重要的是要认识到这一点,对吧?你可能会破坏一些东西,通常这会阻止你的应用启动。在这里,它会产生一些略微不同的影响,但可以监控这些情况并在发生时采取行动。
[听众提问]
它应该会覆盖,因为它只会以更高的优先级更新总体的属性源集合。你是说如果我使用刷新而不是 Consul?
它的作用是相同的。所以基本上,自动刷新所做的与你亲自调用刷新端点所做的一模一样。
[听众提问]
是的,没错。正如我刚才所说,绑定和验证,如果你使用它们,可能会失败。如果你绑定的不是字符串而是其他类型,绑定可能会出问题。如果你使用验证,显然也可能失败,但幸运的是,我们有这个刷新指标。
你应该考虑如何使用它。如果你打算在生产环境中使用这些功能,一定要确保配置警报,对吧?
我不确定你是否想把它放在默认的健康端点中,然后在每次刷新时让你的应用进入崩溃循环,但这可能正是你想要的,对吧?这有点取决于你期望能够在运行时更改的配置类型。它可能不会是所有配置。只有有限的配置集会受益于此,但请记住这一点。
Kubernetes 动态配置与 ConfigMap Watcher
展示 Consul 很好,但来参加技术会议,当然不能不谈 Kubernetes,因为 Kubernetes 是我们又爱又恨的平台,因为每个人都被迫在上面运行他们的应用。
我准备了一个演示,我在笔记本电脑上运行 Kubernetes。我总是拒绝做这种事,但为了演示,我会做,因为很多人都在 Kubernetes 上运行。
如果你在 Kubernetes 上运行,你就知道 Kubernetes 上有一个名为 ConfigMap 的东西,这是在 Kubernetes 内部处理配置的默认方式。应用可以看到 ConfigMap,可能是因为它们被绑定为环境变量,或者被绑定到一个文件,或者被绑定到多个文件,Spring Boot 支持所有这些机制。
你也有专门的 Secrets,这也是配置值,但它们是密文。有一个名为 Spring Cloud Kubernetes 的项目,它实际上允许你刷新这类配置值。
它有一种机制,应用本身可以动态重载其配置,例如它创建一个 Socket 连接或进行轮询。然而,这个支持已经存在很多年了,但自 2020 年以来已被弃用。
所以如果你发现这个支持,它使用的配置属性是 spring.cloud.kubernetes.reload.enabled,你不应该再使用它了。它已经被弃用了 5 年。
你应该使用一个名为 Configuration Watcher 的东西。这是一个单独的应用,你将其部署到你的 Kubernetes 集群中。这个应用会监控 ConfigMap,如果你愿意,也可以监控 Secrets(这是可选启用的),以查找更改。
当有东西更改时,它会确定这个配置是针对哪个应用的,然后它会调用该应用的刷新端点。这基本上就是它的工作原理。
它会为此触发事件。它不是对所有东西都这样做。你可能有一个 ConfigMap,你觉得“我不需要在这个东西更改时进行刷新”。
所以默认情况下,ConfigMap 在 Kubernetes 中需要有一个标签,告诉 Kubernetes 配置 Watcher“应该考虑这个东西”。
如果该标签存在,并且它找到了一个名为 my-application 的 ConfigMap,它会假设你还有一个名为 my-application 的 Deployment 在运行,因此这是该应用的配置。然后它默认调用 Actuator 刷新端点,或者如果你使用 Spring Cloud Bus,它也可以使用它。
它有各种配置选项。你通常不必自己构建这个东西,它有一个预制的 Docker 镜像,你可以用环境变量来配置它。
我想指出一件事:假设你的应用通过将其挂载到文件系统上作为属性文件来使用 ConfigMap,这是一种非常常见的做法。那么,如果你用更改后的 ConfigMap 更新 Kubernetes,应用需要一段时间才能在挂载的文件中实际看到这个更改。
因此,如果配置 Watcher 立即告诉你的应用“你的配置已更改”,它会说:“哦,太棒了,我要寻找新配置”,但它仍然看到的是旧配置。
所以默认情况下,这个东西会等待两分钟,才告诉你的应用配置已更改,以给 Kubernetes 时间将更改反映到你的挂载文件系统中。我在演示中将其设置为 30 秒。经过试验和错误,发现它足够了,但我们来看看。
你也可以配置其他东西,例如你可能有共享配置,所以你希望 ConfigMap 能够通知多个应用。你可以设置是哪些应用。
演示:Kubernetes ConfigMap Refresh
我们将进行另一个演示。我的 Kubernetes 集群已经在运行了,但我将向你展示我将用于此的配置。
我们来看一下我已经演示过的基本应用,这是我部署到 Kubernetes 的应用。但在我的 application.properties 中,我添加了这一行。
你可以看到我有一个 spring.config.import,我说如果它存在,请确保导入 /etc/config/application.properties。这将是我的 ConfigMap,它将被挂载到容器的文件系统中,对吧?
如果它在那里,它就会拾取它。这是我对应用所做的唯一更改,因为我的应用本身不需要知道 Spring Cloud Kubernetes 或任何东西。它是我要部署的这个单独应用。
这个应用就是这里的 Config Watcher Deployment。我不是自己写这个文件的,我是从文档中复制粘贴的,因为他们有一个预制的配置 Watcher 的部署描述符。
我只做了一件事,正如我解释过的,我确保配置了这个超时。有一个名为 spring.cloud.kubernetes.configuration.watcher.refresh.delay 的东西,我将其设置为 30 秒,而不是两分钟,否则我们都要等两分钟才能看到变化,我没有那么多时间。
其他一切都是默认的,这里有很多配置,因为这个东西需要很多权限,它需要能够查看配置映射,可能还有密文,它需要能够调用其他应用。
这是我的应用部署。它只是告诉我我有我的应用,我有一个它的 Docker 镜像,所以我使用了内置的 Buildpack 支持来创建它,我正在部署它。
唯一重要的是这个:Volume Mount。你可以看到我将 ConfigMap 挂载到了 /etc/config 上。这就是为什么我可以从那里导入 application.properties。
最后,我们有 ConfigMap。ConfigMap 看起来像这样,非常简单。你可以看到,在这种情况下,我将 application.properties 作为单个内容放入,它有我的三个值。
你可以看到这已经部署在 Kubernetes 中了。我可以在控制台上的 k9s 之类的工具中查看,但实际上 IntelliJ 对使用 Kubernetes 有很好的支持。
在这里,我可以看到我的 Docker Desktop Kubernetes 集群正在运行,在我的配置中,我可以看到一个名为 basic 的 ConfigMap,它有一个 application.properties。如果我打开它,我可以看到它确实是我在这里配置的内容,现在在 Kubernetes 中可用。
让我们看看应用。它应该已经在运行了,所以它被绑定到了另一个端口号,这样就不会冲突。在这种情况下,我已经做了一个小演示,所以第一个已经更新了,我们看到了另外两个值。
让我们更新可变配置,看看它是否有效。我将进入我的 ConfigMap,我将更新可变配置,将其设置为 updated during the talk。我将右键单击,说我要将它应用到我的集群。
你可以看到它执行了 kubectl 操作。现在它应该在我的集群中运行了,在接下来的 30 秒内,它将更新我的应用。
如你所见,它获取了前一个 ConfigMap 更改,对吧?所以这个现在恢复到默认值了,但这个还没有被获取。再说一次,这可能是因为它需要一点时间,而且我的 30 秒在准备工作中是足够的,但在我进行现场演示时可能就不够长了。
好消息是,我仍然可以演示它确实有效,只需再做一次更改,因为那将再次触发配置端点更新,我们可能只会比当前配置落后一个版本,因为我的 30 秒不够长,但这没关系。
它仍然展示了整个机制,以及为什么这种延迟很重要,你需要弄清楚适合你的环境的正确值,宁愿长一点也不要短,否则你会看到这种情况。
好了,这就是我所期望的,对吧?我们可以看到我的第一次更新成功了,但因为它还没有看到最终的更新,所以它没有捕捉到另一个。所以 30 秒可能不够。
如果你将整个 ConfigMap 作为文件挂载,情况尤其如此。如果你以另一种方式使用它,例如通过环境变量,我认为会更快,但请自己进行测试,并确保你知道在你的特定集群和环境中,这些东西是如何工作的。
但这表明我们也可以对 Kubernetes 配置进行动态重载,而且你的应用本身不需要知道 Spring Kubernetes 项目,它只需要启用基本的 Spring Cloud 配置支持,因为实际更新是由一个单独的应用完成的。
看到这个之后,有谁认为“嘿,这可能对我的应用很有用”?好的,相当多的人,这显然是我这次演讲的目的。
最终考量与最佳实践
当你开始自己研究这个问题时,有一些事情你需要记住。
首先,如果你开始阅读文档,你会发现很多都是过时的,因为它仍然提到了一个名为 Bootstrap Context 的东西。这是 Spring Cloud 的一个旧概念,他们会先创建一个父应用上下文,然后再创建一个子上下文。大部分内容已经过时了,因为 Spring Boot 已经吸收了大部分逻辑,所以注意这一点即可。
此外,正如我演示的那样,除非你想在所有地方都使用 Refresh Scope,否则请确保在使用配置属性类时,你始终调用访问器,也就是调用 getter。这样你就能看到最新值。
这可以让你不必在所有地方都使用 Refresh Scope,这可能是件好事,对吧?你不一定总是希望拥有 Bean 的新实例。
此外,保护你的 Actuator 端点,正如我提到的。在 env 端点上启用 POST,我通常不会在生产环境中这样做,因为我实在想不出好的用例,但在暂存环境肯定很好。
但在生产环境中,请确保你的刷新端点不向全世界开放,对吧?你不希望有人通过不断让你刷新所有应用来发动拒绝服务攻击。
其他注意事项:当你使用 CRaC 时,这听起来很奇怪,但这是 Checkpoint Restore。这意味着你将通过基本上创建一个应用的预编译镜像来大幅减少启动时间。
它不像 GraalVM,它仍然是一个带有反射的完整 VM,但它通常已经包含了你的配置。因此,你可以将它与此结合起来,确保在使用 CRaC 时,你启动应用时仍然可以看到当前的配置值。
今天下午 2:00 在同一个房间会有一场专门讨论 CRaC、Project Leyden 和其他改进应用启动性能的倡议的演讲。
使用此功能的一个用例是轮换数据库凭据。很多人都这样做:你创建一个 DataSource Bean,并将其设置为 refresh scope。然而,不要对 Hikari 这样做。
如果在你有开放连接时将其终止,Hikari 不会喜欢,而 Hikari 是默认的连接池。所以如果你希望能够使用新的连接设置动态重载数据库配置,请切换到不同的连接池。
如果你在 AWS 等平台上,并且使用托管数据库,你可能不需要这个。他们有专门的支持,对吧?我不会详细讨论,但要知道 AWS 本身就有一个库,它支持重载凭据和重新连接等功能,所以这个担忧也得到了其他选择的覆盖。
最后,如果你想将它与 GraalVM 一起使用,不行,它不受支持。所以不要这样做。我无法说得更好听,事实就是如此。
正如我已经演示过的,请注意绑定错误和可能发生在你应用上的部分更新。所以你最终需要测试这些东西,显然,因为这是你应用的核心,配置是一个非常重要的关注点。错误的配置可能会破坏你的应用,甚至更糟。
所以要测试它。使用健康指标来查看一切是否正常,并确保通过查看 Environment 和 Config Props Actuator 端点的输出来验证发生的情况,因为这些显示了 Spring 认为你当前配置的实际真相,这将对你有巨大帮助。
总结与问答
然后享受这个过程吧。因为就像我说的,这是一个鲜为人知的功能。这就是为什么我想做这个演讲,我觉得向人们展示这里实际可能发生的事情会很好,而不是总是不得不重启你的应用,等待它重新启动,然后说:“哦,现在我们有了新的配置属性”,对吧?
那么,感谢大家的参加。如果你想自己运行这些示例代码,可以在 GitHub 上找到。它有用于 Consul 的 Docker Compose,有 Kubernetes 部署描述符,所以你很快就能开始尝试这些功能。
我想我们还有两分钟,所以有什么问题吗?
[听众提问关于 @Conditional 注解]
不,@Conditional 用于确定在应用上下文和 Bean 方面实际创建了什么,所以不行。条件属性用于定义应用上下文本身的内容。这不会重启你的应用上下文,它只刷新配置,所以它不会生效。
简而言之,不行。说“我要对一个属性设置条件,然后更改该属性”是没有意义的,因为你需要重启应用上下文才能实现。
实际上有支持这种功能的,我认为是通过一个自定义端点或通过一个属性,你可以说:“当我刷新时,我希望只是重启”,但这会是一个不同于我刚才演示的用例。
不,所以对于只在启动时使用的内容,答案基本上是必须启动。
还有其他问题吗?
好的,那么非常感谢大家,祝大家会议愉快。
