在微服务架构中,我们会使用一个分布式的“配置中心”来管理所有的配置文件和配置项,本章节将介绍 Nacos 配置中心的特性,以及这些特性在微服务体系中所发挥的作用。在 Spring Boot 应用中,我们习惯于使用传统的配置管理方式,将各种配置项都维护在 application.yml 或 application.properties 文件中。从完成业务逻辑的角度来看,这样做是没问题的。但在微服务架构中,我们可以采取一种更“优雅”的方式组织配置文件,实现高效灵活的配置管理。
1 传统配置管理的弊端
先回顾下一下传统的配置管理途径都有哪些,这些途径在使用上有哪些弊端。然后,我们再来了解微服务架构下的“配置中心”是如何解决这些问题的。
我们通常可以采用四种途径在程序中指定配置项,它们分别是硬编码、配置文件、环境 / 启动变量、数据库动态获取,我们先来了解一下这四种配置管理方式是如何实现的。
硬编码:最简单粗暴的方式,在 Bean 初始化的上下文中,直接通过在代码中 hardcode 的方式指定配置信息;
配置文件:使用 application 和 bootstrap 配置文件来设置配置项,这是目前比较“优雅”常用的方式;
环境 / 启动变量:通过操作系统的环境变量,或者启动命令中的 -D 参数传入配置项;
数据库 / 缓存动态获取:将配置项保存在数据库里,每次执行一个 select 语句实现动态查询。
看似我们有了不少办法来实现配置管理,但实际上,以上的几种途径或多或少都有一些弊端。
从职责分离的角度来讲,硬编码无法将“业务逻辑”与“配置管理”拆分开来。尽管硬编码实现起来最为简单,它仍然是我最不推荐的一种方式。
从灵活性的角度来讲,无论你用的是硬编码、配置文件还是环境 / 启动变量的方式,只要你需要对配置项进行变更,你就必须对代码 / 启动命令进行修改,然后重新打包并部署应用,无法做到在运行期灵活变更配置项。
数据库 / 缓存动态获取的方式和上面几种相比,具备了一定的灵活性。但从版本控制的角度来看,配置项也是一种“代码资源”,采用数据库 / 缓存控制并不能很好地实现配置版本的控制和历史版本回滚。而面对高并发场景时,如果我们采用数据库方案,还要时刻关注 DB 的性能指标,以免被流量打崩。
除此以外,多格式支持和安全性也是需要考虑的因素。对于用户名密码之类的敏感数据,如果明目张胆地放在代码库中,那么将显著增加“删库跑路”事件的发生几率。
到这里你会发现,传统的配置管理方式或多或少都存在着一些弊端。你也可能会问,分布式配置中心可以解决这些问题吗?答案是当然。接下来了解下分布式配置中心有哪些具体的作用。
2 分布式配置中心
在微服务的架构体系中,我们会使用一个中心化的分布式配置中心作为配置文件的管理者。
在应用程序端,我们只将一些必要的配置项添加到配置文件中(如 application.yml 和 bootstrap.yml),而大部分的配置项都被保存在配置中心集群里。客户端在启动的时候从配置中心获取所有的配置项,用于各个组件的初始化。
以 Nacos Config 为例:
高可用性:微服务组件的高可用性是首要目标。配置中心并不是一个中心化的单点应用,而是一个通过集群对外提供服务的组件。在一致性算法的基础上,集群中各个节点之间会互相同步配置数据,或者从统一数据源读取配置数据。即便个别节点挂掉,也不影响整个集群的可用性;
环境隔离特性:Nacos 支持通过 Namespace 属性指定当前配置项所在的环境,你可以为自己的应用系统创建开发环境、预发环境和生产环境,不同环境之间的配置文件是相互隔离的;
多格式支持:Nacos 支持多种不同格式的配置内容,你可以使用纯文本、JSON、XML、YAML 和 Properties 多种文件后缀;
访问控制:Nacos 实现了权限管理功能,你可以在控制台创建用户账号和权限组,限制某个账号可以访问哪些命名空间,并配置账号的读写权限(只读、只写、读写)。通过这种方式,你可以保障敏感信息(如数据库用户名和密码)的安全;
职责分离:配置项从 jar 包中抽离了出来,修改配置项再也不需要重新编译打包应用程序了,完美实现了配置项管理与业务代码之间的职责分离;
版本控制和审计功能:配置项也是一种代码,而且配置 bug 往往比代码中的 bug 造成的影响更大。因此,在微服务架构中我们需要确保配置中心具备完善的版本控制和审计功能。
从图中你可以看出,通过 Nacos 的“历史版本”功能,你可以查看任何一个配置文件的历史修改记录,包括改动的时间和操作人。针对每一个改动记录,我们可以查看这一版本的配置详情,或者做线上配置项的回滚操作。
除了上面我们提到的功能以外,Nacos 还可以支持多文件源读取以及运行期配置变更。尤其是动态变更推送,更是微服务架构下不可或缺的配置管理能力。
Nacos 具备很高的灵活性,你可以在项目中指定从多个 Nacos 配置文件中获取信息,这些文件可以是不同名称、不同格式的配置文件。这个特性允许你对配置文件做更细致的“职责隔离”。比如你可以把 Redis 连接信息做成一个独立的配置文件,让集群中的所有应用消费同一个文件来初始化 Redis Connection。
当配置项发生变化的时候,服务端可以通过监听变更事件的方式,从 Nacos 服务器获取到最新的配置信息。这个功能就是配置项动态更新,它可以让你在不重启应用程序的前提下更新配置信息,这在微服务系统中大有用途。
列举几个配置项动态更新的使用场景,帮助你理解它的作用:
1.1 业务开关
动态配置的一个作用是通过业务开关控制功能的开启 / 关闭。比如在做主链路规划的时候,我们经常需要在一些非关键服务上预留一个“人工降级”开关,在业务运行期对特定业务做定向熔断。
对于一些大需求点的功能更新,经常涉及到上下游多个微服务的改动,但每个微服务的上线时间往往是不一样的。这时候我们就可以在代码中预留一个“业务开关”,在当前服务上线之初,开关处于关闭状态,待所有上下游服务都完成了上线之后,再通过开关开启新功能。如果出现异常情况,还可以通过这个功能开关切换回老的执行逻辑。
1.2 业务规则更新
对于一些更新比较频繁的业务数据,我们可以把这部分数据放到配置中心中。比如说在搭建新零售平台的商品中心的时候,会将一些运营文案信息部署到配置中心。这样一来,我们就可以根据运营活动随时更新资源位的布局、样式以及展位商品。
1.3 灰度发布验证
如果你即将发布新的配置项变更,但是在应用到整个集群之前你想先挑几台服务器测试一下,那么你可以使用 Nacos 的 Beta 发布功能,将配置项定向推送到特定 IP 地址的 Client 机器,完成线上测试。利用 Beta 发布 + 业务开关的组合,你还可以在线上定向开启特定 IP 服务器的业务开关,实现轻量级的灰度测试。
对于开发人员来说,“动态属性推送”应该是我们在工作中最常用到的功能点了,尤其在拥抱变化的互联网公司更是如此。因为互联网行业的需求变动非常频繁,如何巧妙地利用配置中心的动态推送功能,将“变化的需求部分”和“不变的代码部分”隔离开来,是开发业务场景时需要着重考虑的。
3 集成 Nacos Config 实现配置项动态刷新
接下来,将应用程序接入到 Nacos 获取配置项,然后再来实现动态配置项刷新。本章节选择 coupon-customer-serv 作为改造目标,因为 customer 服务的业务场景比较丰富,便于我们来演示各个不同的场景和用法。
接入 Nacos 配置中心的第一步,就是要添加 Nacos Config 和 Bootstrap 依赖项。
3.1 添加依赖项
我们打开 coupon-customer-serv 的 pom 文件,在 pom 中添加以下两个依赖项。
<!-- 添加Nacos Config配置项 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 读取bootstrap文件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
第一个依赖项是 Nacos 配置中心的依赖包。尽管我们已经在 customer 服务中添加过了 Nacos 的依赖项,但此依赖项非彼依赖项,初学者很容易搞混。Nacos 既能用作配置管理也能用作服务注册,如果你想要引入 Nacos 的服务发现功能,需要添加的是 nacos-discovery 包;而如果你想引入的是 Nacos 的配置管理功能,则需要添加 nacos-config 包。
第二个依赖项是为了让程序在启动时能够加载本地的 bootstrap 配置文件,因为 Nacos 配置中心的连接信息需要配置在 bootstrap 文件,而非 application.yml 文件中。在 Spring Cloud 2020.0.0 版本之后,bootstrap 文件不会被自动加载,你需要主动添加 spring-cloud-starter-bootstrap 依赖项,来开启 bootstrap 的自动加载流程。
为什么集成 Nacos 配置中心必须用到 bootstrap 配置文件呢?这就要说到 Nacos Config 在项目启动过程中的优先级了。
如果你在 Nacos 配置中心里存放了访问 MySQL 数据库的 URL、用户名和密码,而这些数据库配置会被用于其它组件的初始化流程,比如数据库连接池的创建。为了保证应用能够正常启动,我们必须在其它组件初始化之前从 Nacos 读到所有配置项,之后再将获取到的配置项用于后续的初始化流程。
因此,在服务的启动阶段,你需要通过某种途径将 Nacos 配置项加载的优先级设置为最高。
而在 Spring Boot 规范中,bootstrap 文件通常被用于应用程序的上下文引导,bootstrap.yml 文件的加载优先级是高于 application.yml 的。如果我们将 Nacos Config 的连接串和参数添加到 bootstrap 文件中,就能确保程序在启动阶段优先执行 Nacos Config 远程配置项的读取任务。这就是我们必须将 Nacos Config 连接串配置在 bootstrap 中的原因。
依赖项添加完成之后,我们就可以去配置 Nacos Config 的连接串了。
3.2 添加本地 Nacos Config 配置项
首先,我们需要在 coupon-customer-impl 项目的 resource 文件夹中创建 bootstrap.yml 配置文件。接下来,你需要在 bootstrap.yml 文件中添加一些 Nacos Config 配置项,下边是常用的配置项:
spring:
# 必须把name属性从application.yml迁移过来,否则无法动态刷新
application:
name: coupon-customer-serv
cloud:
nacos:
config:
# nacos config服务器的地址
server-addr: localhost:8848
file-extension: yml
# prefix: 文件名前缀,默认是spring.application.name
# 如果没有指定命令空间,则默认命令空间为PUBLIC
namespace: dev
# 如果没有配置Group,则默认值为DEFAULT_GROUP
group: DEFAULT_GROUP
# 从Nacos读取配置项的超时时间
timeout: 5000
# 长轮询超时时间
config-long-poll-timeout: 10000
# 轮询的重试时间
config-retry-time: 2000
# 长轮询最大重试次数
max-retry: 3
# 开启监听和自动刷新
refresh-enabled: true
# Nacos的扩展配置项,数字越大优先级越高
extension-configs:
- dataId: redis-config.yml
group: EXT_GROUP
# 动态刷新
refresh: true
- dataId: rabbitmq-config.yml
group: EXT_GROUP
refresh: true
下面,我就带你了解一下代码中的的配置项,我把这些配置项分为了几大类,我们分别来看一下。
文件定位配置项:主要用于匹配 Nacos 服务器上的配置文件。
namespace:Nacos Config 的 namespace 和 Nacos 服务发现阶段配置的 namespace 是同一个概念和用法。我们可以使用 namespace 做多租户(multi-tenant)隔离方案,或者隔离不同环境。我指定了 namespace=dev,应用程序只会去获取 dev 这个命名空间下的配置文件;
group:概念和用法与 Nacos 服务发现中的 group 相同,如未指定则默认值为 DEFAULT_GROUP,应用程序只会加载相同 group 下的配置文件;
prefix:需要加载的文件名前缀,默认为当前应用的名称,即 spring.application.name,一般不需要特殊配置;
file-extension:需要加载的文件扩展名,默认为 properties,我改成了 yml。你还可以选择 xml、json、html 等格式。
超时和重试配置项:
timeout:从 Nacos 读取配置项的超时时间,单位是 ms,默认值 3000 毫秒;
config-retry-time:获取配置项失败的重试时间;
config-long-poll-timeout:长轮询超时时间,单位为 ms;
max-retry:最大重试次数。
长轮询机制的工作原理:当 Client 向 Nacos Config 服务端发起一个配置查询请求时,服务端并不会立即返回查询结果,而是会将这个请求 hold 一段时间。如果在这段时间内有配置项数据的变更,那么服务端会触发变更事件,客户端将会监听到该事件,并获取相关配置变更;如果这段时间内没有发生数据变更,那么在这段“hold 时间”结束后,服务端将释放请求。采用长轮询机制可以降低多次请求带来的网络开销,并降低更新配置项的延迟。
通用配置:
server-addr:Nacos Config 服务器地址;
refresh-enabled: 是否开启监听远程配置项变更的事件,默认为 true。
扩展配置:
extension-configs:如果你想要从多个配置文件中获取配置项,那么你可以使用 extension-configs 配置多源读取策略。extension-configs 是一个 List 的结构,每个节点都有 dataId、group 和 refresh 三个属性,分别代表了读取的文件名、所属分组、是否支持动态刷新。
在实际的应用中,我们经常需要将一个公共配置项分配给多个微服务使用,比如多个服务共享同一份 Redis、RabbitMQ 中间件连接信息。这时我们就可以在 Nacos Config 中添加一个配置文件,并通过 extension-configs 配置项将这个文件作为扩展配置源加到各个微服务中。这样一来,我们就不需要在每个微服务中单独管理通用配置了。
3.3 添加配置文件到 Nacos Config Server
首先,我们在本地启动 Nacos 服务器,打开配置管理模块下的“配置列表”页面,再切换到“开发环境”命名空间下(即 dev 环境)。
然后,我们点击页面右上角的➕符号创建三个配置文件,coupon-customer-serv.yml(默认分组)、redis-config.yml(EXT_GROUP 分组)和 rabbitmq-config.yml(EXT_GROUP 分组)。
注意,添加配置时可能会出现报错,大概率是因为:
nacos 初始化sql与nacos版本不一致
nacos 2.1.0版本之后初始化数据库中config_info 和 his_config_info 表中新增了encrypted_data_key密钥字段
nacos.2.1.0 及之前数据库初始化脚本为nacos-mysql.sql,2.2.0 之后重命名为mysql-schema.sql
接下来,你就可以将原本配置在本地 application.yml 中的配置项转移到 Nacos Config 中了,由于 Data ID 后缀是 yml,所以在编辑配置项的时候,你需要在页面上选择“YAML”作为配置格式。
以 coupon-customer-serv.yml 为例,在新建配置的页面中,指定了 Data ID 为 coupon-customer-serv.yml、Group 为默认分组 DEFAULT_GROUP、配置格式为 YAML。在“配置内容”输入框中,将 spring.datasource 的配置项添加了进去。除此之外添加了一个特殊的业务属性:disableCouponRequest:true,待会儿你就会用到这个属性实现动态业务开关推送。
填好配置项的内容之后,你就可以点击“发布”按钮来创建配置文件了。redis-config.yml 和 rabbitmq-config.yml 两个配置文件将在后面的章节中用到,我们目前还不需要向这两个文件中添加配置项。
一切配置妥当之后,我们就可以去启动应用程序来验证集成效果了。为了测试应用程序能否正确读取远程配置项,你可以打开 coupon-customer-impl 模块的 application.yml 文件,将其中的 datasource 相关配置注释掉,然后尝试重新启动服务。如果项目启动正常,你将会在日志文件看到配置文件的订阅通知。
INFO c.a.n.client.config.impl.ClientWorker : [fixed-localhost_8848-dev] [subscribe] coupon-customer-serv.yml+DEFAULT_GROUP+dev
INFO c.a.nacos.client.config.impl.CacheData : [fixed-localhost_8848-dev] [add-listener] ok, tenant=dev, dataId=coupon-customer-serv.yml, group=DEFAULT_GROUP, cnt=1
// 省略其它配置文件的加载日志
接下来你可以尝试调用本地数据库的 CRUD 接口,如果业务正常运作,那么就说明你的程序可以从 Nacos Config 中获取到正确的数据库配置信息。
你可以使用同样的方法,将一些配置项信息迁移到 Nacos Config 中。当你需要更改配置项的时候,就不用每次都重新编译并发布应用了,只需要改动 Nacos Config 中的配置即可。这样一来,我们就实现了“配置管理”与“业务逻辑”的职责分离。
别忘了,前边 Nacos Config 中添加了一个 disableCouponRequest 配置项,接下来我就用它做一个动态配置推送的场景,控制用户领券功能的打开和关闭。
3.4 动态配置推送
首先,我们打开 CouponCustomerController 类,声明一个布尔值的变量 disableCoupon,并使用 @Value 注解将 Nacos 配置中心里的 disableCouponRequest 属性注入进来。
@Value("${disableCouponRequest:false}")
private Boolean disableCoupon;
在上面的代码中,我们给 disableCouponRequest 属性设置了一个默认值“false”,这样做的目的是加一层容错机制。即便 Nacos Config 连接异常无法获取配置项,应用程序也可以使用默认值完成启动加载。
然后,我们找到用户领券接口 requestCoupon,在其中添加一段业务逻辑,根据 disableCoupon 属性的值控制是否发放优惠券,如果值为“true”则暂停领券。
@PostMapping("requestCoupon")
public Coupon requestCoupon(@Valid @RequestBody RequestCoupon request) {
if (disableCoupon) {
log.info("暂停领取优惠券");
return null;
}
return customerService.requestCoupon(request);
}
最后,别忘了在 CouponCustomerController 类头上添加一个 RefreshScope 注解,有了这个注解,Nacos Config 中的属性变动就会动态同步到当前类的变量中。如果不添加 RefreshScope 注解,即便应用程序监听到了外部属性变更,那么类变量的值也不会被刷新。
@RefreshScope
public class CouponCustomerController {
}
到这里,我们就完成了所有改造工作。你可以启动应用程序,然后登录 Nacos 控制台并打开 coupon-customer-serv.yml 文件的编辑窗口,将 disableCouponRequest 的值由 true 改为 false,并调用 requestCoupon 服务查看接口逻辑的变化。
总结
本项目使用 Nacos Config 作为配置中心,实现了配置项和业务逻辑的职责分离,然后落地了一个动态属性推送的场景。
配置中心还有一个重要功能是“配置回滚”。如果你错误地修改了某些业务项,引起了系统故障,这时候你可以执行一段 rollback 操作,将配置项改动退回到之前的某一个历史版本。在 Nacos 控制台的“配置管理 -> 历史版本”菜单中,你可以查看某个配置项的历史修改记录,并指定回滚的版本。
除此之外,我们还可以在 Nacos 上查看某个文件的监听列表,了解目前有多少实例监听了指定配置文件的动态改动事件。你可以点击“配置管理 -> 监听查询”来访问这个功能。