跳到正文

配置中心 ​

一句话:配置中心把配置从「打包进镜像的静态文件」变成「运行时可读写的远程数据」,换来不发版改配置;代价是多了一条远程依赖、一套生效时序,以及「改了但没刷新」这类最难排查的问题。

适用版本:Nacos 3.2.4;2.5.4 的差异单独标注。Spring 示例为 Spring Cloud Alibaba 2025.0.0.0(Boot 3.5),该版本已移除 bootstrap.yml 支持,统一用 spring.config.import。

为什么需要 ​

没有配置中心时,改一个开关、一个阈值、一个灰度比例,都要走「改代码 → 打包 → 发布 → 重启」。慢只是表层:100 个服务有 100 份重复配置,生产密码散落在各仓库,回滚要重新发版。配置中心的价值是集中管理、动态生效、变更留痕,「不重启」只是最容易被感知的那一项。

核心机制 ​

配置模型:三元组定位一份配置 ​

维度作用默认值易错点
namespace环境/租户隔离3.x 为 public,2.x 为空字符串填名称不填 ID,表现是「读到空配置但不报错」
group业务分组DEFAULT_GROUP与注册服务的 Group 是两套
dataId配置文件名无SCA 默认用 spring.application.name + 扩展名拼

Spring Cloud Alibaba 的 spring.config.import 必须显式写出 dataId 和 group;少写一个,启动时不会去 Nacos 拉配置,而不是「拉了但没生效」。

变更怎么到达客户端 ​

版本通知方式客户端动作延迟量级
1.xHTTP 长轮询,服务端挂起约 30s返回后按 dataId 拉取内容、比对 MD5秒级到 30s
2.xgRPC 长连接推送收到通知后拉取内容,另有定时比对兜底毫秒到秒级
3.x同 2.x同 2.x;HTTP OpenAPI 不再提供长轮询监听毫秒到秒级

图里最关键的是「通知只带身份,内容由客户端回拉」:服务端不必为每个订阅者复制全量配置,只告诉你哪份变了;客户端再比对 MD5,决定是否刷新。这也解释了为什么配置很大时推送不会把服务端带宽打满,但会让客户端多一次读请求。

1.x 的长轮询不是「服务端每秒推」:客户端发起请求,服务端没有变更就挂住,30s 后返回空,客户端立刻再发下一次。它的问题是每个客户端占一个挂起请求,实例数一多,连接和线程都吃紧。2.x 换成 gRPC 后,通知走同一条长连接,客户端再按需拉内容,既减少了空轮询,也不再为每个订阅占一个挂起请求。2.x/3.x 仍有「定时比对」,但它是兜底,不是主路径;看到延迟十几秒,先查推送连接,不要直接归因于「长轮询就是慢」。

客户端三级缓存 ​

三级缓存从内到外是应用内存(Environment / @ConfigurationProperties)、本地快照(~/nacos/config/,磁盘)、Nacos Server(MySQL/Derby)。Server 不可用时,客户端先用本地快照启动;运行中继续用内存里的当前值。所以「配置中心挂了,应用立刻挂」通常不成立;真正的风险是「新实例启动时既连不上 Server,又没有本地快照」。

刷新边界:哪些会变,哪些不会 ​

配置方式是否自动刷新原因
@ConfigurationProperties是Environment 变更后重新绑定属性对象
@Value + @RefreshScope是销毁代理 Bean,下次访问重建并重新注入
@Value 无 @RefreshScope否注入时已固定
@NacosValue(autoRefreshed=true)是Nacos 自己的注解处理
ConfigService.addListener手动回调只给内容,解析和应用要自己写
static / final 字段否不在 Spring 的刷新链路里
DataSource、连接池、线程池通常否连接已建立;重建 Bean 会短暂中断

结论:不要把「连接池大小、数据库 URL、线程池核心数」当作热更新项;这类变更走「改配置 → 滚动重启」。需要动态调整的开关、阈值、灰度比例,用 @ConfigurationProperties 承接。

灰度、历史与回滚 ​

