
最近把公司一套Nacos集群从MySQL迁移到国产关系型数据库上踩了不少坑。我用的版本是nacos3.1.0本以为只是换一个JDBC驱动的事结果发现Nacos 3.x把数据源适配层整个重构了一遍玩法和2.x完全不同。这篇文章就把我摸出来的路子整理一下包括存储层架构变化、不同国产数据库的适配路线、手写数据源插件的完整过程以及一堆只有实际翻车才能总结出来的兼容性坑给正准备做国产数据库适配的同学一个参考。先说结论Nacos 3.1.0适配国产数据库这件事本质上不是“换个驱动”而是“让Nacos的数据源插件机制和SQL模板体系适配目标数据库的方言”。搞懂这个前提后面所有工作都有章可循。1. Nacos 3.1.0的存储层到底改了什么1.1 注册中心对数据库的依赖比很多人想得重很多人有个错觉觉得Nacos这种注册中心/配置中心组件跟数据库关系不大顶多存个配置而已。真去深入看代码才发现Nacos对数据库的依赖相当重。配置管理这块就不用说了配置内容、配置历史、灰度配置、监听器记录全在表里。服务发现这块服务实例的临时/持久数据、服务元数据、健康检查的计数、服务间的映射关系也都要落库。Nacos单机模式默认用内嵌Derby确实可以“无脑启动”但一旦上集群Derby就废了必须切换到外部数据库。因为Nacos集群节点之间需要共享数据Derby是每个节点本地一份数据根本没法统一。所以生产环境下数据库就是Nacos的“状态存储底座”数据库的稳定性、性能、方言兼容性直接决定了整个注册发现链路和配置推送链路的可用性。这也是为什么国产化改造的时候Nacos这种基础组件往往是最后动刀、但一旦动刀就很疼的部分。它不像业务应用那样换个驱动改个连接串就能跑它的SQL语句、分页逻辑、事务行为都是围绕着某一类数据库的习惯写的。1.2 3.x数据源插件机制从SQL硬编码到SPI可扩展Nacos 2.x早期版本对数据库的支持是比较“硬”的默认MySQL单机用DerbySQL语句跟具体数据库方言耦合在一起。到2.2.0版本官方意识到这个问题开始引入DataSourcePlugin的SPI机制允许外部扩展数据库类型。到了3.x这个机制被彻底模块化数据源相关代码从核心工程里剥离出来形成了独立的插件体系。这套机制的核心抽象有几个DataSourcePlugin负责创建和返回目标数据库的数据源DataSource并声明当前插件支持的数据库类型DbType。Nacos拿到这个插件后就知道该用哪个数据源去连库。Mapper与SQL模板Nacos内部把每个表的操作封装成Mapper接口例如ConfigInfoMapper、ServiceMapper等。Mapper的每个方法对应一条SQL模板不同数据库方言的差异通过实现不同的Mapper来覆盖。插件只需要针对目标数据库重写那些方言差异比较大的SQL模板比如分页语句、主键生成语句。DbType标识Nacos通过DbType来路由到底加载哪个插件的数据源和SQL模板。默认支持MySQL和Derby外部扩展则返回自定义的DbType字符串。所以Nacos 3.1.0适配国产数据库核心工作可以拆成两件事第一让Nacos能用目标数据库的驱动连上库第二让Nacos在执行SQL的时候用的是目标数据库能听懂的话。明白了这个根本逻辑后面所有操作就都是围绕这两件事展开的。2. 国产数据库适配路线对比没有万能方案只有最合适的协议2.1 OceanBaseMySQL协议兼容适配成本最低OceanBase在适配这件事上绝对是“天胡开局”。因为OceanBase除了有自己的原生模式还提供了高度兼容MySQL协议和语法的租户模式。也就是说如果OceanBase这边开的是一个MySQL兼容租户Nacos完全可以把它当成一个“特殊的MySQL”来用。实际操作中我们连OceanBase的MySQL租户时可以直接用MySQL官方驱动也可以使用OceanBase官方提供的驱动。连接串的写法跟MySQL非常像只是端口和租户信息不太一样。更关键的是Nacos默认的MySQL SQL模板在OB的MySQL租户上基本都能执行LIMIT分页、自增主键、事务行为都兼容得比较好。我的建议是如果目标数据库是OceanBase优先确认租户模式是MySQL兼容模式。如果是直接复用Nacos的MySQL数据源插件再把JDBC驱动替换成OB驱动适配工作量最小。唯一要注意的是驱动版本和OB服务端版本的匹配度建议以官方文档为准避免驱动过老导致连接参数不识别。2.2 openGaussPostgreSQL生态社区插件做跳板openGauss是华为开源的数据库内核基于PostgreSQL发展而来所以它对外提供的协议、语法都跟PostgreSQL高度亲和。Nacos社区其实有一个PostgreSQL的数据源插件postgresql-datasource-plugin-ext这套思路完全可以借用到openGauss上。我们当时的做法是先引入社区已经验证过的PostgreSQL插件然后把底层的JDBC驱动替换成openGauss提供的基于PG协议的驱动org.opengauss.Driver。因为openGauss兼容PG的协议大部分PostgreSQL方言的SQL都能执行再配合PostgreSQL插件里已经写好的分页、主键逻辑整个Nacos就可以跑起来了。不过要注意openGauss毕竟不是100%的PostgreSQL某些系统函数、数据类型、锁行为、事务隔离级别实现会有些差异。比如一些PG内置视图或类型转换函数在openGauss里名字可能不同。这就要求在验证阶段多跑几轮全链路测试不能想当然觉得“PG能跑openGauss就一定能跑”。2.3 达梦/金仓Oracle兼容模式的另类路径达梦DM8和人大金仓KingbaseES这两家在政企、金融的国产化项目里出现频率极高。它们的共同点是都主打Oracle兼容模式同时也提供MySQL兼容模式的开关。这里就有两条路可以选一条路是“踩MySQL兼容模式”。达梦DM8和人大金仓的部分版本支持MySQL兼容语法开启后LIMIT分页、自增主键这些写法可以勉强用。但实测下来兼容度不是100%遇到复杂的JOIN、子查询、类型转换还是可能报语法错误。这条路适合时间紧、来不及定制插件的项目但要做好排查方言差异的长期准备。另一条路是“正经写一个定制插件”。既然目标数据库的方言跟MySQL差异大那就老老实实针对达梦或者金仓实现一套独立的SQL模板。这也正是我在第3章要展开讲的方式。不要觉得写插件很重其实Nacos留好的扩展点定制一个数据源插件的工程量是可控的而且一劳永逸。2.4 各路线横向对比对比项OceanBaseMySQL租户openGauss达梦DM8 / 金仓协议兼容基础高度兼容MySQL兼容PostgreSQL兼容Oracle/MySQL视模式可复用插件Nacos默认MySQL插件社区PostgreSQL插件基本需要自定义JDBC驱动MySQL驱动或OB驱动gaussdb JDBCPG协议dm.jdbc.driver.DmDriver等SQL模板改动量很小较小较大主要风险点驱动版本匹配系统函数差异方言兼容度、锁行为、序列建议场景优先选这条路PG底子好的团队政企项目被迫接入时3. 实操从零实现一个Nacos 3.1.0数据源插件这一章我以达梦DM8为例完整走一遍数据源插件的实现过程。选达梦是因为它的方言差异更大、坑更多把这个啃下来其他数据库就是照猫画虎的事。3.1 工程结构与依赖准备插件的本质是一个独立的JAR包通过JDK的SPI机制被Nacos识别并加载。工程结构上最少需要一个实现类和一个SPI配置文件。Maven依赖方面核心要依赖Nacos的datasource-plugin模块和Mapper模块这两个提供了DataSourcePlugin接口和Mapper基类。另外需要引入目标数据库的JDBC驱动以及连接池依赖。连接池我建议直接用HikariCPNacos本身也是用HikariCP作为默认连接池这样行为最一致。构建配置里有个容易忽略的点打包的时候要把JDBC驱动一起打进来或者放到Nacos的插件目录里。如果只打了自定义插件的代码运行时还是会报驱动类找不到。3.2 数据源提供者实现首先实现DataSourcePlugin接口。核心逻辑就是构造一个连接达梦数据库的HikariDataSource并声明DbType为dameng。package com.example.nacos.datasource.dm; import com.alibaba.nacos.plugin.datasource.spi.DataSourcePlugin; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; public class DamengDataSourcePlugin implements DataSourcePlugin { private HikariDataSource dataSource; Override public void init(...) { HikariConfig config new HikariConfig(); config.setDriverClassName(dm.jdbc.driver.DmDriver); config.setJdbcUrl(jdbc:dm://127.0.0.1:5236?schemaNACOScompatibleModemysql); config.setUsername(SYSDBA); config.setPassword(your_password); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } Override public DataSource getDataSource() { return dataSource; } Override public String getDbType() { return dameng; } Override public void destroy() throws Exception { if (dataSource ! null) { dataSource.close(); } } }这里有几个点要说一下。一个是JDBC URL里我加了compatibleModemysql参数但这个是可选的取决于达梦实例是否开启了MySQL兼容语法。如果你决定走完全自定SQL模板的路线这个参数可以不写把SQL改造成达梦原生方言即可。我个人的经验是尽量开启兼容模式减少接口层 SQL 排查量但核心的底层表结构和初始化脚本仍要按达梦的习惯手工调一遍。另一个是connectionInitSql如果目标库做了特殊字符集或事务设置可以在这里补初始化SQL。比如设置会话字符集、事务隔离级别等。这个属于隐藏经验一般文档不会提但遇到乱码和锁超时时往往就是这里的问题。3.3 自定义SQL模板与Mapper数据源提供者只解决了“连得上”的问题更关键的是“读得懂”。Nacos内部有几张核心表每个表都有对应的Mapper接口默认实现是根据MySQL方言写的。要适配达梦就得继承默认Mapper覆盖那些方言差异大的方法。最常见的就是分页语句。比如配置列表查询MySQL写的是LIMIT ? OFFSET ?达梦虽然兼容Oracle的ROWNUM同时部分版本也支持LIMIT语法。我在适配时选择覆盖分页方法显式返回达梦能接受的分页写法package com.example.nacos.datasource.dm; import com.alibaba.nacos.plugin.datasource.mapper.ConfigInfoMapper; import com.alibaba.nacos.plugin.datasource.mapper.DefaultConfigInfoMapper; public class DamengConfigInfoMapper extends DefaultConfigInfoMapper { Override public String getPageLimitSql() { return LIMIT ? OFFSET ?; } }实际适配过程中不仅仅分页。还有几类SQL需要重点关注获取自增主键的语句。MySQL是SELECT LAST_INSERT_ID()达梦则可能要用SELECT IDENTITY或者序列相关语法。布尔值处理。MySQL里是0/1达梦里可能涉及BIT、TINYINT或者BOOLEAN的映射差异。字符串拼接与函数。比如GROUP_CONCAT在达梦里可能是LISTAGG日期格式化函数也可能不同。批量插入的语法达梦可能不认MySQL的INSERT INTO ... VALUES (), () ...这种多值写法虽然DM8较新版本对很多语法做了兼容但老实审查一遍最稳。这些SQL模板需要在实现时逐一对照Nacos源码里默认Mapper的每个方法然后结合目标数据库语法进行改写。这个阶段最花时间也最能体现适配工作的价值。3.4 配置文件与SPI注册实现代码写完之后需要在resources目录下建SPI配置文件路径是META-INF/services/com.alibaba.nacos.plugin.datasource.spi.DataSourcePlugin文件内容就一行com.example.nacos.datasource.dm.DamengDataSourcePlugin然后把整个模块打成JAR包连同达梦的JDBC驱动一起放到Nacos安装目录下的plugins/datasource目录。Nacos启动时会通过SPI扫描机制加载插件扫描到插件后会以getDbType()的返回值作为数据库标识。接下来修改Nacos的配置文件application.propertiesspring.datasource.platformdameng nacos.datasource.driver-class-namedm.jdbc.driver.DmDriver nacos.datasource.urljdbc:dm://127.0.0.1:5236?schemaNACOS nacos.datasource.usernameSYSDBA nacos.datasource.passwordyour_password注意spring.datasource.platform这个值要跟插件返回的DbType保持一致Nacos启动时才会把数据源路由到你的插件上。这个配置项不匹配的话插件就算放在目录里也不会被启用属于很隐蔽的配置问题。3.5 部署验证与异常观察插件部署完之后不要急着开始业务验证先看启动日志。Nacos启动时如果成功加载插件日志里会打印数据源初始化的信息。如果SQL模板有问题通常启动阶段建表或者第一次访问配置列表接口时就会爆出SQLSyntaxErrorException。我建议的验证顺序是先确认Nacos控制台能正常登录因为登录涉及用户表查询。再创建命名空间、发布一条配置触发配置表的插入和查询。注册一个临时服务实例观察服务注册表、心跳记录的写入。做一次配置变更观察配置历史表和监听器表是否正常写入。最后把Nacos集群所有节点切到同一套达梦库重启全部节点做集群级验证。4. 适配过程中躲不开的兼容性坑4.1 分页SQL的LIMIT/OFFSET差异分页是适配中最容易翻车的地方。MySQL的分页写法是LIMIT offset, size或者LIMIT size OFFSET offset但达梦、金仓对LIMIT的语法支持并不完全一致openGauss虽然兼容PG风格但参数绑定方式也可能有坑。我们当时遇到过的问题是普通查询没问题一旦配置列表数据量超过一定阈值控制台页面就会报错或者数据错乱。后来排查发现是某些Mapper里的分页SQL模板没有被自定义实现覆盖还在走MySQL那份。所以我的建议是适配完成后全局搜索一下SQL模板里所有的LIMIT关键字逐条确认到底走的是哪个Mapper。4.2 主键生成策略与序列自增Nacos很多表的主键是BIGINT自增MySQL下依赖AUTO_INCREMENT。切到达梦后如果表的定义还是AUTO_INCREMENT可能会出现两种情况建表语句直接不支持或者插入数据时主键不自动生成。解决办法通常是把自增列改成序列触发器的方式或者在插入SQL里显式指定主键值。这里面有一个经验如果选择在插入SQL里显式生成主键就要保证全局唯一不能多个Nacos节点同时插入时产生冲突。Nacos集群多节点并发写库时这个坑很容易爆出来表现为偶发的主键冲突异常。4.3 字段类型映射、字符集与时区国产数据库对数据类型的命名和映射跟MySQL有差异。比如MySQL的datetime、timestamp、tinyint、varchar在达梦里可能表现为DATE、TIMESTAMP、SMALLINT、VARCHAR2语义不同。如果初始化脚本是从MySQL版本直接执行类型不匹配的问题会一个接一个冒出来。字符集方面MySQL默认utf8mb4但某些国产数据库默认字符集可能是GBK或UTF-8排序规则也可能不一样。我们当时出现过配置内容包含emoji表情时写入失败的问题后来把达梦库表和连接会话的字符集统一改成UTF-8问题才消失。时区问题也是一样MySQL连接串里常用serverTimezone国产数据库里未必认这个参数不设置的话时间字段偏差8小时的情况很常见。4.4 事务、锁与隔离级别Nacos的核心操作里配置发布、实例注册都涉及事务。国产数据库的锁行为和默认隔离级别不一定跟MySQL一致。比如达梦默认事务隔离级别是读已提交但某些并发场景下的锁等待超时时间更短导致Nacos在高并发下出现配置发布超时。这里分享一个排查思路遇到偶发的锁等待超时不要急着去改Nacos代码先看数据库端的锁等待参数把这个参数调大并确认是否存在慢SQL长时间占用事务。很多时候国产数据库的默认参数偏保守调整后问题就消失了。4.5 常见报错排查速查表报错/现象常见原因处理方式Driver class not foundJDBC驱动JAR没放进插件目录或依赖缺失把驱动JAR复制到plugins/datasource并重启Unknown database / schema不存在URL里的schema名称与库名不一致提前建好对应schema并检查URL参数SQLSyntaxErrorExceptionSQL模板还是MySQL方言检查对应Mapper是否被自定义实现覆盖Lock wait timeout exceeded锁等待超时时间太短或事务冲突调整数据库锁等待参数排查慢SQL中文/emoji乱码库表字符集与会话字符集不一致统一库表、连接URL的字符集为UTF-8时间偏差8小时连接串时区参数不生效在连接串中指定数据库支持时区写法或改JVM默认时区分页数据重复/缺失分页SQL模板未生效全局搜索LIMIT关键字逐条确认Mapper归属自增主键冲突自增机制未正确适配改用序列触发器或显式生成唯一主键4.6 改名和保留字的坑这个值得单独拿出来说。国产数据库对保留字的处理跟MySQL差别不小。Nacos表字段里有一些名字在达梦或金仓里可能是保留字比如USER、SYSTEM、ORDER、GROUP这些。如果初始化脚本里没有加引号建表时就会报错或者查询时报语法错误。我们当时碰到的最诡异的一个问题是某张表的某个字段名在达梦里是保留字但建表时没报错查询时也没报错偏偏在某个特定操作时报语法错误排查了很久才发现是保留字冲突。所以初始化脚本里所有字段名和建议加上双引号或者反引号的地方都要提前处理不要偷懒。5. 数据迁移与上线前检查清单5.1 存量数据迁移的三个步骤适配完成后还有一块硬骨头是把MySQL里的存量数据搬到国产数据库。Nacos的数据量一般不大几万条配置、几十万条实例记录已经算很多了所以迁移手段可以很朴素。第一步初始化目标库的表结构。先基于Nacos提供的MySQL Schema脚本按照目标数据库的语法手工调整类型、自增、字符集等然后执行建表。第二步写数据导出导入脚本。从MySQL导出INSERT语句检查特殊字符和编码再在目标库执行。如果数据量大用ETL工具更稳但Nacos这个量级用脚本就够。第三步一定要做数据校验。对比源库和目标库的表记录数抽查关键配置内容的MD5是否一致。这一步不能省否则上线后可能发现历史配置丢了或者内容变了影响面非常大。5.2 集群联调与主备切换演练数据迁移完成不代表能直接上线。Nacos是多节点集群所有节点必须指向同一套国产数据库。我第一次切换时就犯过一个错只改了一个节点的数据源配置其他节点还在连旧库结果集群里出现配置不同步的情况。正确做法是先把所有Nacos节点配置全部切换然后逐台重启。重启顺序建议从非主节点开始最后处理当前主节点这样可以减少对客户端的影响。重启完成后观察集群节点状态确保所有节点都注册成功再进行一次配置发布和实例注册的小规模验证。有条件的话可以做一次主备切换演练。国产数据库的主备机制跟MySQL的复制不一定一模一样切换后Nacos连接是否会自动恢复这个必须提前验证。我们当时演练时发现主库切换后连接池里的旧连接不会自动重建导致Nacos持续报连接异常最后是通过配置HikariCP的连接检测参数解决的。5.3 上线前的性能验证指标适配工作完成之后不要直接宣布“搞定”性能验证才是检验适配质量的试金石。我建议至少关注以下指标配置发布QPS与时延连续发布几百条配置观察控制台响应和客户端感知延迟。服务注册与心跳处理能力模拟大量客户端注册观察数据库写入是否存在锁等待。慢SQL数量国产数据库的慢查询日志一定要打开上线前清掉所有慢SQL再放量。连接池占用观察Nacos各节点的数据源连接数确认没有连接泄漏。长稳运行至少压测环境和预发环境跑3到7天观察连接数曲线和内存曲线。这些指标不需要一步到位但至少要有基线数据。以后线上出问题才有数据可以对比。最后再分享一个我个人踩了很多次坑之后的体会做国产数据库适配心态上不能把它当成“MySQL换壳”SQL方言、类型映射、锁行为、字符集、保留字每一个角落都可能藏着问题。最稳妥的路径是先把SQL模板逐条过一遍再上线压测最后再谈优化。先把一条链路完整跑通比一次性铺开所有功能要高效得多。希望这篇记录能帮你少走点弯路。