Nacos Config 持久化、Dump 缓存与历史记录规范深度解析
【免费下载链接】nacosan easy-to-use dynamic service discovery, configuration and service management platform for building AI cloud native applications.项目地址: https://gitcode.com/GitHub_Trending/na/nacos
本篇文章以 specs/zh-cn/config/config-persistence-history-spec.md 为骨架,结合 Nacos 当前仓库config模块源码,系统讲解 Config 领域"持久化 → 本地 Dump 缓存 → 变更同步 → 历史记录与清理"的完整链路。读完你将掌握config_info/config_info_gray/his_config_info等核心表的职责边界、正式与灰度配置的 Dump 机制、嵌入式(Derby+Raft)与外部存储(MySQL 等)两种启动模式的行为差异,以及历史保留窗口config_retention_days的配置方法,并能据此排查配置查询延迟、磁盘满故障等生产问题。
1. 总览:Config 的可靠状态从哪来
Nacos Config 的运行时正确性高度依赖"持久化层 + 本地服务状态"两层结构。规范明确指出:Config 领域通过 repository service 存储可靠状态,而每个服务端节点则维护以内部 group key 为键的本地 JVM 缓存和本地 dump 文件,运行时查询只读缓存与本地 dump 文件,不做大范围数据库查询。这意味着数据库只是"事实来源"(source of truth),而每个节点本地的缓存与文件才是提供低延迟查询的"工作副本"。
从源码结构看,这两层分别对应:
- 持久化层:
config/src/main/java/com/alibaba/nacos/config/server/service/repository/下的ConfigInfoPersistService、ConfigInfoGrayPersistService、HistoryConfigInfoPersistService等 repository 接口; - 本地状态层:
config/src/main/java/com/alibaba/nacos/config/server/service/dump/下的DumpService系列,以及ConfigCacheService维护的 JVM 缓存与磁盘文件。
2. 持久化记录族
规范将 Config 持久化记录划分为四个主要记录族,职责边界非常清晰:
| 记录族 | 目的 |
|---|---|
config_info | 正式 Config 内容、md5、类型、元数据、来源信息、namespace、group 和 encrypted data key。 |
config_info_gray | 灰度 Config 内容、md5、灰度名称、序列化灰度规则、namespace、group 和 encrypted data key。 |
his_config_info | 正式和灰度发布或删除操作的历史变更记录。 |
tenant_capacity/group_capacity | namespace、group 和 cluster 范围的容量限制和用量计数。 |
其中前三个记录族在代码层面有明确的 repository 接口一一对应:
ConfigInfoPersistService负责正式配置的增删改查(对应config_info);ConfigInfoGrayPersistService负责灰度配置(对应config_info_gray),其方法如findConfigInfo4Gray(dataId, group, tenant, grayName)同时返回灰度规则grayRule;HistoryConfigInfoPersistService负责历史记录(对应his_config_info)。
规范特别强调:具体 SQL 和数据库方言属于实现细节,由 数据源方言插件规范 覆盖。也就是说,config_info等表的具体建表语句、分页语法、时间函数都通过数据源方言插件(derby / mysql / oracle / postgresql 等)解耦,上层 repository 只依赖方言抽象接口。
3. 本地 Dump 缓存:JVM 缓存 + 磁盘文件
规范对本地服务状态的描述包含两个组成部分:
- 以内部 group key 为键的JVM 缓存,保存 md5、时间戳、encrypted data key、内容类型和灰度规则状态;
- 保存正式和灰度配置内容的本地 dump 文件。
在源码中,JVM 缓存即config/src/main/java/com/alibaba/nacos/config/server/service/ConfigCacheService.java中的静态成员:
static final ConcurrentHashMap<String, CacheItem> CACHE = new ConcurrentHashMap<>();CacheItem以 group key(由GroupKey2.getKey(dataId, group, tenant)生成)为键,保存 md5、lastModifiedTs、encryptedDataKey、type 等状态信息。而磁盘文件层由config/src/main/java/com/alibaba/nacos/config/server/service/dump/disk/ConfigDiskService.java抽象,当前仓库提供了两种实现:
ConfigRawDiskService:经典的按 dataId/group/tenant 目录结构落盘的文件系统实现;ConfigRocksDbDiskService:基于 RocksDB 的落盘实现,由ConfigDiskServiceFactory按配置选择。
3.1 启动阶段必须完成全量 dump
规范要求:启动阶段必须将持久化的正式和灰度配置 dump 到本地服务状态。对应实现为DumpService.dumpOperate()(见 DumpService.java):
dumpAllConfigInfoOnStartup(dumpAllProcessor); dumpAllGrayConfigInfoOnStartup(dumpAllGrayProcessor);启动流程会先清空本地(ConfigDiskServiceFactory.getInstance().clearAll()/clearAllGray()),再调用DumpAllProcessor与DumpAllGrayProcessor分别从config_info与config_info_gray全量重建本地缓存与文件。任何一次 dump 失败都会被记录到LogUtil.FATAL_LOG并抛出异常,直接导致节点启动失败——这正是"启动 dump 是节点可用性前提"的体现。
3.2 集群模式下的周期性全量 dump
在非单机模式(集群)下,dumpOperate()还会额外调度两个周期性任务:
DumpAllProcessorRunner与DumpAllGrayProcessorRunner:初始延迟在 10 至 6 小时(DUMP_ALL_INTERVAL_IN_MINUTE = 6 * 60)之间随机取值,之后每 6 小时执行一次全量 dump,作为对增量 dump 的兜底修正;DumpChangeConfigWorker/DumpChangeGrayConfigWorker:按PropertyUtil.getDumpChangeWorkerInterval()配置的间隔扫描数据库中变更时间之后的记录,进行增量 dump。
4. 变更 Dump 流程:事件驱动的增量同步
规范给出的变更流程为:写入成功后,Config 发布变更事件;Dump service 将事件转化为 dump 任务;任务从持久化层重新加载受影响的正式或灰度记录,并更新本地缓存和本地 dump 文件。
对照源码,这条链路非常清晰:
- 发布事件:配置写入成功后发布
ConfigDataChangeEvent; - 订阅转换:
DumpService构造时通过NotifyCenter.registerSubscriber(...)订阅ConfigDataChangeEvent,在handleConfigDataChange()中将其转换为DumpRequest(携带 dataId、group、tenant、lastModifiedTs、grayName、来源 IP),并调用dump(); - 任务入队:
dump()根据是否携带grayName分流——灰度走dumpGray(),任务 key 为groupKey + "+gray+" + grayName;正式走dumpFormal(),任务 key 为groupKey。两者都加入TaskManager dumpTaskMgr,由默认处理器DumpProcessor消费; - 重新加载并落盘:
DumpProcessor.process()(见 DumpProcessor.java)解析 groupKey 还原 dataId/group/tenant,灰度配置调用findConfigInfo4Gray(...),正式配置调用findConfigInfo(...),从持久化层重新读取最新记录并构造ConfigDumpEvent,最终交给DumpConfigHandler.configDump(); - 更新本地状态:
DumpConfigHandler(见 DumpConfigHandler.java)调用ConfigCacheService.dumpGray(...)/ConfigCacheService.dump(...)更新 JVM 缓存与本地文件;若记录已被删除则调用removeGray(...)/remove(...)。dump 成功与否会通过ConfigTraceService.logDumpEvent(...)记录 trace 日志,便于事后审计。
规范还明确了一个重要的幂等与优化规则:
- Dump 必须忽略过期时间戳,并保留更新的本地状态;
- 如果 md5 未变化但时间戳更新,只更新时间戳状态而不重写内容;
- 如果内容变化,则同时更新本地文件内容和 JVM md5 状态。
这保证了"空发布"(仅更新时间戳)不会引发磁盘写放大,而真正的内容变更则能快速传播到全部节点的本地状态。
4.1 特殊内置配置的旁路处理
DumpConfigHandler中还有一个容易被忽略的细节:当 dataId 命中内置元数据配置时,会先做旁路处理:
ClientIpWhiteList.CLIENT_IP_WHITELIST_METADATA→ 调用ClientIpWhiteList.load(content)重载 IP 白名单;SwitchService.SWITCH_META_DATA_ID→ 调用SwitchService.load(content)重载开关配置。
即开关与白名单这类控制面配置同样走持久化 + dump 链路下发到各节点。
本地事件行为由 事件分发与 NotifyCenter 规范 定义,dump task 行为由 任务执行规范 定义。
5. 嵌入式与外部存储两种启动模式
Config 支持嵌入式和外部存储两种模式,两者的启动 dump 时机与管理操作执行节点策略截然不同:
| 模式 | 启动 dump 时机 | 管理/清理操作执行节点 |
|---|---|---|
| 嵌入式存储(Derby + Raft) | 必须等到 CP 协议已有可读数据后,才完成启动 dump | 仅 Raft group leader |
| 外部存储(MySQL 等) | 直接从配置的数据源初始化 dump | 集群首节点(first IP) |
5.1 EmbeddedDumpService:等待 Raft Leader 就绪
config/src/main/java/com/alibaba/nacos/config/server/service/dump/EmbeddedDumpService.java通过@Conditional(ConditionOnEmbeddedStorage.class)在嵌入式存储模式下生效:
- 单机模式:直接执行
dumpOperate(),无等待; - 集群模式:向 CP 协议注册一个 observer,订阅
CONFIG_MODEL_RAFT_GROUP的LEADER_META_DATA元数据。只有 leader 元数据有值(即 Raft 协议已选出 leader、具备可读数据)后才执行dumpOperate(),并且通过CountDownLatch阻塞节点初始化直至 dump 完成;dump 失败时抛异常使节点启动失败; - 错误分类重试:
shouldRetry()对读取失败消息做了两类区分——"The conformance protocol is temporarily unavailable for reading" 这类临时不可读错误会每 500ms 重试;而FSMCaller is overload.、STATE_ERROR这类 Raft 状态机内部错误则视为不可恢复,直接终止。
这解释了规范中"嵌入式存储必须等到 CP 协议已有可读数据后,才完成启动 dump"的原因:Derby 数据由 Raft 状态机写入,节点启动时本地 Derby 可能还没有可读的已提交数据,必须等 leader 就绪后才能安全地全量 dump。
5.2 ExternalDumpService:直接初始化
config/src/main/java/com/alibaba/nacos/config/server/service/dump/ExternalDumpService.java通过@Conditional(ConditionOnExternalStorage.class)生效,并@DependsOn("rpcConfigChangeNotifier")保证变更通知组件先就绪:
- 启动时直接调用
dumpOperate(),从外部数据源全量 dump,无需等待 CP 协议; canExecute()返回memberManager.isFirstIp(),即只有集群中 IP 最小的首节点才执行历史清理等管理类存储操作,避免多节点并发清理。
规范明确要求:历史清理和管理类存储操作必须只在存储模式策略选中的节点上执行——嵌入式模式对应 leader,外部存储模式对应首节点,这正是canExecute()抽象方法的两种实现。
嵌入式存储的 CP 基础见 CP 一致性规范,Config Notify 传播见 AP 一致性规范。持久化、dump、任务和事件边界由 持久化与 Dump 规范、任务执行规范 和 事件分发与 NotifyCenter 规范 定义。
6. 历史记录:审计与回滚的基础
6.1 历史记录必须保留的信息
规范要求his_config_info至少保留以下信息,以便检查和恢复配置变化:
dataId、groupName和namespaceId;- 内容和 md5;
- 来源用户和来源 IP;
- 操作类型,例如发布或删除;
- 发布类型,例如正式或灰度;
- 适用时的
grayName和扩展信息; - 创建和修改时间。
注意"发布类型(正式/灰度)"与"grayName"这两个字段是 Nacos 灰度发布能力的直接体现——灰度配置的每次发布/删除同样会沉淀为一条历史记录,保证灰度变更也可审计、可回滚。
6.2 历史 API 与分页边界
规范强调:历史列表和详情 API 属于管理 API,分页大小必须有边界。这类接口位于 Config 管理面,调用时务必显式传入受限的 pageNo/pageSize,避免一次拉取全量历史拖垮服务端。
6.3 历史清理:保留窗口默认 30 天
规范给出默认保留窗口为30 天。对应实现有两处关键源码:
config/src/main/java/com/alibaba/nacos/config/server/constant/Constants.java定义配置键:CONFIG_RENTENTION_DAYS_PROPERTY_STATE = "config_retention_days";config/src/main/java/com/alibaba/nacos/config/server/service/dump/DefaultHistoryConfigCleaner.java的cleanHistoryConfig():
Timestamp startTime = getBeforeStamp(TimeUtils.getCurrentTime(), 24 * getRetentionDays()); int pageSize = 1000; getHistoryConfigInfoPersistService().removeConfigHistory(startTime, pageSize);清理逻辑要点:
- 以当前时间减去
24 * retentionDays小时为界,批量删除更早的历史记录; - 每批
pageSize = 1000条,分批执行避免长事务; getRetentionDays()读取PropertyUtil.getConfigRententionDays(),即由config_retention_days属性控制,默认 30 天;- 清理任务在
DumpService.dumpOperate()中通过ConfigExecutor.scheduleConfigTask(new ConfigHistoryClear(cleaner), 10, 10, TimeUnit.MINUTES)注册,每 10 分钟调度一次,且执行前会先调用canExecute()确认本节点是选中的管理节点(leader 或首节点)。
若需调整保留窗口,可通过config_retention_days配置项覆盖默认值,例如设置为 7 天:config_retention_days=7。
7. 恢复与运维:Admin 全量 dump 与磁盘致命条件
7.1 Admin 本地缓存操作
规范指出:Admin 本地缓存操作可以触发从持久化层到本地缓存的全量 dump,该操作是管理修复机制,不应作为正常发布链路使用。
对应源码为DumpService.dumpAll():
public void dumpAll() { dumpAllTaskMgr.addTask(DumpAllTask.TASK_ID, new DumpAllTask()); }它会向dumpAllTaskMgr投递全量 dump 任务,由DumpAllProcessor(正式)与DumpAllGrayProcessor(灰度)消费。适用场景是:节点本地缓存与数据库出现不一致(如手工改库、历史遗留脏数据、误删 dump 文件)时,作为修复手段强制重建本地状态;日常发布链路应始终走第 4 节的增量 dump 流程,否则会对数据库和磁盘造成不必要的全量压力。
7.2 磁盘满 = 致命条件
规范给出了一个非常硬性的结论:当本地磁盘已满或无法安全保存 dump 内容时,服务端必须将其视为致命条件,因为运行时查询正确性依赖本地服务状态。
ConfigCacheService中对磁盘写入失败的识别印证了这一点(见 ConfigCacheService.java):
private static final String NO_SPACE_CN = "设备上没有空间"; private static final String NO_SPACE_EN = "No space left on device"; private static final String DISK_QUOTA_CN = "超出磁盘限额"; private static final String DISK_QUOTA_EN = "Disk quota exceeded";这些字符串用于识别写盘失败是否由磁盘空间/配额引起。因为一旦本地 dump 文件无法写入,节点后续无法为客户端提供正确的配置内容,继续运行只会返回过期或缺失数据,因此必须作为致命条件处理。生产实践中应为 dump 目录(与数据目录)配置独立的磁盘监控和告警,将磁盘使用率阈值调低,提前扩容或清理。
8. 与相关规范的衔接
Config 持久化与 Dump 机制并非孤立设计,它依赖并协同以下规范,阅读时建议按需深入:
- Config 一致性、Dump 与可见性规范:Config 一致性契约的完整定义;
- 持久化与 Dump 规范:持久化与 dump 的整体边界;
- CP 一致性规范:嵌入式存储的 Raft 基础;
- AP 一致性规范:Config Notify 的传播语义;
- 内部 RPC 与集群请求规范:集群节点间的请求与同步;
- 任务执行规范:dump task 的执行模型;
- 事件分发与 NotifyCenter 规范:
ConfigDataChangeEvent等本地事件的发布订阅机制; - 数据源方言插件规范:
config_info等表 SQL 与方言实现。
总结
本文围绕 config-persistence-history-spec.md 的七个章节,结合config模块源码还原了 Nacos Config 的数据链路全貌:config_info/config_info_gray/his_config_info构成持久化事实来源;每个节点通过启动全量 dump + 事件驱动增量 dump 维护本地 JVM 缓存与磁盘文件;嵌入式模式等待 Raft leader 就绪、外部存储模式直接初始化;历史记录由每 10 分钟一次的清理任务按config_retention_days(默认 30 天)保留窗口分批删除;Admin 全量 dump 作为修复手段存在,而磁盘写满则必须视为致命条件。理解这条链路,是排查配置查询异常、节点启动卡顿、历史数据膨胀等问题的第一把钥匙。
【免费下载链接】nacosan easy-to-use dynamic service discovery, configuration and service management platform for building AI cloud native applications.项目地址: https://gitcode.com/GitHub_Trending/na/nacos
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考