一条配置发布,服务端如何做到“改一次全网秒级生效“:配置管理模块源码走读

📅 2026/7/10 9:31:15 👁️ 阅读次数 📝 编程学习
一条配置发布,服务端如何做到“改一次全网秒级生效“:配置管理模块源码走读

一条配置发布,服务端如何做到"改一次全网秒级生效":配置管理模块源码走读


文章目录

  • 一条配置发布,服务端如何做到"改一次全网秒级生效":配置管理模块源码走读
    • 你在控制台点了"发布",几百个客户端是怎么秒级收到的
    • 请求入口:ConfigController 的发布方法
    • 发布链路:从请求到落库再到通知
      • 第一步:参数校验
      • 第二步:落库
      • 第三步:MD5 计算
      • 第四步:发布变更事件
    • 变更通知:长轮询是怎么"憋住"请求的
      • 长轮询的工作原理
      • 挂起期间配置变了怎么办
      • 为什么是 29.5 秒
    • 集群内的配置同步:不是靠 Raft
    • 一张图带走:配置管理模块全链路

你在控制台点了"发布",几百个客户端是怎么秒级收到的

场景很常见:你在 Nacos 控制台改了一个数据库连接池大小,点"发布"。几秒钟后,几百个订阅了这个配置的服务实例全都拿到了新值,不用重启。

问题来了——这几百个客户端,服务端是挨个推送的吗?配置改动先落库还是先通知?如果推送的一瞬间某个客户端刚好断线,它还能拿到新配置吗?

这篇文章从ConfigController的发布入口开始,拆开配置管理模块在服务端的完整链路:发布落库 → MD5 计算 → 变更通知 → 长轮询响应 → 客户端拉取。每一步给出类名和方法,你可以对着 nacos-config 模块的源码跟。

⚠️ 本文基于 Nacos 2.3.0 源码。配置的存储链路 1.x 和 2.x 差异不大(都走 MySQL),但变更通知机制 2.x 增加了 gRPC 推送通道,与 1.x 纯 HTTP 长轮询有区别,文中会标注。


请求入口:ConfigController 的发布方法

配置发布的入口是ConfigController.publishConfig