Nacos 的灰度不是复制一份配置,而是同一 namespace + group + dataId 下挂多个灰度版本:

  • Beta:按客户端 IP 列表匹配,优先级最高;控制台勾选 Beta 发布并填 IP。
  • Tag:按 tag_{tag} 匹配,优先级次之;适合按机房/版本打标。
  • 默认每个配置最多 10 个灰度版本(nacos.config.gray.version.max.count,以官方文档为准);灰度发布与删除都进历史,2.5.0 起 Beta 历史也会保留。

回滚的正确做法是「查历史 → 以历史内容重新发布为正式配置」,不要直接改本地 dump 文件。历史保留天数由数据库/配置决定,常见托管服务口径是 30 天,自建环境以实际配置为准。

加密与审计 ​

Nacos 用 dataId 前缀决定是否加密:cipher-{algorithm}-,例如 cipher-aes-order-service.yaml。发布时插件生成数据密钥,密文和 encrypted_data_key 一起入库;读取时用同一插件解密。注意:

  • 服务端不内置生产可用的加密算法,官方示例是 AES 插件;SM4 需要自己实现 EncryptionPluginService。
  • 使用加密插件时,config_info、config_info_gray、his_config_info 需要 encrypted_data_key 列;主密钥接 KMS/HSM,不要硬编码。

审计方面,Nacos 自带配置历史与灰度历史,能回答「谁在什么时候改了哪份配置」;更完整的登录、权限、删除审计要接外部日志系统,不要假设控制台自带全套合规审计。

写入冲突:CAS 与 LWW 是两条路径 ​

带 cas(内容 MD5)的发布先比对 MD5,不一致返回冲突,这是乐观锁;OpenAPI 不带 cas 则直接覆盖,后写胜出(Last-Write-Wins)。要防覆盖就在发布链路里带 cas,不要把两条路径混成一句「Nacos 是 LWW」。

实战场景 ​

一、Beta 发布忘关,一台机器长期跑特殊配置 ​

现象:为验证一个新阈值,用 Beta 方式按客户端 IP 发布到 1 台实例。验证完成后直接在控制台改正式配置发布,没有清理 Beta。两周后同一服务出现“偶发的限流不准”,只有某一个 Pod 异常;重新发布、重启都无效,因为 Beta 仍然生效。

排查:Beta 的优先级高于 Tag 与正式配置,且不会因为正式配置变化而失效。

定位层取证据判定
是否存在灰度版本控制台该 dataId 的灰度列表(Beta IP、Tag)有残留条目即命中
该实例拿到的是哪份实例日志里配置拉取的 MD5/版本,与正式版本比对不一致 → 命中灰度
匹配规则Beta 按客户端 IP 列表匹配,优先级最高;Tag 按 tag_{tag} 次之IP 漂移后可能意外命中或失效
其他环境是否也有按 dataId 全量扫灰度记录灰度残留通常是批量现象
客户端缓存本地快照时间与内容快照能证明“当时确实拿到过这份”

对症做法:

text
1. 清理:删除该 dataId 的 Beta/Tag 灰度条目,观察实例是否回到正式版本;
   必要时让实例重启一次以强制重新拉取。
2. 灰度必须带回收:创建时登记“到期时间 + 负责人”,到期自动删除灰度条目。
3. 用 Tag 而不是写死 IP 做机房级灰度,避免 Pod 重排后 IP 漂移导致命中不可预期。
4. 发布平台把“存在灰度版本”作为该 dataId 的显著状态提示,不允许静默共存。
bash
# 对账脚本:逐实例确认生效版本一致(示例口径)
for ip in $(kubectl get pods -l app=order-service -o jsonpath='{..ip}'); do
  curl -s "http://$ip:9090/actuator/env" | jq -r '.activeProfiles, .propertySources[].name' | sort -u
done
# 输出里出现非 nacos 来源的同名 key,说明被环境变量或本地配置覆盖

复盘要点:判据是“灰度条目存续时间”:任何灰度版本超过约定时长(例如 24 小时)未清理就告警。灰度是配置中心里最容易留下长期副作用的功能,因为它不改变正式配置,只改变某些实例看到的值——这类差异在配置列表里几乎不可见,只能靠对账发现。

二、密钥进配置中心之后:密文当明文用 ​

现象:安全审计要求数据库口令加密存放。平台给配置中心接了加密插件,只覆盖了 8 个服务;某服务把已加密的 spring.datasource.password 发布后,该服务的配置里所有加密项原样下发,应用启动报认证失败。另一处问题是密钥解密后的值被 toString 打进了启动日志。

