Skip to content

Nacos 注册与配置中心 ​

S4-021建议学高级约 60 分钟JDK 21企业开发进阶 · 微服务

1. 本节定位 ​

《Spring Cloud 核心组件》讲了 Spring Cloud 的组件全景;本节深入其中最核心的一个:Nacos。

★ 它管两件事 ✓:

text
注册中心:服务在哪?   (谁活着、地址是什么、怎么找到)
配置中心:配置是什么? (改了配置多久生效、怎么回滚)

★ 本节的实测回答三个工程问题 ✓✓:

text
① 一个实例挂掉后,多久才会被"发现已挂"?
② 服务列表变化后,调用方多久能感知?
③ 配置改一行,多久能生效到所有实例?

2. 学习目标 ​

  • 能说清注册中心的三个动作(注册、心跳、摘除)与各自的可配参数 ★★★;
  • 知道"靠心跳发现故障是有延迟的",并知道要配哪三样兜底 ★★★;
  • 能对比"推送 / 轮询 / 本地缓存"三种服务发现方式 ★★★;
  • 知道配置中心的三个维度(namespace / group / dataId) ★★★;
  • 知道临时实例与持久实例的区别与选错后果 ★★★;
  • 知道 @RefreshScope 与 @Scheduled 的坑 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个"优雅下线"缺失导致的事故 ✗✗:

text
现象:每次发版,都有几十秒的请求失败率飙升
原因:进程被 kill 时**没有先注销自己** →
      调用方还在往这个实例发请求(它的 IP 还在注册中心的列表里)→ 连接被拒 ✗✗

★ 本节的实测会告诉你"故障被发现的延迟"是多久 ✓ —— 而这段时间里,调用方一直在往"已经死掉"的实例上发请求 ✗

5. 核心概念 ​

5.1 注册中心的三个动作 ★★★ ​

本节的实测模型 ✓:

text
注册了 3 个实例(心跳间隔 1000 ms,超时 3000 ms)
实例 10.0.0.3 在第 1 秒崩溃停止心跳
**被摘除的时刻:崩溃后约 3.2 秒**(超时 3000 ms + 扫描间隔)
最终发现结果:2 个实例(已剔除故障实例)✓

★ 三个动作与参数 ✓✓:

动作对应接口参数真实 Nacos 默认值
注册 ★★POST /nacos/v1/ns/instance——
心跳 ★★★PUT /nacos/v1/ns/instance/beat心跳间隔5 秒
摘除 ★★★服务端定时扫描超时 + 扫描周期15 秒不健康,30 秒剔除

★★ 关键认识 ✓✓:

靠心跳发现故障是有延迟的 ✗

最坏延迟 = 心跳超时 + 扫描周期 ✓

这段时间里,调用方仍然会把请求发到已经挂掉的实例上 ✗✗

★ 所以生产环境必须配三样(缺一不可) ✓✓:

#措施作用
①心跳参数调优 ★★心跳越频繁发现越快,但注册中心压力越大 ⚠️
②客户端健康检查 / 重试 ★★★发现失败立刻换一个实例重试 ——这是最后一道防线 ✓✓
③优雅下线 ★★★进程关闭前先注销自己(别让调用方白等)✓✓

★★ 第三条最容易漏,且后果最直接 ✓✓:

K8s 滚动发布时,如果没有优雅下线,每次发布都会有几十秒的请求失败 ✗✗

做法:配 preStop 钩子 → 先调用 Nacos 注销接口 → 等几秒(让调用方刷新列表)→ 再退出 ✓✓

5.2 服务发现:推送 vs 轮询 vs 本地缓存 ★★★ ​

★ 三种方式的感知延迟 ✓:

方式感知延迟(典型)感知延迟(最坏)代表
推送(长连接订阅) ★★约 50 ms ✓✓几百 msNacos 长轮询 ✓
轮询(定时拉取) ⚠️约 2500 ms5000 msEureka 客户端
客户端本地缓存 ★★★0(先用旧列表) ✓同 TTL所有实现都必备 ✓✓

