Chapter 03

配置中心原理:近实时下发

上一章是服务列表怎么传播(Distro + 推送)。配置内容的传播是另一套机制—— 它用"长轮询"在"实时"和"省资源"之间取巧。这一章拆开配置从发布到生效的全链路,并解释最常见的"改了不生效"。

本章你将建立的 schema

  • 长轮询:客户端发一个 30s 超时请求,服务端 hold 29.5s,变更即时返回——以及那 0.5s 的用意。
  • 变更检测靠 MD5 比对,不靠"定时全量拉"。
  • 一条配置从发布到客户端回调的完整链路。
  • @RefreshScope 的刷新边界——"改了配置不生效"的头号原因。
  • 灰度发布、历史回滚,以及配置在单机 / 集群下分别存哪。

3.1长轮询:在"推"和"拉"之间取巧

客户端发一个超时 30s 的请求,服务端 hold 住 29.5s 不立刻回;期间配置变了就立即返回,没变到点回"无变化"。

为什么不用最朴素的两种做法

客户端要"配置一变就知道"。最朴素是短轮询(每隔几秒拉一次)——实时性取决于间隔,且大量请求多数是"没变",纯浪费。另一极是服务端推(维护长连接主动推)——实时但要维护海量连接、且要求客户端可被连达。长轮询取两者之间:一次请求覆盖近 30s,期间一有变更立即返回,既近实时又不空转。

Client Server ① 长轮询请求(MD5, timeout=30s) 配置发布 → ②a 命中变更 → 立即返回 dataId ②b 无变更 → 29.5s 后返回 304 ③ 客户端立即重新发起
图 3.1服务端 hold 期间有两个出口:命中变更(②a,红色,立即)或到点无变更(②b,返回 304)。 注意:hold 29.5s 而非 30s——留 0.5s 给网络往返,避免客户端先于服务端超时、把正常的"无变化"误判成请求超时。
比文档深一层

服务端 hold 住请求不占线程:用 Servlet 3.0 的 AsyncContext 把请求挂起,线程立刻释放去处理别的请求,到点或有变更时再唤醒响应。这就是单机能扛大量长轮询连接的原因。而"配置变没变"靠MD5 比对:客户端把自己持有内容的 MD5 一起发来,服务端拿当前内容的 MD5 一比,不一致才返回——不必传整段内容来判断。

表 3.1 · 三种"感知变更"方案
方案实时性服务端开销取舍
短轮询(定时拉)差(取决于间隔)高:大量空轮询实时与开销两头不讨好
服务端推(长连接)好维护海量连接1.x 时代连接成本高(2.x 才转向它)
长轮询接近实时低:hold 不占线程选中:一次请求覆盖 29.5s,变更即时
现状 · 截至 2026-06

长轮询是 1.x 的经典机制,也是面试最常考的模型,理解它是理解"配置近实时"的基础。2.x 起,配置变更通知也并入了 gRPC 长连接(与服务发现同一条连接),服务端可直接推 ConfigChangeNotify,比 HTTP 长轮询更直接;但"MD5 比对判断是否真的变了"这一思想仍然保留。下文链路对两者都成立。

3.2配置变更的全链路

发布 → 服务端落库并标记变更 → 唤醒挂起的长轮询 / 推送通知 → 客户端拉取最新 → 触发监听回调。

把一次"在控制台改了一个值"拆成五步,每一步都能对应到一个排障点:

① 发布配置 控制台 / API ② 落库 标记变更 ③ 唤醒/推送 挂起的请求 ④ 客户端拉取 最新内容 ⑤ 回调 刷新 Bean
图 3.2配置变更的五步链路。 注意:③ 是关键一跳——服务端找到所有挂起的相关长轮询请求(或 gRPC 订阅者)立即响应,这一步把"近实时"变成现实;⑤ 的回调是否生效,取决于下一节的刷新边界。

这条链路里,前四步是 Nacos 负责的,第 ⑤ 步回调发生在你的应用进程内——也正是这一步最容易"看起来没生效"。

3.3刷新边界:为什么"改了不生效"

配置推到客户端后,只有标了 @RefreshScope 的 Bean 会被重建以读到新值;普通 @Value 字段不会。

这是 Nacos 配置最高频的"灵异事件":控制台明明改了、日志也显示收到变更,但接口返回的还是旧值。根因不在 Nacos,在 Spring 的刷新边界:

配置变更 新值 v2 已到达 @RefreshScope + @Value Bean 重建 → 读到 v2 ✓ 仅 @Value(无 RefreshScope) 不重建 → 仍是旧值 v1 ✗ 变更事件 变更事件
图 3.3同一个变更事件,两个 Bean 命运不同。 注意:刷新边界由 @RefreshScope 划定——它告诉 Spring"这个 Bean 在配置变更时要丢弃重建"。没标它的 Bean 持有的还是初始化时注入的旧值。
表 3.2 · 三种注入写法的刷新行为
写法配置变更后是否自动读到新值
@Value(无 @RefreshScope)否——仍是旧值
@Value + @RefreshScope(同一 Bean)是——Bean 重建后读到新值
@ConfigurationProperties是——默认支持刷新,无需 @RefreshScope
想一想

