文章目录
- 先掰扯一下"兼容MongoDB"这话到底指什么
- 时间线捋一下,你就知道这需求是怎么逼出来的
- 兼容度那张表,我给你翻译成人话
- 迁移前那几天,我是这么盘代码的
- 三个文档库特有的坑
- 性能:写入这块它真不如原生
- 先停这儿
上周三还是周四,记不清了,反正是个下雨的晚上,一个合作五六年的老客户给我打电话。
开口第一句是"哥,文档库要国产化,这咋整"。
我当时正啃一个存储过程,脑子没转过来,回了句你那不是MongoDB么,跑得挺好啊换它干啥。他说上面来检查了,要求全栈替代。关系库换完了,缓存换完了,中间件也换完了,就剩这个文档库还杵在那儿。然后他去翻名录,翻了三遍——没有"文档数据库"这么一个独立品类。
这事儿就挺荒诞的。你翻目录找不着东西,不是说这东西不重要,是压根没人给它单独设个格子。
那摆在面前的就三条路。第一条,硬着头皮继续用,赌没人较真。第二条,自己找个开源方案套一层,出事了没人兜底。第三条,上个"能存文档的关系库",但心里没底——真能替吗?性能掉多少?代码要改多少?那哥们儿原话是"我这心里跟揣了只兔子似的"。
后来我们试了第三条路,用的就是KingbaseES的MongoDB兼容版。这篇先讲迁移那一段,下一篇讲迁完之后代码到底怎么写。
结论我先撂这儿:能替。但"能替"这两个字里面藏着一堆前提,我一个个摆出来。我不是来吹的,它有明显不如原生的地方,我照实说。
先掰扯一下"兼容MongoDB"这话到底指什么
这词太容易被糊弄了。
我见过太多方案在PPT上写"全面兼容MongoDB",一上手——驱动连不上。你说这不扯淡么。
所以我自己有个三层判断法,你拿去验任何号称兼容的东西都行。
最硬的一层是协议。你的应用用的是官方MongoDB驱动,连接串指着27017端口,驱动发出去的是那套wire protocol二进制协议。对方得在这个端口上听懂这套东西。听懂了你一行代码不用改就能连上去。听不懂,后面全别提,什么语法兼容都是废话。
往上一层是命令和操作符。连上之后,db.collection.find()、$group、$lookup、updateOne这些,人家认不认、认多少。这一层直接决定你的工期。认八成和认九成五,中间差的是几个通宵。
最里一层,存储和语义。数据怎么落盘、索引怎么建、事务怎么保证、_id的唯一性怎么维持。这层最隐蔽,也最容易做假。很多方案前两层糊弄过去了,第三层是个黑盒,你跑半年数据对不上,都不知道从哪儿查起。
我为什么这么较真分层。因为踩过。早些年有个项目,方案商拍胸脯说"我们支持文档存储",结果怎么着,把JSON整个塞进一个大文本字段,查询靠LIKE '%xxx%'。索引?用不上。几百万条就卡死了。它支持了吗?字面上还真支持了。但那不叫兼容,那叫糊弄,属于把"能存"偷换成"能用"。
这个兼容版我实测下来,三层都过了。而且做法有点意思——不是在外面套个转换层,是往内核里塞了一套协议解析的组件。这个待会儿细说。
时间线捋一下,你就知道这需求是怎么逼出来的
早年根本没人管文档库国产化这事儿。那会儿信创的重点是核心交易系统,Oracle、DB2能换掉就谢天谢地了。
第一步,初始化的时候就得把兼容模式定下来。
# initdb,-m 指定兼容模式,-U 指定默认用户# -m 具体填什么看部署手册,别自己猜initdb-D/data/kes_data-m<兼容模式>-Usystem参数值我不写。不是藏着,是不同版本、不同手册里写法有差异,我写死了反而害你。但有一句话我得说重:这个模式是决定后面协议能不能通的总开关,一开始就得定好。等库都跑起来了才发现选错,那就得重装。
第二步,改kingbase.conf。
enable_protocol_compat = on extension_protocol_port = 27017 # BSON 用 EJSON 形式表示,建议打开,不然看到的数据格式很别扭 documentdb_core.bsonUseEJson = on # 预加载,缺一个后面插件都起不来 shared_preload_libraries = 'kdb_cron,kdb_documentdb_core,kdb_documentdb'shared_preload_libraries这一行,如果你原来已经有别的预加载项,是往后追加,别把原来的覆盖了。这坑我见过不止一次,有人复制粘贴把整行替换掉,结果原来的功能全挂,排查了半天才发现是自己手贱。
第三步,建插件。
-- ksql 连进去,注意端口 54321,跟 27017 是两套,别搞混-- ksql -Usystem -p 54321 dbnamecreateextension documentdbcascade;-- 顺手把密码设一下,等下驱动要用alterusersystemwithpassword'123456';第四步,验证。
mongosh"mongodb://system:123456@127.0.0.1:27017/dbname?maxPoolSize=1&directConnection=true&authMechanism=SCRAM-SHA-256"连上之后你就当它是MongoDB用。
db.products.insertMany([{item:"card",qty:15},{item:"envelope",qty:20},{item:"stamps",qty:30}]);db.products.find({item:"envelope"});db.products.createIndex({item:1});跑通那一刻挺爽的。应用侧真的什么都不用改,就是把连接串端口改到27017。这话听着像广告,但我实测就是这样——Java的、Python的、Go的驱动都试了,全是官方驱动,一个没换。
别急着高兴。能连上只是第一步,坑都在后头。
兼容度那张表,我给你翻译成人话
厂商给了张兼容度表。我一开始以为又是凑数的,仔细看完发现它挺诚实——该低的地方真写了低。
| 项目 | 数量/总数 | 支持率 | 我的解读 |
|---|---|---|---|
| 命令 | 7/7 | 100% | insert/find/update/drop这类基础的全通 |
| 查询操作符 | 31/32 | 96.88% | 差一个,日常无感 |
| 更新操作符 | 22/22 | 100% | $set/$inc/$push/$addToSet全在 |
| 投影操作符 | 3/3 | 100% | 位置投影、$slice都有 |
| 聚合管道阶段 | 32/38 | 84.21% | 开始掉了,注意 |
| 聚合管道操作符 | 167/169 | 98.82% | 基本全 |
| 管理命令 | 22/32 | 68.75% | 掉得明显 |
| 身份认证命令 | 3/4 | 75% | 够用 |
| 用户管理命令 | 5/7 | 71.43% | 够用 |
| 查询计划缓存 | 0/4 | 0% | 完全不支持 |
| 角色管理 | 0/10 | 0% | 完全不支持 |
| 会话命令 | 6/8 | 75% | 一般 |
| 诊断命令 | 18/25 | 72% | 一般 |
这张表暴露了两个真短板,也暴露了一个假短板。得分开说,混着说就把自己吓死了。
真短板之一是聚合管道阶段缺了6个,84.21%。这个有点要命。聚合是MongoDB里最常用的复杂查询手段,$lookup、$facet、$graphLookup这类稍微花哨点的,你项目里用了几个?用了缺的那几个,代码就得改。
我给个土办法,比看任何百分比都准:把你自己代码里用到的聚合阶段全grep出来,跟这38个一个个对。对完你手上就是一张真实的工期表,不是别人给的营销数字。
真短板之二是管理命令只有68.75%。fsync、fsyncUnlock、convertToCapped这些暂不支持。业务代码平时碰不到,但运维脚本里可能会用。比如你有个定时任务调fsync刷盘,迁过去直接报错。这种坑最阴,因为开发和测试完全测不出来——谁会在测试环境跑运维脚本啊——等上线了半夜炸。
至于角色管理0%、查询计划缓存0%,这个我反倒不慌。为什么。因为这两件事在KES里本来就有,只是不通过MongoDB协议这个口子提供。角色权限你用KES自己的用户角色体系去管,功能比MongoDB那套还细。查询计划缓存你没这个命令,但库自己有执行计划管理。
打个比方,你换了个牌子的车,方向盘上按钮位置变了,功能还在,就是得重新找一下。
索引这块主流都覆盖了:默认_id索引、单字段、复合、多键、文本、通配符、hash、2dsphere、2d。唯一明确不支持的是geohaystack——那玩意儿MongoDB里早废了,基本没人用,可以忽略。
迁移前那几天,我是这么盘代码的
光知道兼容度没用,你得知道你自己的代码用了什么。
这活儿听着枯燥,但它省下来的是上线后半夜被电话叫起来的次数。我给客户做的清单大概四步,你直接抄。
第一步,把集合名和操作统计出来。MongoDB驱动一般能开profiling,跑几天生产,慢查询和操作类型全捞出来。
// 开profiling,慢操作记下来(生产慎用,先把阈值调高点)db.setProfilingLevel(1,{slowms:100});// 捞出来看db.system.profile.find({op:{$in:["query","update","insert","command"]}}).sort({ts:-1}).limit(200);第二步,把聚合阶段提取成清单,跟那38个对。
grep-rn'\$lookup\|\$facet\|\$graphLookup\|\$bucket\|\$setWindowFields\|\$merge'src/为什么单挑$lookup和$facet。因为这俩业务代码最爱用,一个做文档内关联,一个一次查询出多个维度的统计。真缺了,改造成本不小。
第三步,把运维脚本过一遍。所有fsync、convertToCapped、db.stats()这类调用,一个个标出来。这块最容易漏,因为运维脚本通常不在代码仓库里,躺在某台跳板机的/home/xxx/scripts下面,谁也不敢动。你去问运维要,别指望开发知道。
第四步,把用了什么驱动、什么版本列出来。官方驱动基本都通,但有些老项目用的是特别古早的版本,或者套了某层ORM的私有封装,这种得单独验。
几百GB、TB级就得用正经迁移工具了,一般是配套的KDTS那类,支持并行、断点续传、增量追平。这块我不展开,逻辑跟关系库迁移一样:全量打底、增量追平、校验、割接。
但文档库有个专门的坑得提醒——_id的类型。MongoDB的_id默认是ObjectId,但也有项目自己用字符串、用数字、甚至用复合结构。搬的时候类型对不上(比如ObjectId被当字符串存了),后面所有按_id的查询全废。搬完必须抽样验一下_id的类型和值,别只看条数对不对。
三个文档库特有的坑
这几个跟关系库迁移完全不是一回事,写下来给你避雷。
嵌套数组的元素更新,语义特别容易错。MongoDB更新数组元素有一堆位置操作符:$是匹配的第一个、$[]是全部、$[<identifier>]配合arrayFilters做条件过滤。这套东西语义很微妙。我见过一个案例,arrayFilters条件写得含糊,在原库上因为文档结构恰好简单所以结果是对的,迁过去之后数据结构稍微复杂一点,更新到了不该更新的元素上。这类问题不报错,只是数据悄悄变了。最恶心。所以涉及数组元素更新的逻辑,迁完必须写对拍脚本,两边跑同一批操作比结果。
日期和时区也是个大雷。MongoDB存的是UTC的ISODate,很多应用在展示层做时区转换。迁到关系库底座上,服务端时区、字段类型映射任何一处不一致,你就会看到"时间差8小时"这种经典事故。而且它不一定全局差8小时,可能是某些写入路径差、某些不差,排查起来要命。迁移前把服务端时区统一成UTC,应用层自己转,别在数据库层做隐式转换。
还有个$type,说到底就是类型宽松度的问题。MongoDB是弱类型的,同一个字段在不同文档里可以是数字、字符串、甚至数组,$type就是用来处理这种混乱的。迁到关系库底座上,底层是强类型的BSON序列化,大部分情况没问题,但你的数据里本来就有"脏类型"(同一字段混着存数字和字符串)的话,迁移后可能出现排序不一致、比较不一致。这种脏数据在MongoDB里能混着活,在哪儿都是隐患。迁移正好是个清理的机会,别浪费。
性能:写入这块它真不如原生
厂商给了组对比数据,我盯着看了半天,因为它没藏着掖着,有些数挺扎眼。
大致情况:写入MongoDB更快。百万级数据量下,KES的INSERT和UPDATE耗时明显高于MongoDB。查询反过来,KES快得多,尤其全表扫描和单字段查询,差距是数量级的。
具体数字我不贴了。那张表行列排布有点乱,我怕转述错了误导你。但趋势清楚——写慢,读快。
为什么。我自己的分析三点,你判断对不对。
一是索引开销。前面说了,单字段索引映射成RUM,RUM比B树大、维护成本高,每条写入都要更新索引,自然慢。这是为查询性能付的账。
二是行存加事务开销。KES本质是关系库,写入要走完整的事务机制、WAL日志、MVCC版本管理。这套东西保证了强一致和事务安全,但单条写入的固定开销比MongoDB那种轻量写入大。MongoDB早期为了性能在一致性上做了不少妥协,现在虽然也有多文档事务了,但默认配置下还是轻。
三是协议转换。MongoDB协议进来,要解析、要转成内部的关系操作。这层转换本身有成本,不大,但架不住量大。
所以结论是:读多写少的场景,比如内容管理、商品详情、配置中心、日志查询,这个替换几乎白赚;超高并发写入的场景,比如IoT时序上报、海量埋点,得慎重,先压测,别直接上。
这话我说重一点。我见过有人拿这个去替一个每秒几万写的场景,结果写入堆积,最后怪工具不行。工具没错,是选错了场景。任何技术选型都有它的甜蜜区,硬拗就是自找麻烦。
先停这儿
迁移这条线基本捋完了:三层判断标准、四步配置、兼容度表怎么读、数据怎么存、性能的真实取舍。
还是开头那句,能替,但有前提。你的聚合管道没用到缺的那6个阶段、你的运维脚本没依赖那些不支持的管理命令、你的写入量没到临界点。这三条自己对着代码和监控过一遍,比听我说一百句管用。
下一篇讲更实际的——迁过去之后代码到底怎么写。因为你会发现一件挺有意思的事