★ 三条工程结论 ✓✓:

#结论说明
①客户端必须有本地缓存 ★★★注册中心挂了也要能继续调用(否则它就是单点)✓✓
②推送比轮询快,但服务端要维护大量长连接 ⚠️Nacos 用「长轮询」折中 ✓
③要接受"短暂的不一致" ★★★列表变更期间少数请求会打到已下线实例 → 所以客户端重试是必配的 ✓✓

★ 什么是"长轮询" ✓✓:

text
客户端发起一个请求 → 服务端**先挂起不返回** →
有变更时立刻返回(或等 30 秒超时后返回空)→ 客户端再发起下一个

★ 它的巧妙之处 ✓✓:既有推送的实时性,又不用维护真正的长连接 ✓

★ 与 《DNS、IP、端口与 ARP》 的呼应 ✓:

"客户端本地缓存 + TTL"这个模式,与 DNS 缓存是同一个思路 ✓✓

都是"用短暂的不一致,换取可用性与性能" ✓

5.3 配置中心:三个维度与四条规矩 ★★★ ​

★ 真实 Nacos 的配置模型 ✓:

概念含义例子
namespace ★★★环境隔离(不同环境互不可见)dev / test / prod ✓✓
group ★★业务分组DEFAULT_GROUP / order-group ✓
dataId ★★★具体配置文件order-service.yaml ✓

★ 权限按这三层授予 ✓✓ ——尤其要保证"开发环境的人看不到生产配置" ✓

★ 四种生效方式的延迟对比 ✓:

生效方式感知延迟风险
重启应用 ✗✗分钟级中断服务,不可接受
定时拉取(如 30s) ⚠️0~30 秒变更慢,且拉取有 QPS 成本
长轮询推送 ✓✓< 1 秒服务端要维护挂起请求
配置变更 + 手动刷新端点 ✓秒级要人工介入(应急时很有用)✓

★ 四条实战规矩 ✓✓:

#规矩原因
①敏感配置(密码、密钥)用加密或专用密钥管理 ✗✗不要明文放配置中心 ✓
②配置变更必须能回滚 ✓✓Nacos 有历史版本,要会用 ✓
③变更要审计(谁改的、改了什么、什么时候)出事时第一手线索 ✓
④灰度发布配置(先推 1 个实例观察)✓✓Nacos 支持按标签灰度 ✓

★★ 一个经典事故:配置中心挂了,服务能启动吗 ✓✓:

做法后果
启动时必须拉配置 ✗✗配置中心成为强依赖,它挂 = 全站挂 ✗✗
本地快照兜底 ✓✓Nacos 客户端会把配置缓存到本地文件 → 用它也能启动 ✓✓

★ 所以"本地快照"不是可选优化,而是可用性设计 ✓✓

5.4 临时实例 vs 持久实例(Nacos 特有)★★★ ​

★ 两者的区别 ✓✓:

类型健康检查方式挂掉后适用
临时实例(默认) ★★★客户端心跳上报自动摘除 ✓✓普通微服务 ✓✓
持久实例 ★★服务端主动探活标记不健康但不删数据库 / 中间件 ✓

★★ 选错的后果 ✓✓:

把数据库配成临时实例 ✗ —— 它网络抖动时会被摘除,恢复后又要等心跳上报才回来 —— 期间所有访问它的服务都在报错 ✗✗

反过来,把普通微服务配成持久实例 ✗ → 它挂了但不会被摘除 → 调用方一直往死实例上打流量 ✗✗

★ 判断依据 ✓✓:

这个实例"挂了之后能不能自动恢复" ✓

能(无状态微服务)→ 临时实例 ✓

不能(数据库、有状态中间件)→ 持久实例 ✓✓

5.5 @RefreshScope 的两个坑 ★★ ​

★ 坑一:与 @Scheduled 一起用 ✗✗:

java
// ✗ 危险:配置刷新时 Bean 会被重建,导致定时任务被重复注册或丢失
@Component
@RefreshScope
public class MyScheduler {
    @Scheduled(fixedRate = 1000)
    public void run() { ... }
}