排查:加密是客户端与服务端都要认可的链路,不是控制台上的一个勾选框。

定位层取证据判定
加密项范围该 dataId 里哪些 key 被加密;插件的匹配规则只按 key 名前缀匹配时,改名即漏加密
客户端是否具备解密能力依赖里是否有对应 SDK/插件;启动日志是否有解密失败告警未接插件的服务拿到密文直接使用
密钥来源数据密钥来自 KMS/Vault 还是配置中心自身密文与主密钥同库存放等于没加密
是否进了日志搜索日志里的明文口令、Token、密钥片段启动日志、异常栈、/actuator/env 是三个出口
轮换路径新旧密钥并存窗口、灰度顺序无并存窗口会让在途请求批量失败

对症做法:

text
1. 分层存放:配置中心只存“非密钥的配置”和密文,主密钥放 KMS/Vault;
   需要轮换、硬件隔离、严格审计的凭证不进配置中心。
2. 加密改造按“先兼容代码,再改数据”的顺序:
   先全量发布具备解密能力的客户端并验证,再把配置项改成密文发布。
3. 日志出口收紧:口令字段禁用 toString 输出,异常信息只给请求 ID;
   管理端点 env/configprops 默认关闭,见 [企业级可观测性](../10_SpringCloud/13-企业级可观测性与故障定位.md)。
4. 敏感配置的变更纳入审计:谁改的、改成什么(密文指纹)、何时生效。
java
// 客户端兼容写法:能识别密文格式,也能处理明文,避免灰度期两种值混存
public String resolve(String raw) {
    return raw.startsWith("ENC:") ? kms.decrypt(raw.substring(4)) : raw;   // 迁移期两种形态并存
}

复盘要点:加密改造失败的方式通常是“一半服务能解、一半不能”,所以判据是覆盖率:具备解密能力的实例比例达到 100% 之后才允许切换密文。密钥治理与认证链路一起设计,见 认证、加密与端点安全。

三、回滚了配置,但生效状态没收敛 ​

现象:一次误操作把线程池核心数从 32 改成 2 并全量发布,10 分钟内接口 P99 从 90ms 涨到 1.4s。操作者用控制台“历史版本回滚”回到上一版,但 15 分钟后仍有约 1/3 实例处于错误配置;其中若干实例是回滚后新扩容出来的,另有一些是 @Value 注入的旧值一直没变。

排查:回滚写回了服务端,不等于所有客户端都拿到了新值。

定位层取证据判定
服务端当前值直接读 OpenAPI/控制台的 dataId 内容与版本号版本已回滚 → 问题在客户端侧
各实例实际值逐实例查 /actuator/env 或 /actuator/configprops值不一致 → 生效未收敛
绑定方式读取处是 @ConfigurationProperties 还是 @Value@Value 未配 @RefreshScope 时永不更新
优先级覆盖同名 key 的最终来源(命令行 > 环境变量 > 本地 yaml > 配置中心)被镜像内配置压住时,回滚配置中心无效
新实例起点扩容实例启动时拉取的版本拉到错误版本会长期存在,直到再次发布
组件是否可热更变更项属于可热更、需重建 Bean 还是不可热更线程池/连接池属于需重建或必须重启

对症做法:

java
// 1. 线程池这类“能刷但不该刷”的项,明确按滚动重启处理,而不是指望推送
//    刷新会让 Bean 重建并造成瞬时中断;生产按批次滚动更安全
text
2. 回滚后必须做客户端侧收敛验证:逐实例比对生效值与目标值,差异实例重启或再次发布。
3. 关键项要“先灰度到少量实例、观察、再全量”,回滚同理:先回一台验证生效路径可用。
4. 客户端收到变更后加随机延迟(毫秒到几秒,经验值)再刷新,
   避免几百个实例同时重建 Bean 造成瞬时抖动。
5. 配置项分类维护:可热更 / 需重建 / 不可热更 三张清单,
   写进配置模板,改到“不可热更”项时流程强制走发布而不是推送。
properties
# 能刷新的写法统一成 ConfigurationProperties,避免 @Value 散落在业务类里
feature.rate-limit.enabled=true
feature.rate-limit.qps=800

