一条配置发布,服务端如何做到"改一次全网秒级生效":配置管理模块源码走读
文章目录
- 一条配置发布,服务端如何做到"改一次全网秒级生效":配置管理模块源码走读
- 你在控制台点了"发布",几百个客户端是怎么秒级收到的
- 请求入口: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 步:
第一步:参数校验
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),它既不是纯轮询,也不是纯推送,而是一种折中。
长轮询的工作原理
关键在于服务端的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——所有节点连同一个(或同一套主从)数据库。配置落库后,集群内的同步靠的是节点间的变更通知广播,让每个节点去数据库刷新自己的内存缓存:
每个节点本地维护了配置的内存缓存和磁盘快照(dump 文件),客户端拉配置时优先读内存,不用每次查库。节点收到变更通知后,从 DB 拉最新内容更新本地缓存。
⚠️ 生产提示:如果集群中某节点的内存缓存和 DB 不一致(比如通知丢失),可以调用
/nacos/v1/cs/ops/localCache相关的 dump 接口触发全量刷新,无需重启节点。执行前建议确认业务低峰期,全量 dump 会有短暂的磁盘 IO 抬升。
一张图带走:配置管理模块全链路
发布 → 落库(含历史表) → ConfigDataChangeEvent → 唤醒本节点长轮询 + 广播集群刷缓存 → 客户端通过 MD5 比对拉到新配置。整个链路的"秒级生效"靠长轮询的提前唤醒实现。
你们线上配置"改了不生效",最后定位到的原因是什么?评论区留数字:1=namespace 选错环境 2=dataId 拼写不一致 3=客户端没加监听器 4=@RefreshScope 没生效 5=集群某节点缓存没刷