★ 解法 ✓✓:定时任务放到单独的 Bean 里,且不要加 @RefreshScope ✓

★ 坑二:@RefreshScope 的 Bean 是"懒加载 + 每次重建" ⚠️:

它的实现是"代理 + 刷新时销毁旧实例" ✓

所以不适合有状态的 Bean(每次刷新状态就没了)✗

正确用法:只给"读配置"的 Bean 加,且它应该是无状态的 ✓✓

★ 一个更稳的替代方案 ✓✓:

用 @ConfigurationProperties + @RefreshScope —— 它能自动绑定配置到对象,且支持松散绑定与类型校验 ✓✓

比在每个字段上写 @Value("${xxx}") 清晰得多 ✓

5.6 完整的一个服务接入 Nacos ★★★ ​

java
@SpringBootApplication
@EnableDiscoveryClient                      // ★ 开启服务注册发现
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
yaml
spring:
  application:
    name: order-service                    # ★ 服务名(注册与发现的 key)
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: prod
        ephemeral: true                    # ★ 临时实例(默认)
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        namespace: prod
        group: DEFAULT_GROUP

★ 三个容易配错的点 ✓✓:

#点错的后果
①spring.application.name ★★★它是服务名,改了等于换了身份(调用方找不到)✗
②namespace 要与调用方一致 ★★不一致会导致**"注册成功但发现不到"** ✗✗(最常见的坑)
③group 也要一致 ★同上 ✓

★★ 一条排障经验 ✓✓:

"服务注册不上 / 发现不到"→ 先查 namespace 与 group 是否一致 ✓

其次是网络(server-addr 是否可达)与命名冲突 ✓

6. 递进示例 ​

6.1 最小示例:优雅下线 ​

java
@PreDestroy
public void deregister() {
    // 先从注册中心摘掉自己,让调用方不再往这台发新请求
    nacosDiscoveryProperties.getNacosServiceManager();   // 或直接调 OpenAPI
    // 再等几秒,让调用方的本地缓存刷新
    Thread.sleep(5000);
}

★ K8s 里对应 preStop 钩子 ✓:

yaml
lifecycle:
  preStop:
    exec:
      command: ["sh", "-c", "sleep 10"]     # 给注册中心与调用方留出刷新时间

6.2 业务示例:读动态配置 ​

java
@Component
@ConfigurationProperties(prefix = "order")
@RefreshScope
public class OrderConfig {
    private Duration timeout = Duration.ofSeconds(3);    // ★ 有类型、有默认值
    private boolean newCheckoutEnabled = false;
    // getter / setter
}

6.3 反例:把敏感配置明文放配置中心 ​

yaml
# ✗ 任何有控制台读权限的人都能看到
spring:
  datasource:
    password: Prod@123456

★ 正确做法:用 Nacos 的加密配置、或接 KMS/Vault ✓✓

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/RegistryConfigLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.RegistryConfigLesson

实测内容与关键数据 ✓:

实验量出什么
心跳与故障剔除实例崩溃后约 3.2 秒被摘除(超时 3000 ms + 扫描间隔)✗✓
服务发现方式对比推送约 50 ms / 轮询平均 2500 ms(最坏 5000 ms)/ 本地缓存 0 ✓✓
配置生效方式对比重启分钟级 / 轮询 0~30 s / 长轮询 < 1 s ✓✓

★ 三条可落地的结论 ✓:

#结论依据
①故障发现是有延迟的,必须配客户端重试约 3.2 秒的窗口 ✗
②客户端本地缓存 + 重试是可用性底线否则注册中心成为单点 ✗✗
③优雅下线能消除发布期间的失败否则每次发版都抖几十秒 ✗

8. Debug 观察点 ​

  1. 把心跳间隔调大(如 10 秒):观察摘除延迟变长 ✓;
  2. 杀掉一个实例,立刻访问:观察调用方的失败请求(客户端重试生效则无感)✓;
  3. 把 namespace 改错:观察"注册成功但发现不到" ✗✗(最常见的坑);
  4. 把配置中心的本地快照删掉再重启:观察服务起不来(说明它依赖配置中心)✗;
  5. 给 @Scheduled 的 Bean 加 @RefreshScope:观察任务被重复注册 ✗。

