主题
界面
主题
《Spring Cloud 核心组件》讲了 Spring Cloud 的组件全景;本节深入其中最核心的一个:Nacos。
★ 它管两件事 ✓:
注册中心:服务在哪? (谁活着、地址是什么、怎么找到)
配置中心:配置是什么? (改了配置多久生效、怎么回滚)★ 本节的实测回答三个工程问题 ✓✓:
① 一个实例挂掉后,多久才会被"发现已挂"?
② 服务列表变化后,调用方多久能感知?
③ 配置改一行,多久能生效到所有实例?@RefreshScope 与 @Scheduled 的坑 ★★。先看一个"优雅下线"缺失导致的事故 ✗✗:
现象:每次发版,都有几十秒的请求失败率飙升
原因:进程被 kill 时**没有先注销自己** →
调用方还在往这个实例发请求(它的 IP 还在注册中心的列表里)→ 连接被拒 ✗✗★ 本节的实测会告诉你"故障被发现的延迟"是多久 ✓ —— 而这段时间里,调用方一直在往"已经死掉"的实例上发请求 ✗
本节的实测模型 ✓:
注册了 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 注销接口 → 等几秒(让调用方刷新列表)→ 再退出 ✓✓
★ 三种方式的感知延迟 ✓:
| 方式 | 感知延迟(典型) | 感知延迟(最坏) | 代表 |
|---|---|---|---|
| 推送(长连接订阅) ★★ | 约 50 ms ✓✓ | 几百 ms | Nacos 长轮询 ✓ |
| 轮询(定时拉取) ⚠️ | 约 2500 ms | 5000 ms | Eureka 客户端 |
| 客户端本地缓存 ★★★ | 0(先用旧列表) ✓ | 同 TTL | 所有实现都必备 ✓✓ |
★ 三条工程结论 ✓✓:
| # | 结论 | 说明 |
|---|---|---|
| ① | 客户端必须有本地缓存 ★★★ | 注册中心挂了也要能继续调用(否则它就是单点)✓✓ |
| ② | 推送比轮询快,但服务端要维护大量长连接 ⚠️ | Nacos 用「长轮询」折中 ✓ |
| ③ | 要接受"短暂的不一致" ★★★ | 列表变更期间少数请求会打到已下线实例 → 所以客户端重试是必配的 ✓✓ |
★ 什么是"长轮询" ✓✓:
客户端发起一个请求 → 服务端**先挂起不返回** →
有变更时立刻返回(或等 30 秒超时后返回空)→ 客户端再发起下一个★ 它的巧妙之处 ✓✓:既有推送的实时性,又不用维护真正的长连接 ✓
★ 与 《DNS、IP、端口与 ARP》 的呼应 ✓:
"客户端本地缓存 + TTL"这个模式,与 DNS 缓存是同一个思路 ✓✓
都是"用短暂的不一致,换取可用性与性能" ✓
★ 真实 Nacos 的配置模型 ✓:
| 概念 | 含义 | 例子 |
|---|---|---|
namespace ★★★ | 环境隔离(不同环境互不可见) | dev / test / prod ✓✓ |
group ★★ | 业务分组 | DEFAULT_GROUP / order-group ✓ |
dataId ★★★ | 具体配置文件 | order-service.yaml ✓ |
★ 权限按这三层授予 ✓✓ ——尤其要保证"开发环境的人看不到生产配置" ✓
★ 四种生效方式的延迟对比 ✓:
| 生效方式 | 感知延迟 | 风险 |
|---|---|---|
| 重启应用 ✗✗ | 分钟级 | 中断服务,不可接受 |
| 定时拉取(如 30s) ⚠️ | 0~30 秒 | 变更慢,且拉取有 QPS 成本 |
| 长轮询推送 ✓✓ | < 1 秒 | 服务端要维护挂起请求 |
| 配置变更 + 手动刷新端点 ✓ | 秒级 | 要人工介入(应急时很有用)✓ |
★ 四条实战规矩 ✓✓:
| # | 规矩 | 原因 |
|---|---|---|
| ① | 敏感配置(密码、密钥)用加密或专用密钥管理 ✗✗ | 不要明文放配置中心 ✓ |
| ② | 配置变更必须能回滚 ✓✓ | Nacos 有历史版本,要会用 ✓ |
| ③ | 变更要审计(谁改的、改了什么、什么时候) | 出事时第一手线索 ✓ |
| ④ | 灰度发布配置(先推 1 个实例观察)✓✓ | Nacos 支持按标签灰度 ✓ |
★★ 一个经典事故:配置中心挂了,服务能启动吗 ✓✓:
| 做法 | 后果 |
|---|---|
| 启动时必须拉配置 ✗✗ | 配置中心成为强依赖,它挂 = 全站挂 ✗✗ |
| 本地快照兜底 ✓✓ | Nacos 客户端会把配置缓存到本地文件 → 用它也能启动 ✓✓ |
★ 所以"本地快照"不是可选优化,而是可用性设计 ✓✓
★ 两者的区别 ✓✓:
| 类型 | 健康检查方式 | 挂掉后 | 适用 |
|---|---|---|---|
| 临时实例(默认) ★★★ | 客户端心跳上报 | 自动摘除 ✓✓ | 普通微服务 ✓✓ |
| 持久实例 ★★ | 服务端主动探活 | 标记不健康但不删 | 数据库 / 中间件 ✓ |
★★ 选错的后果 ✓✓:
把数据库配成临时实例 ✗ —— 它网络抖动时会被摘除,恢复后又要等心跳上报才回来 —— 期间所有访问它的服务都在报错 ✗✗
反过来,把普通微服务配成持久实例 ✗ → 它挂了但不会被摘除 → 调用方一直往死实例上打流量 ✗✗
★ 判断依据 ✓✓:
这个实例"挂了之后能不能自动恢复" ✓
能(无状态微服务)→ 临时实例 ✓
不能(数据库、有状态中间件)→ 持久实例 ✓✓
@RefreshScope 的两个坑 ★★ ★ 坑一:与 @Scheduled 一起用 ✗✗:
// ✗ 危险:配置刷新时 Bean 会被重建,导致定时任务被重复注册或丢失
@Component
@RefreshScope
public class MyScheduler {
@Scheduled(fixedRate = 1000)
public void run() { ... }
}★ 解法 ✓✓:定时任务放到单独的 Bean 里,且不要加 @RefreshScope ✓
★ 坑二:@RefreshScope 的 Bean 是"懒加载 + 每次重建" ⚠️:
它的实现是"代理 + 刷新时销毁旧实例" ✓
所以不适合有状态的 Bean(每次刷新状态就没了)✗
正确用法:只给"读配置"的 Bean 加,且它应该是无状态的 ✓✓
★ 一个更稳的替代方案 ✓✓:
用
@ConfigurationProperties+@RefreshScope—— 它能自动绑定配置到对象,且支持松散绑定与类型校验 ✓✓比在每个字段上写
@Value("${xxx}")清晰得多 ✓
@SpringBootApplication
@EnableDiscoveryClient // ★ 开启服务注册发现
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}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是否可达)与命名冲突 ✓
@PreDestroy
public void deregister() {
// 先从注册中心摘掉自己,让调用方不再往这台发新请求
nacosDiscoveryProperties.getNacosServiceManager(); // 或直接调 OpenAPI
// 再等几秒,让调用方的本地缓存刷新
Thread.sleep(5000);
}★ K8s 里对应 preStop 钩子 ✓:
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 10"] # 给注册中心与调用方留出刷新时间@Component
@ConfigurationProperties(prefix = "order")
@RefreshScope
public class OrderConfig {
private Duration timeout = Duration.ofSeconds(3); // ★ 有类型、有默认值
private boolean newCheckoutEnabled = false;
// getter / setter
}# ✗ 任何有控制台读权限的人都能看到
spring:
datasource:
password: Prod@123456★ 正确做法:用 Nacos 的加密配置、或接 KMS/Vault ✓✓
代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/RegistryConfigLesson.java
运行:
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 秒的窗口 ✗ |
| ② | 客户端本地缓存 + 重试是可用性底线 | 否则注册中心成为单点 ✗✗ |
| ③ | 优雅下线能消除发布期间的失败 | 否则每次发版都抖几十秒 ✗ |
@Scheduled 的 Bean 加 @RefreshScope:观察任务被重复注册 ✗。| # | 错误 | 说明 |
|---|---|---|
| 1 | 没有优雅下线 ✗✗ | 每次发版都抖几十秒 ✓✓ |
| 2 | 只靠心跳发现故障,不配客户端重试 ✗✗ | 3 秒窗口内的请求全失败 ✓ |
| 3 | 没有本地缓存 ✗✗ | 注册中心挂了就全站不可用 ✓ |
| 4 | namespace / group 配错 ✗✗ | 注册成功但发现不到 ✓ |
| 5 | 敏感配置明文放配置中心 ✗✗ | 谁有读权限谁就能看到 ✓ |
| 6 | 给有状态 Bean 加 @RefreshScope ✗ | 刷新时状态丢失 ✓ |
| 7 | 把数据库配成临时实例 ✗✗ | 网络抖动时被摘除 ✓✓ |
目标:把"注册、发现、配置"三板斧跑通并验证容错。
步骤:
RegistryConfigLesson,记录摘除延迟与感知延迟 ✓;验收方式:能说出"你的服务在"注册中心挂了 / 实例挂了 / 发布中"三种情况下的行为"。
扩展任务:设计一个"多环境隔离"方案:
某服务用 Nacos 做注册中心,但每次发版都有 30 秒的请求失败。最可能的原因是什么?怎么改? 5. (设计)给"配置中心"设计安全方案:要求开发能读生产配置但不能改,且不能看到密码。
preStop ③ 客户端加重试 ✓✓@RefreshScope 与 @Scheduled 的坑 ★★。《Sentinel 限流与熔断》:当依赖开始失败时,怎么不被拖死。
暂无