Nacos 持久化与 Dump 机制深度解析:嵌入式/外部存储、CP 写入链路与本地服务缓存重建
【免费下载链接】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
导读:本文以 Nacos 官方《Persistence And Dump Spec》为骨架,系统拆解 Nacos 的持久化与 Dump 基础能力——包括嵌入式(Derby)与外部(Hikari + JDBC)两类存储模式的选择规则、
DataSourceService数据源契约、分布式嵌入式存储的 CP 写路径、Config 领域的 Dump 与本地服务缓存重建机制,以及维护清理与边界规则。读完本文,你将能理解 Nacos 服务端"数据持久化在哪、如何被写入、如何重建本地读缓存"的完整链路,并能对照 persistence 模块源码与 config 模块实现定位每一层职责。
1. 定位:持久化与 Dump 是基础能力,不是领域资源
在 Nacos 的架构分层中,持久化(Persistence)与 Dump 属于Foundation(基础)能力,而非 Config、Naming、AI 等领域的业务资源。它们提供:
- 持久化存储的选择与生命周期管理;
- JDBC 数据源的初始化、健康检查与事务访问;
- 持久化层的交互抽象:数据库操作、行映射、事务与 Repository 支持;
- 嵌入式存储启用时的 CP 复制;
- 面向高频读的本地 Dump 文件与 JVM 服务缓存;
- 从持久化状态重建本地服务状态的启动与修复流程。
而领域规格(Domain Spec)则负责逻辑 Schema、资源身份、校验、鉴权与用户可见行为。持久化基础能力只负责"存储管道"(storage plumbing),严禁重新定义 Config、Naming、AI 或 Auth 资源的语义。
由于持久化的数据通常是领域语义数据,因此具体的 Repository 接口与实现可能位于领域模块而非persistence模块中。最典型的例子是 Config 领域的ConfigInfoPersistService:它的嵌入式与外部实现之所以放在 config 模块,是因为这些操作理解 Config 记录、历史、灰度字段、容量元数据与 Config 可见性规则——这些语义不属于通用持久化层。
2. 存储模式(Storage Modes)
Nacos 支持两大类存储模式:
| 模式 | 当前实现 | 持久化事实来源(Source of Durable Truth) |
|---|---|---|
| 嵌入式存储 | 通过LocalDataSourceServiceImpl使用 Derby;集群模式下写入经 CP 协议排序 | 单机 Derby,或nacos_configCP 组 + 本地 Derby 状态机 |
| 外部存储 | 通过ExternalDataSourceServiceImpl使用 Hikari 托管的 JDBC 数据源 | 由数据源属性与方言规则选定的外部数据库 |
选择规则(与 DatasourceConfiguration 的实现一一对应):
- 单机模式默认使用嵌入式存储;
- 集群模式默认使用外部存储;
- 配置了非空且非
derby的数据源平台时,选择外部存储; - 集群模式可通过嵌入式存储属性开启嵌入式分布式存储;
- 一旦选定,
DynamicDataSource必须返回匹配的DataSourceService,且每个进程只初始化一次。
在源码中,DynamicDataSource.java 使用单例模式,通过DatasourceConfiguration.isEmbeddedStorage()判断后懒加载并缓存LocalDataSourceServiceImpl或ExternalDataSourceServiceImpl。而 DatasourceConfiguration 的加载逻辑可以概括为:
platform = 解析数据源方言平台(如 mysql) useExternalDb = platform 非空 且 platform != derby 若 useExternalDb:关闭嵌入式存储 否则:embeddedStorage = 单机模式标志 或 系统属性 embeddedStorage=true 若仍未开启嵌入式:自动升级为外部存储关键点:存储模式是运维部署选择。领域规格不得因为持久化存储模式变化而暴露不同的资源身份。
2.1 配置层面的落点
外部存储的典型配置位于 distribution/conf/application.properties:
# 启动时选择的数据库方言;遗留的 spring.sql.init.platform 仍被支持 #nacos.plugin.datasource-dialect.type=mysql #spring.sql.init.platform=mysql # DB 数量 # nacos.plugin.datasource.db.num=1 # DB 连接 URL # nacos.plugin.datasource.db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC # nacos.plugin.datasource.db.user=nacos # nacos.plugin.datasource.db.password=nacos # JDBC 查询超时(秒) # nacos.plugin.datasource.db.query-timeout=3注意:遗留的db.*属性与QUERYTIMEOUTJVM 属性仍作为别名受支持;当两种形式同时配置时,nacos.plugin.datasource.db.*优先。
3. 数据源契约(Datasource Contract)
DataSourceService是存储运行时契约。从 DataSourceService.java 的接口定义看,它提供:
JdbcTemplate getJdbcTemplate():SQL 执行入口;TransactionTemplate getTransactionTemplate():事务写入入口;String getDataSourceType():数据源类型与当前 JDBC URL(用于诊断);void init()/void reload():初始化与重载;boolean checkMasterWritable():外部存储的主库可写检查。
3.1 外部存储规则
ExternalDataSourceServiceImpl(见 ExternalDataSourceServiceImpl.java)的实现要点:
- 数据源属性构建一个或多个 Hikari 数据源,连接建立前会通过
ConnectionCheckUtil.checkDataSourceConnection校验连通性; - 存在多数据源时,
SelectMasterTask(每 10 秒执行)使用写探测选择主库:对每个数据源执行DELETE FROM config_info WHERE data_id='com.alibaba.nacos.testMasterDB',成功后即切换当前活跃的JdbcTemplate与事务管理器; - 每个数据源周期性健康检查(
CheckDbHealthTask,每 10 秒):执行SELECT * FROM config_info_gray WHERE id = 1探测,失败则标记isHealthList为 false 并递增DatasourceMetrics.getDbException(); - 外部存储健康状态可以是UP / WARN / DOWN,取决于主库与从库的可用性;
reload()会先构建新池并完成探测,再关闭旧池并摘除旧模板,保证重载过程安全;checkMasterWritable()通过SELECT @@read_only(1 秒超时)判断主库是否可写,防止主库不可用时登录接口长时间阻塞;- 额外提供 PostgreSQL 租户 Schema 校验:
validatePostgresqlTenantSchema检查config_info等表的tenant_id列必须为NOT NULL DEFAULT '',否则提示应用META-INF/pg-upgrade-null-tenant-id.sql迁移脚本。
3.2 嵌入式存储规则
LocalDataSourceServiceImpl(见 LocalDataSourceServiceImpl.java):
- 本地 Derby 从
META-INF/derby-schema.sql(或{nacos.home}/conf/derby-schema.sql)初始化,JDBC URL 形如jdbc:derby:{nacos.home}/data/derby-data;create=true; - 单机嵌入式存储直接对本地 Derby 执行;
- 分布式嵌入式存储启动时会清空并重新打开本地 Derby(
cleanAndReopenDerby:先jdbc:derby:;shutdown=true关闭,再删除derby-data目录重建),因为有效状态必须来自 CP 日志回放与快照恢复; - Derby 健康状态在嵌入式 CP 状态机上报不可恢复的 apply 错误时置为
DOWN(通过setHealthStatus与RaftDbErrorEvent事件联动); - 嵌入式 JdbcTemplate 默认
setMaxRows(50000)、setQueryTimeout(5000)(毫秒级配置),事务模板超时同为 5000。
3.3 数据源方言插件
数据源方言插件位于 Repository 之下、物理数据库家族之上(详见 Datasource Dialect Plugin Spec)。它们可以适配某类数据库平台的 SQL、Mapper、分页、生成主键与函数行为,但不得改变逻辑数据含义、Schema 归属、鉴权与资源身份。方言选择属性为nacos.plugin.datasource-dialect.type,遗留属性spring.sql.init.platform已标记@Deprecated并计划在 Nacos 4.0.0 移除(见 PersistenceConstant.java)。
4. Repository 契约
Repository 服务在数据源细节之上定义领域存储语义,是"领域语义"与"持久化层抽象"交汇的边界。核心规则(摘录自规格原文并对照源码):
persistence模块可提供通用原语,例如数据库操作、行映射注册(RowMapperManager)、存储常量(PersistenceConstant)与异步执行(PersistenceExecutor);- 当存储数据具有领域含义时,领域模块可定义具体 Repository 接口与实现,例如 Config 的
ConfigInfoPersistService; - Repository 接口拥有逻辑操作:新增、更新、删除、查询、历史、容量、元数据持久化;
- Repository 实现可在嵌入式与外部存储间不同,但必须保持相同的领域语义;
- Repository 实现可使用数据源方言 Mapper 构建数据库特定 SQL,但校验、资源身份、历史、Trace 与鉴权语义必须保留在领域 Repository 层;
- Controller、请求处理器与领域服务应依赖领域 Repository 接口,而非直接调用 Mapper 或方言 SPI;
- 用户可见的领域写入必须在同一领域操作中记录规格要求的历史、Trace、容量与变更元数据;
- 大范围列表与搜索操作必须分页或有界(源码中 JdbcTemplate 默认
setMaxRows(50000)即为防止内存膨胀的兜底); - Repository 代码不得直接使用来自公共 API 输入的裸 SQL 片段(必须经校验与方言受控构造);
- 方言插件与 Mapper不得成为定义领域行为或兼容性策略的替代场所;
- 仅为兼容保留的 Schema 字段不得成为新的语义契约,除非领域规格明确提升其地位。
Config 的 Repository 接口定义见 Config Persistence, Dump, And History Spec。
5. 分布式嵌入式存储:CP 写入链路
分布式嵌入式存储借助 CP 基础能力保证持久化写入排序,当前 Config 嵌入式存储组为nacos_config(常量CONFIG_MODEL_RAFT_GROUP,见 PersistenceConstant.java)。
5.1 写入模型
规格给出的完整写入链路:
Repository operation -> ModifyRequest list -> DatabaseOperate.update / blockUpdate -> CP WriteRequest(group = nacos_config) -> JRaft commit -> DistributedDatabaseOperateImpl.onApply -> local Derby transaction -> embedded apply hooks对照源码 DistributedDatabaseOperateImpl.java,该类通过@Conditional(ConditionDistributedEmbedStorage.class)启用,继承RequestProcessor4CP并实现BaseDatabaseOperate。其类注释中给出了一张完整的时序图:PersistService.publishConfig→ 保存 SQL 到SqlContextUtils上下文 →DatabaseOperate提交List<ModifyRequest>→JRaftProtocol→onApply落库 Apache Derby → 返回执行结果给JdbcTemplate。
ModifyRequest.java 是携带executeNo(执行序号)、sql、args与rollBackOnUpdateFail标志的可序列化写请求;DatabaseOperate.java 接口提供queryOne/queryMany(读)、update/blockUpdate(写)与dataImport(外部数据源导入嵌入式)三类操作,其中blockUpdate()会取出当前线程EmbeddedStorageContextHolder中的 SQL 上下文执行并自动清理。
5.2 关键规则
- 分布式嵌入式写入必须先经 CP commit,本地 Derby apply 之后才被视为持久化;
- 需要排序时,
ModifyRequest按executeNo排序后执行必须确定性; - SQL 限制器(
SqlLimiter/SqlTypeLimiter)必须在执行前拒绝不支持的查询或修改形式; - apply 钩子(
EmbeddedApplyHook)必须在已提交的 apply 之后运行,且不得用慢操作阻塞状态机; BadSqlGrammarException与DataIntegrityViolationException作为操作失败返回,而不是停止 Raft 状态机;- 严重的数据源访问失败可能表现为一致性错误,且必须更新可观测的存储健康状态。
5.3 分布式读取安全
分布式嵌入式存储激活时,读走 CP 读路径。启动 dump 可设置阻塞读上下文(常量EXTEND_NEED_READ_UNTIL_HAVE_DATA = "00--0-read-join-0--00"),让节点在存在可读数据之前等待,再重建服务缓存。
分布式嵌入式读请求在 JDBC 执行前必须校验声明的结果目标类型:
- Mapper 查询只能使用已注册到持久化行映射注册表(
RowMapperManager)的 row mapper 类名; - 标量查询只能使用显式白名单的基础结果类型。源码中
BASIC_RESULT_TYPES仅允许Integer、Long、String三类(见 DistributedDatabaseOperateImpl.java),从固定类常量解析,不采用请求驱动的类加载; - 缺失、未知或与查询类型不兼容的目标类型必须使读请求失败——这是防止嵌入式读路径被恶意或错误类型利用的关键安全设计。
快照规则由 CP Consistency Spec 定义。嵌入式存储必须提供足够的快照数据,以便重启或成员变更后重建本地 Derby 状态。
6. Dump 与本地服务缓存
Dump 是本地服务状态的重建与刷新机制,不是第二个持久化事实来源。持久化层始终是持久化事实来源。
6.1 Config Dump 模型
Durable write or cluster change notification -> ConfigDataChangeEvent or ConfigChangeClusterSyncRequest -> DumpRequest -> DumpTask keyed by groupKey or groupKey + "+gray+" + grayName -> repository reload -> ConfigDumpEvent -> ConfigCacheService -> local disk dump and JVM cache -> LocalDataChangeEvent -> listener/watch push这条链路在 config 模块中有完整实现:DumpService负责调度,DumpTask以groupKey或groupKey + "+gray+" + grayName作为任务键,ConfigCacheService负责把最新内容写入本地磁盘与 JVM 缓存,随后发布LocalDataChangeEvent触发监听器/长轮询推送。
6.2 启动 Dump 规则
- 启动时先清理旧的正式与灰度 dump 文件,再从持久化重建本地 dump 数据;
- 启动时把正式与灰度记录 dump 到本地磁盘与 JVM 缓存;
- 嵌入式集群启动必须等待 CP 组具备可读数据后,启动 dump 才算完成(对应上文阻塞读上下文);
- 外部存储启动可直接从配置的数据源 dump;
- 启动 dump 失败 = 服务启动的致命失败。
6.3 运行时 Dump 规则
- 一次成功的持久化写入后,领域必须为该资源调度 dump 刷新;
- 集群变更通知可要求对端节点从持久化重新加载本地 dump,但通知载荷不是权威内容;
- 全量 dump 任务是修复与漂移控制机制;
- 变更 dump 任务应按键组织,使同一资源的重复变更可以按 Task Execution Spec 的规则合并或替换;
- dump apply 只有在本地服务状态更新之后才能发布本地变更事件,遵循 Event Dispatch And NotifyCenter Spec。
对当前 Config 实现而言,运行时客户端读全部由本地缓存与 dump 文件服务,持久化层仍是持久化事实来源——这也是 Nacos 配置读取能做到毫秒级响应的核心设计。
7. 本地 Dump 存储
本地 dump 存储由ConfigDiskServiceFactory选择(见 ConfigDiskServiceFactory.java):
rawdisk是默认的config_disk_type(通过System.getProperty("config_disk_type", "rawdisk")读取),未知值也回退到 raw disk 行为;rocksdb可被选择为替代的本地 dump 后端(ConfigRocksDbDiskService);- 本地 dump 存储必须实现:save、remove、read、清空正式记录、清空灰度记录;
- raw disk 存储把正式与灰度内容放在 Nacos home 数据目录下,按带命名空间与无命名空间两类路径分离;
- dump 存储键必须从经过校验与编码的 Config 身份字段派生;
- 更换本地 dump 后端不得改变Config 资源身份、md5 语义、鉴权与持久化事实来源。
这里需要特别强调:本地 dump 存储属于服务缓存路径的一部分,它不是 Repository 契约,也绝不能作为独立的持久化数据库使用。
8. 缓存更新规则(单调可见性)
本地服务缓存必须对每个资源键保持单调可见性(monotonic visibility):
- 携带更旧
lastModified时间戳的 dump 必须被忽略; - 内容 md5 变化时,dump 必须同时更新本地磁盘内容与 JVM 缓存 md5 状态;
- md5 未变但时间戳更新时,dump 可只更新时间戳而不重写内容(避免无谓 IO);
- remove dump 必须删除本地磁盘数据并移除 JVM 缓存状态;
- 灰度 dump 还必须在字段变化时更新灰度规则、加密数据键与灰度排序;
- 同一资源键的缓存与 dump 变更必须由本地读写锁保护;
LocalDataChangeEvent是本地可见性事件,不得视为跨节点复制保证(参见 Event Dispatch And NotifyCenter Spec)。
安全边界:如果本地磁盘已满或无法安全存储正式 dump 内容,服务必须将之视为致命状态,因为运行时查询路径依赖本地服务状态。
9. 维护与清理
持久化与 dump 维护操作属于管理能力(Administrative Capabilities),并带有明确的归属约束:
- 历史清理只能在存储模式属主节点上执行:
- 嵌入式单机:唯一节点;
- 嵌入式集群:Config 存储组的 CP Leader;
- 外部存储:按成员顺序选出的第一个 Nacos 成员;
- 本地缓存全量 dump 是修复操作,不得替代正常的发布/删除路径;
- Derby 导入与查询操作是维护者专属操作,必须由显式运维设置与权限保护(源码中对应
DerbyImportEvent、DerbyLoadEvent及DatabaseOperate.dataImport(File)); - 大型导入与 dump 操作必须分批且有界;
- 维护 API 必须报告失败,不得把部分存储修复静默当作成功。
10. 边界规则(Boundary Rules)
规格最后用十条边界规则收束各层职责,这也是理解整个持久化体系最凝练的总结:
- 持久化是持久化存储管道;领域规格定义资源含义;
- Dump 文件与 JVM 缓存是派生的服务状态,除非领域规格另有说明;
- 嵌入式分布式存储的持久性来自CP commit 与快照恢复,而非仅本地 Derby 写入;
- 外部存储的持久性来自配置的外部数据库,而非 Nacos 集群复制;
- 数据源方言插件适配的是SQL,而非领域语义;
- 本地事件、dump 任务与缓存更新是实现内部细节,除非接口规格显式暴露;
- 不再代表领域语义的 Schema 兼容字段,必须标注为兼容或待移除。
11. 关联规格速查
- Foundation Capabilities Spec
- CP Consistency Spec
- AP Consistency Spec
- Internal RPC And Cluster Request Spec
- Task Execution Spec
- Event Dispatch And NotifyCenter Spec
- Config Persistence, Dump, And History Spec
- Config Listener And Watch Spec
- Datasource Dialect Plugin Spec
- Config Encryption Plugin Spec
12. 总结:一图读懂 Nacos 持久化分层
把整篇规格浓缩为分层视角:
领域语义层(Config/Naming/AI) -> Repository 接口与实现(如 ConfigInfoPersistService) 持久化抽象层(persistence 模块) -> DatabaseOperate / RowMapperManager / 存储常量 数据源运行时层(DataSourceService) -> LocalDataSourceServiceImpl | ExternalDataSourceServiceImpl 方言适配层(插件) -> Datasource Dialect Plugin(适配 SQL,不改语义) 物理存储层 -> 本地 Derby(嵌入式) | MySQL/PG/Oracle 等外部库 服务缓存层(dump) -> 本地磁盘 dump 文件 + JVM 缓存(仅服务读,非持久化事实)理解这一分层后,再去看 persistence 模块的datasource、repository包,以及 config 模块的service/dump、service/ConfigCacheService,你就能在源码中精确对应到"存储模式选择 → 数据源契约 → CP 写入 → 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),仅供参考