你用 @Value("${feature.rate}") 注入了一个字段,在 Nacos 改了 feature.rate,控制台显示已更新、客户端日志也打印"received config change",但接口行为没变。最可能缺了什么?

展开答案(先停 10 秒)

最可能缺了 @RefreshScope。变更确实推到了客户端(日志为证),但承载 @Value 的 Bean 没被标记为可刷新,于是不会重建,字段停留在启动时注入的旧值。给该 Bean 加 @RefreshScope,或改用 @ConfigurationProperties(默认可刷新)。

隔离语义复用:namespace / group

呼应 §1.5:配置的 namespace / group 和服务发现是同一套。实战里,namespace 拿来隔离 dev / prod 的配置(同一个 dataId 在两个环境互不影响),group 拿来把"同一类配置"跨应用归组。配错 namespace(填了名字而非 UUID)就读不到配置——和发现不到服务,是同一个根因的两种症状。

3.4灰度、回滚与持久化

灰度(beta)发布按指定 IP 先推;配置存历史版本可一键回滚;单机用内嵌 Derby,集群用外置 MySQL。

配置改错的代价比代码 bug 更直接——它瞬间生效到所有实例。Nacos 用三个能力降低这个风险:

  • 灰度 / beta 发布:发布时指定一批 IP,变更只推给这些 IP,验证无误再全量。它和普通发布的区别就在推送对象:普通发布推给全部订阅者,灰度只推给指定 IP 列表。
  • 历史版本与回滚:每次发布留存历史,出问题可一键回滚到上一版本。
  • 持久化:见下表——这也决定了你的集群要不要外挂数据库。
表 3.3 · 配置的持久化
部署形态配置存哪含义
单机 standalone内嵌 Derby开箱即用,仅适合本地 / 测试
集群 cluster外置 MySQL(多个节点共享)生产标配;DB 要做高可用,否则成单点
洞察 · 回到那条脊柱

注意配置数据落在强一致这一侧:它持久化、要求各节点看到的内容一致——正是起点页脊柱图里和持久实例同侧的 CP。这也解释了为什么配置中心不像临时实例那样"分区时各写各的":一份算错钱的配置在两个节点上不一致,后果比一个实例短暂消失严重得多。下一章把这条脊柱完整收口。

§本章 self-check

先合上教程作答,再展开对照。

  1. 长轮询里服务端为什么 hold 29.5s 而不是 30s?这 0.5s 防的是什么?
  2. 服务端 hold 住请求期间为什么不占线程?变更检测靠什么判断"内容真的变了"?
  3. "改了配置不生效",从 @Value / @RefreshScope / @ConfigurationProperties 角度,最常见的原因是什么?
  4. 灰度发布和普通发布,在"推送对象"上的区别是什么?
  5. (综合 01)namespace 配错在配置侧表现为"读不到配置";同样的 namespace 配错,在服务发现侧会表现成什么症状?为什么是同一个根因?
答案(先做完再展开)
  1. 留 0.5s 给网络往返余量。若服务端也 hold 满 30s,客户端可能先到 30s 超时,把本该是"无变化"的正常响应误判成请求超时。
  2. 用 Servlet 3.0 AsyncContext 挂起请求、释放线程,到点或有变更再唤醒——所以不占线程。变更检测靠 MD5 比对:客户端发来持有内容的 MD5,服务端比对当前 MD5,不一致才返回。
  3. 承载 @Value 的 Bean 没加 @RefreshScope,变更虽推到但 Bean 不重建,字段停在旧值。修复:加 @RefreshScope 或用 @ConfigurationProperties。
  4. 普通发布推给全部订阅者;灰度只推给发布时指定的 IP 列表,验证后再全量。
  5. 服务发现侧表现为"发现不到服务 / no available server"。根因相同:namespace 是服务发现和配置中心共用的隔离边界,配错(如填名字落到 public)会同时让两边都"找不到"。
进阶挑战 · 刚好够不着

一个开关,要先在 2 台机器上验证

你要上线一个高风险开关 risk.switch=on,希望先在 2 台金丝雀机器验证 10 分钟,再推全量。用本章的能力设计这个流程,并说明:灰度期间,其余实例读到的是 on 还是 off?灰度配置和正式配置在 Nacos 里是什么关系?

提示(卡住再展开)

用灰度 / beta 发布,发布时填那 2 台金丝雀的 IP。灰度期间:只有这 2 台收到 on,其余实例仍读正式版本的 off——因为变更只推给了指定 IP。验证无误后"停止 beta / 全量发布",把灰度内容转正,推给全部。关键认知:灰度本质是"按推送对象切分",不是改了一个不同的 dataId。