复盘要点:真正的验收是“全部实例生效值等于目标值”,而不是“服务端已回滚”。这个对账可以自动化:发布平台在每次变更(含回滚)后逐实例读 configprops 做 diff,非零差异即阻塞后续批次。连接池大小、数据源 URL、监听端口、JVM 参数属于不可热更项,任何“改配置就生效”的假设都要先验证绑定方式,见 框架定制与配置治理。

最小上手 ​

Spring Boot 3.5 + SCA 2025.0.0.0:

xml
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
yaml
spring:
  application:
    name: order-service
  config:
    import: nacos:order-service.yaml?group=DEFAULT_GROUP&refreshEnabled=true
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: public

发布并读回(3.x v3 API;写用 Admin,读可用 Client):

bash
curl -s -X POST "http://127.0.0.1:8848/nacos/v3/admin/cs/config" \
  -H "accessToken: $ACCESS_TOKEN" \
  --data-urlencode "dataId=order-service.yaml" \
  --data-urlencode "groupName=DEFAULT_GROUP" \
  --data-urlencode "namespaceId=public" \
  --data-urlencode "content=feature:
  newCheckout: false"
curl -s "http://127.0.0.1:8848/nacos/v3/client/cs/config?dataId=order-service.yaml&groupName=DEFAULT_GROUP&namespaceId=public" \
  -H "accessToken: $ACCESS_TOKEN"

应用侧用 @ConfigurationProperties 绑定 feature.newCheckout,改值后看 /actuator/env 或业务日志;@Value 则必须加 @RefreshScope。optional:nacos: 表示拉不到也启动,生产是否加要按「配置缺失能否接受」决定。

生产动作 ​

  • 先确认坐标:dataId、group、namespace 三者任一不对,表现都是「读不到配置」,不是「配置没生效」。
  • 确认导入方式:SCA 2020+ 默认不加载 bootstrap;没有 spring.config.import 就不会去 Nacos 拉配置。
  • 确认刷新方式:@Value 配 @RefreshScope;@ConfigurationProperties 不需要;改完看日志里的 Refresh keys changed。同名 key 会被命令行、环境变量、本地 application.yaml 覆盖,用 /actuator/env 看最终值。
  • 本地快照:怀疑读到旧值时,先看 ~/nacos/config/ 下快照的时间和内容。
  • 变更治理:生产配置走审批/发布记录;敏感项用加密插件 + KMS;不要明文进配置中心。灰度不要依赖秒级一致,需要所有实例同一时刻切换的场景,配置中心做不到。

排障路径:改了不生效 → 查 dataId/group/namespace → 查 spring.config.import → 查是否 @RefreshScope → 查优先级;启动报 Could not resolve placeholder → 配置没拉到或 key 拼错;多实例生效不一致 → 看推送连接与网络;配置被覆盖 → 查发布是否带 cas。

边界 ​

  • 不是注册中心,也不是密钥管理系统:配置中心不维护实例列表;需要轮换、硬件隔离、严格审计的密钥,用 Vault/KMS。
  • 不适合高频写入:配置变更应是低频人工/发布动作;每秒改配置会把通知和数据库打成热点。
  • 不能保证强一致生效:多实例、多级缓存决定了「最终生效」;要求同一毫秒切换的场景做不到。
  • 不是所有 Bean 都能热更新:连接池、线程池、DataSource 这类已建立资源的组件,老老实实滚动重启。

讲清楚 ​

配置中心解决「改配置不发版」:配置按 namespace/group/dataId 存到服务端,客户端启动时拉取并落本地快照,运行中通过 gRPC 长连接感知变更,再更新内存里的 Environment。1.x 用 HTTP 长轮询,2.x/3.x 换成 gRPC 推送,客户端仍保留定时比对兜底。它能热更新 @ConfigurationProperties 和带 @RefreshScope 的 Bean,但连接池、线程池、DataSource 这类已经建立连接的组件通常要重启;@Value 不加 @RefreshScope 也不会变。灰度按 Beta IP 或 Tag 匹配,回滚要用历史内容重新发布,不要改本地快照。它的边界是最终生效而不是强一致:多实例、多级缓存、通知链路决定了生效有先后,要求所有实例同一时刻切换的场景,配置中心做不到。

把一门技术讲透到既懂也会用