@RestController@RequestMapping(Constants.CONFIG_CONTROLLER_PATH)publicclassConfigController{@PostMappingpublicBooleanpublishConfig(HttpServletRequestrequest,@RequestParam("dataId")StringdataId,@RequestParam("group")Stringgroup,@RequestParam("content")Stringcontent,...){// 1. 参数校验(dataId/group 不能为空)ParamUtils.checkParam(dataId,group,...);// 2. 计算内容 MD5// 3. 落库 + 发布变更事件configOperationService.publishConfig(...);}}

配置的三维坐标dataId + group + namespace唯一定位一条配置。发布时,这三个值决定了要覆盖数据库里的哪一行。


发布链路:从请求到落库再到通知

一次配置发布,服务端走了 4 步:

事件中心持久化服务ConfigOperationServiceConfigController控制台/API事件中心持久化服务ConfigOperationServiceConfigController控制台/API异步:通知本节点长轮询 + 广播其他节点publishConfig(dataId, group, content)publishConfig(form, request)insertOrUpdate(config_info 表)计算并存储 MD5发布 ConfigDataChangeEvent200 OK(发布成功)

第一步:参数校验

ParamUtils.checkParam校验 dataId、group 的合法性,content 不能超过默认 100KB(可配置)。

第二步:落库

ConfigOperationService调用持久化服务,把配置写入config_info表。这里是insert or update——同一个 dataId+group+namespace 存在就更新,不存在就插入:

// 核心持久化逻辑publicvoidinsertOrUpdateConfig(ConfigInfoconfigInfo,...){try{addConfigInfo(configInfo);// 尝试插入}catch(DuplicateKeyExceptione){updateConfigInfo(configInfo);// 主键冲突则更新}// 同时写入 his_config_info 历史表,支持回滚}

注意:每次发布都会往his_config_info历史表写一条记录,这是配置回滚功能的数据基础。

第三步:MD5 计算

配置内容落库时,服务端会计算内容的 MD5 值并一起存储。这个 MD5 是整个"变更检测"机制的核心——客户端和服务端都靠比对 MD5 来判断配置是否变化,而不是每次都传输完整内容。

第四步:发布变更事件

落库成功后,发布ConfigDataChangeEvent。这个事件触发两件事:通知本节点上正在长轮询的客户端,以及广播给集群其他节点。


变更通知:长轮询是怎么"憋住"请求的

配置中心的精髓在于变更推送。Nacos 用的是"长轮询"(Long Polling),它既不是纯轮询,也不是纯推送,而是一种折中。

长轮询的工作原理

有变化

无变化

客户端发起长轮询请求
携带各配置的 MD5

服务端 LongPollingService

MD5 有变化?

立即返回变更的 dataId

挂起请求
不立即响应,最长 29.5 秒

29.5s 内有变更?

提前唤醒,返回变更

超时返回空
客户端立即发起下一轮

关键在于服务端的LongPollingService:客户端请求进来后,如果配置没变化,服务端不立即返回,而是把这个请求"挂起"(利用 Servlet 3.0 的AsyncContext),最长挂 29.5 秒。

// LongPollingService:挂起客户端请求publicvoidaddLongPollingClient(HttpServletRequestreq,HttpServletResponsersp,Map<String,String>clientMd5Map,intprobeRequestSize){// timeout 默认 30s,减去 500ms 给客户端留余量longtimeout=...-500L;AsyncContextasyncContext=req.startAsync();// 把请求包装成 ClientLongPolling 任务,放进调度器scheduler.execute(newClientLongPolling(asyncContext,clientMd5Map,...));}

挂起期间配置变了怎么办

这就是ConfigDataChangeEvent的作用。事件触发后,LongPollingService会遍历所有挂起的长轮询请求,找到订阅了这个 dataId 的客户端,提前唤醒它们,立即返回变更通知:

// 监听配置变更事件,唤醒相关的长轮询classDataChangeTaskimplementsRunnable{publicvoidrun(){for(ClientLongPollingclientPolling:allSubClients){if(clientPolling.clientMd5Map.containsKey(changedDataId)){// 提前响应,通知客户端这个配置变了clientPolling.sendResponse(Arrays.asList(changedDataId));}}}}

为什么是 29.5 秒

时间点谁的动作原因
30s客户端超时时间客户端设定的请求超时
29.5s服务端主动返回提前 500ms 返回,避免客户端先超时导致请求浪费
< 29.5s配置变更提前唤醒有变更时立即响应,实现"准实时"

长轮询本质是用"可控的延迟"换"低频的连接"。相比每秒轮询,它大幅减少了无效请求;相比服务端主动推送,它不需要维护每个客户端的连接状态(HTTP 场景下)。


集群内的配置同步:不是靠 Raft

一个常见误解:Nacos 配置在集群内是靠 Raft 协议同步的。在 2.x 的默认实现里,这不完全对。

Nacos 配置的持久化底座是MySQL——所有节点连同一个(或同一套主从)数据库。配置落库后,集群内的同步靠的是节点间的变更通知广播,让每个节点去数据库刷新自己的内存缓存:

节点A 收到发布请求

写入共享 MySQL

向集群所有节点
发 ConfigDataChangeEvent

节点B 从 DB 重新加载该配置

节点C 从 DB 重新加载该配置

更新本地内存缓存 + 磁盘快照

每个节点本地维护了配置的内存缓存磁盘快照(dump 文件),客户端拉配置时优先读内存,不用每次查库。节点收到变更通知后,从 DB 拉最新内容更新本地缓存。

⚠️ 生产提示:如果集群中某节点的内存缓存和 DB 不一致(比如通知丢失),可以调用/nacos/v1/cs/ops/localCache相关的 dump 接口触发全量刷新,无需重启节点。执行前建议确认业务低峰期,全量 dump 会有短暂的磁盘 IO 抬升。


一张图带走:配置管理模块全链路

客户端获取

通知

发布

唤醒

ConfigController.publishConfig

ConfigOperationService

config_info 表 + his_config_info 历史表

ConfigDataChangeEvent

本节点 LongPollingService
唤醒挂起的客户端

广播其他节点
刷新内存缓存

客户端长轮询
携带 MD5

服务端比对 MD5

有变化立即返回
无变化挂起 29.5s

发布 → 落库(含历史表) → ConfigDataChangeEvent → 唤醒本节点长轮询 + 广播集群刷缓存 → 客户端通过 MD5 比对拉到新配置。整个链路的"秒级生效"靠长轮询的提前唤醒实现。


你们线上配置"改了不生效",最后定位到的原因是什么?评论区留数字:1=namespace 选错环境 2=dataId 拼写不一致 3=客户端没加监听器 4=@RefreshScope 没生效 5=集群某节点缓存没刷