9. 常见错误 ​

#错误说明
1没有优雅下线 ✗✗每次发版都抖几十秒 ✓✓
2只靠心跳发现故障,不配客户端重试 ✗✗3 秒窗口内的请求全失败 ✓
3没有本地缓存 ✗✗注册中心挂了就全站不可用 ✓
4namespace / group 配错 ✗✗注册成功但发现不到 ✓
5敏感配置明文放配置中心 ✗✗谁有读权限谁就能看到 ✓
6给有状态 Bean 加 @RefreshScope ✗刷新时状态丢失 ✓
7把数据库配成临时实例 ✗✗网络抖动时被摘除 ✓✓

10. 动手实验 ​

目标:把"注册、发现、配置"三板斧跑通并验证容错。

步骤:

  1. 跑 RegistryConfigLesson,记录摘除延迟与感知延迟 ✓;
  2. 启动两个实例,杀掉一个,观察调用方是否自动切换(客户端重试)★★;
  3. 加上优雅下线,对比"有/无"时的发布失败率 ★★★;
  4. 改一次配置,用长轮询观察生效延迟 ✓;
  5. 把注册中心停掉,验证服务仍能调用(本地缓存生效)★★★。

验收方式:能说出"你的服务在"注册中心挂了 / 实例挂了 / 发布中"三种情况下的行为"。

扩展任务:设计一个"多环境隔离"方案:

  • namespace 隔离环境(dev / test / prod)✓;
  • group 隔离业务线 ✓;
  • 权限按 namespace 授予(开发只能读生产、不能写)✓;
  • 说明:为什么"生产配置的写权限要单独管控"(提示:一次误改可能全站故障)★★

11. 练习题 ​

  1. 判断:实例挂掉后注册中心会立刻发现。(对 / 错)
  2. 填空:故障发现的最坏延迟 = ______ + ______。
  3. 简答:为什么客户端必须有"服务列表本地缓存"?
  4. (场景判断)
text
某服务用 Nacos 做注册中心,但每次发版都有 30 秒的请求失败。

最可能的原因是什么?怎么改? 5. (设计)给"配置中心"设计安全方案:要求开发能读生产配置但不能改,且不能看到密码。

参考要点(要点)
  1. 错。有延迟(超时 + 扫描周期) ✗ ——真实 Nacos 默认约 15~30 秒 ✓★
  2. 心跳超时;扫描周期 ✓
  3. 因为注册中心本身也会故障 ✗ —— 有缓存才能"注册中心挂了仍能按旧列表调用",否则注册中心成为全站单点 ✓✓
  4. 没有优雅下线 ✗✗ ——进程被 kill 时 IP 还在注册中心列表里, 调用方继续往死实例发请求 ✓ —— 改法:① 加优雅下线(先注销 + 等几秒)② 配 K8s preStop ③ 客户端加重试 ✓✓
  5. 要点:namespace 隔离 + 按角色授予读写权限 ✓; 密码用加密配置或外接 KMS/Vault(配置中心里只存密文或引用)✓✓; 开启变更审计 ✓

12. 掌握标准 ​

  • [ ] 能说出注册中心的三个动作与关键参数 ★★★;
  • [ ] 知道故障发现有延迟,且必须配客户端重试 ★★★;
  • [ ] 能对比推送 / 轮询 / 本地缓存三种发现方式 ★★★;
  • [ ] 知道配置中心的三个维度与四条规矩 ★★★;
  • [ ] 能区分临时实例与持久实例,并知道选错的后果 ★★★;
  • [ ] 知道优雅下线能消除发布抖动 ★★★;
  • [ ] 知道 @RefreshScope 与 @Scheduled 的坑 ★★。

13. 扩展资源 ​

14. 下一步学习 ​

《Sentinel 限流与熔断》:当依赖开始失败时,怎么不被拖死。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引