改了 Nacos 配置页面,服务怎么没反应
"我把限流阈值从 500 改成 200 了,Nacos 上点了发布,等了五分钟还是没生效。"
这是组内被问过最多的一个问题,我自己也卡过。Nacos 同时做注册中心和配置中心,但这两块的机制完全不同——注册是实时的,配置热更新是有条件的。这篇把我们落地 Nacos 1.2.0 的过程和规划方式整理一下。
为什么从 Eureka 换过来
直接原因是 Eureka 2.0 从 2018 年起就停了,Netflix 全家桶那批组件(Hystrix、Zuul、Ribbon)基本都进入了维护状态。我们评估的时候列了这么几条:
| 对比项 | Eureka + Spring Cloud Config | Nacos 1.2.0 |
|---|---|---|
| 一致性模型 | AP,客户端缓存 | AP(临时实例)/ CP(持久化实例)可切 |
| 健康检测 | 客户端心跳,30 秒周期 | 客户端心跳 5 秒,15 秒标记不健康,30 秒剔除 |
| 配置管理 | Config Server 从 Git 拉,改完要刷 /actuator/bus-refresh | 自带控制台,改完推送 |
| 配置粒度 | 按文件 | namespace / group / dataId 三级 |
| 控制台 | 只有 Eureka 页面,配置没有界面 | 注册和配置都在一个页面上 |
| 部署 | 两个组件 | 一个 |
最打动我的是"改完配置不用发 bus-refresh"。以前用 Spring Cloud Config 那套,改个配置要走 Git 提交、Config Server 拉取、然后手动 POST 一次刷新,还经常漏刷。Nacos 改完点发布,两三秒就推到位。
部署
3 节点集群,MySQL 5.7 做共享存储(Nacos 1.x 的集群模式用集中式存储,不是 gossip 协议):
# conf/cluster.conf,每个节点都写全
10.0.0.21:8848
10.0.0.22:8848
10.0.0.23:8848
# conf/application.properties
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://10.0.0.5:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos
db.password=xxx
建表脚本在 conf/nacos-mysql.sql,执行完 11 张表。前面挂了一台 Nginx 做负载均衡,指向三个节点的 8848。
注意 cluster.conf 里不能写 127.0.0.1,否则节点之间互相找不到,集群起不来。这个错误在日志文件 logs/nacos.log 里的表现是反复打印 "failed to initialize member"。
服务注册:临时实例和持久化实例
这是 Nacos 特有而 Eureka 没有的概念,注册时指定:
spring:
cloud:
nacos:
discovery:
server-addr: 10.0.0.21:8848
namespace: 8f3a2b1c-xxxx-xxxx
group: TRADE_GROUP
ephemeral: true # 默认 true
ephemeral: true(默认):临时实例,走 AP 模式(Distro 协议),靠客户端心跳保活。心跳停了自动剔除。我们的业务服务全用这个。ephemeral: false:持久化实例,走 CP 模式(Raft),由服务端主动探测健康状态。实例不会因心跳停止而被删,只会标记不健康。适合数据库、Redis 这类"注册到 Nacos 上让别人发现"的基础设施。
我们给几个 MySQL 实例配了持久化实例(用 Nacos 的 OpenAPI 注册),这样应用可以通过 Nacos 拿到数据库地址,数据库主从切换时改一处就行。
健康保护阈值,这个默认值要留意
Nacos 有个 ProtectThreshold,默认 0.75。意思是:当某个服务的健康实例数低于总数的 75% 时,Nacos 会把不健康的实例也返回给消费者。
设计意图是防止雪崩——假设 10 台挂了 8 台,如果只返回 2 台健康的,这两台马上也会被压垮。不如全返回,让流量分摊。
但这个行为经常让人困惑:"明明有个实例挂了,为什么还调它?"我们的处理是把它调到 0.5,并且对核心服务单独配。另外服务端剔除实例有 30 秒延迟,加上客户端的本地缓存(默认 1 秒拉取一次),实际感知一个实例下线要 30 秒以上。这是 AP 模型的固有代价。
配置中心:dataId 的规则要背下来
Nacos 的 dataId 完整格式是:
${spring.application.name}-${spring.profile.active}.${file-extension}
例如 order-service-prod.yaml
客户端配置:
# bootstrap.yml —— 必须写在 bootstrap 里,application.yml 太晚了
spring:
application:
name: order-service
profiles:
active: prod
cloud:
nacos:
config:
server-addr: 10.0.0.21:8848
namespace: 8f3a2b1c-xxxx-xxxx
group: TRADE_GROUP
file-extension: yaml
# 公共配置,比如所有服务共用的 Redis 地址
shared-configs:
- data-id: common-redis.yaml
group: COMMON_GROUP
refresh: true
- data-id: common-datasource.yaml
group: COMMON_GROUP
refresh: true
# 业务扩展配置
extension-configs:
- data-id: sentinel-rules.yaml
group: SENTINEL_GROUP
refresh: true
这里有个大坑:Nacos config 的配置必须写在 bootstrap.yml(或 bootstrap.properties)。因为配置中心要在 Spring 容器初始化之前就拉取配置。写在 application.yml 里的话,spring.cloud.nacos.config.server-addr 根本来不及读,会去连默认的 localhost:8848。
Spring Boot 2.4 之后这套 bootstrap 机制会变,但当时我们都在 2.2 和 2.3 上,没问题。
回到开头那个问题:改了配置为什么不生效
答案是普通 Bean 不会因为配置变化而重新创建。Nacos 推送过来的配置会更新 Spring 的 Environment,但已经实例化好的 Bean 里的 @Value 字段不会重新注入。
要生效,得加 @RefreshScope:
@RefreshScope
@Component
public class RateLimitConfig {
@Value("${order.rateLimit:500}")
private int rateLimit; // 配置推送后,这个 Bean 会被销毁重建,值更新
public int getRateLimit() {
return rateLimit;
}
}
原理是 @RefreshScope 把 Bean 放到了一个特殊的作用域里,收到 RefreshEvent 时把这个作用域里的 Bean 全部销毁,下次使用再重建。
三种常见的"不生效":
| 现象 | 原因 |
|---|---|
加了 @Value 但类上没有 @RefreshScope | 最常见。Environment 变了,Bean 没重建 |
@ConfigurationProperties 的 Bean 不刷新 | 同样要加 @RefreshScope,或者用 EnvironmentChangeEvent 监听 |
值被注入到 static 字段 | @Value 不支持静态字段,一直是 null |
还有一类是配置本身没推过来。排查方式是看日志:
2020-04-10 14:22:31.417 INFO [order-service] c.a.n.client.config.impl.ClientWorker
: [fixed-10.0.0.21_8848] [polling-resp] config changed. dataId=order-service-prod.yaml, group=TRADE_GROUP
2020-04-10 14:22:31.509 INFO [order-service] c.a.nacos.client.config.impl.CacheData
: [notify-ok] dataId=order-service-prod.yaml, group=TRADE_GROUP, md5=..., listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1
2020-04-10 14:22:31.611 INFO [order-service] o.s.cloud.bootstrap.config.PropertySourceBootstrapConfiguration
: Located property source: [BootstrapPropertySource {name='bootstrapProperties-order-service-prod.yaml,TRADE_GROUP'}]
三行日志分别对应"检测到变化""通知监听器""重新加载 PropertySource"。缺哪一行就知道卡在哪一步。
另外控制台上能看到监听者列表,如果 listener 是空的,说明客户端压根没订阅这个 dataId,去检查 dataId 和 namespace 对不对。
命名空间和分组怎么设计
这是我觉得最值得说清楚的一件事。三级结构:
namespace(命名空间)
└── group(分组)
└── dataId(配置集)
我们一开始没规划,所有东西都塞在 public 命名空间和 DEFAULT_GROUP 里,两个月后就乱了。后来重定了一套:
| 层级 | 用途 | 我们的取值 |
|---|---|---|
| namespace | 环境隔离,最粗粒度 | dev / test / pre / prod,4 个 |
| group | 业务线或用途 | TRADE_GROUP、USER_GROUP、COMMON_GROUP、SENTINEL_GROUP |
| dataId | 单个配置文件 | {服务名}-{环境}.yaml |
为什么 namespace 放环境?因为 namespace 之间的配置是完全隔离的,互相看不见。测试环境的人不可能误改生产配置,这是最硬的一道保险。用 group 做环境隔离做不到这一点。
group 用来区分用途,好处是权限好控。比如 COMMON_GROUP 里放所有服务共用的 Redis、注册中心地址,只有架构组能改;SENTINEL_GROUP 放限流规则,运维能改。
命名空间的 ID 不是名字,是一串 UUID,创建的时候可以自己填一个好记的:
prod → 8f3a2b1c-prod-env-000000000001
pre → 8f3a2b1c-pre--env-000000000002
配置里写 ID 不写名字:
spring:
cloud:
nacos:
config:
namespace: 8f3a2b1c-prod-env-000000000001
discovery:
namespace: 8f3a2b1c-prod-env-000000000001
配置的优先级
这一点经常搞混,我实测的顺序(从高到低):
Nacos 中的 {服务名}-{profile}.yamlNacos 中的 {服务名}.yamlextension-configs(下标越大优先级越高)shared-configs(下标越大优先级越高)- 本地
application.yml - 本地
bootstrap.yml
注意 Nacos 上的配置默认覆盖本地配置。我们有个同事在本地 application.yml 里加了参数调试,怎么改都不生效,找了半小时才发现 Nacos 上有一份同名的。想让本地优先,可以配:
spring:
cloud:
config:
override-none: true # 本地配置不被远程覆盖
allow-override: true
override-system-properties: false
一些数字
Nacos 上了半年之后的规模:
- 3 节点集群,注册实例 127 个(含灰度实例)
- 配置集 384 个,改配置的平均生效时间 2.3 秒
- 配置变更次数:日均 25 次,其中 8 次是限流规则调整
- Nacos 单机内存占用 1.8 GB,CPU 5% 左右
出问题的时候倒是有,一次是磁盘写满导致配置写入失败,一次是有人误删了 namespace(删之前一定要确认,那个操作没有二次确认弹窗)。
留个问题
关于《Nacos 注册中心与配置中心落地实践》里这个坑,你当时是怎么处理的?欢迎在评论区聊聊你踩过的类似情况。