资讯动态

AWS DocumentDB迁移避坑:兼容性体检、读写分离、备份恢复

发布时间:2026/10/6 11:04:46 来源:尧图企业网站定制
简介《AWS DocumentDB云服务最佳技术实践》是一份面向云架构师、后端开发者与运维人员的PDF技术资料可帮助团队在评估或使用AWS DocumentDB时快速理清架构选型与落地路径。内容系统覆盖DocumentDB核心能力完全兼容MongoDB 3.6 API与常用SDK原生支持JSON嵌套文档存储可通过多可用区副本集获得99.99%可用性并支持从小规模分片起步、扩展到较大存储容量同时介绍了设置读取副本与读写偏好以分散流量、借助安全策略加密和网络隔离加固数据保护以及通过CloudWatch监控和自动快照实现快速恢复等最佳实践。压缩包共1个PDF文件体积约2.17MB适合快速查阅实例规格、连接参数与配置示例。目前已有144人学习/下载对正在规划AWS DocumentDB生产环境、希望替代自建MongoDB并降低运维复杂度的团队尤其实用。1. AWS DocumentDB 是什么托管的是运维不是 MongoDB 的全部AWS DocumentDB 是 AWS 托管的 MongoDB 兼容文档数据库卖点是把自建副本集的运维负担全部收走节点切换、备份、磁盘扩容都变成控制台上的一次点击。很多人把它当成“MongoDB 托管版”上来就迁结果被 Change Streams 不兼容这类问题卡住。这里要先说清楚它兼容的是 Mongo wire protocol 和大部分 CRUD/聚合语义不是所有 MongoDB 特性。下面按我实际迁移和压测的路径讲懂兼容性边界、迁移路线、读写分离、性能避坑、备份恢复这几件必做事给你能直接抄的落地步骤。2. 兼容性体检哪些 MongoDB 功能在 DocumentDB 上会翻车2.1 先说原理为什么“协议兼容”不等于“特性兼容”DocumentDB 不是 MongoDB 的发行版而是 AWS 自研存储引擎对外套了一层 MongoDB wire protocol。换句话说你的 MongoDB 驱动能连上来大部分 CRUD、索引、聚合管道的写法和 MongoDB 一样但底层没有 MongoDB 的 WiredTiger、没有分片、没有 MongoDB 的完整特性栈。这个定位决定了它的兼容性策略能省则省能替代则替代。最容易踩的坑是把 MongoDB 4.x 的高阶能力直接搬上来。比如 Change Streams在 MongoDB 里是监听集合变更的标准姿势DocumentDB 早期版本完全不支持后期版本也只在特定场景下可用。再比如$text全文索引和 MapReduceDocumentDB 一直不支持官方推荐你用 aggregation pipeline 或者把全文检索放到专用引擎里。还有一个隐蔽的坑是 collation排序规则MongoDB 里可以给索引和集合定义 collationDocumentDB 对它的支持很有限一旦表结构里带了 collation迁移后查询行为可能和源库不一致。所以做技术选型时我一般会先拉一张功能对照表把应用里实际用到的 MongoDB 特性过一遍筛而不是对着官方文档的“兼容 MongoDB 3.6/4.0”这句话拍板。下面这两个脚本就是用来干这件事的。2.2 用脚本给存量库做一次兼容性体检先把源库的集合元数据扫一遍。以下脚本在 mongosh 里执行遍历集合并检查三类高危项capped collection、文本索引、collation。// 兼容性体检连接 MongoDB 源库输出集合与索引层面的风险项 const conn new Mongo(mongodb://user:passsource-host:27017/?authSourceadmin); const db conn.getDB(appdb); db.getCollectionNames().forEach(collName { const coll db.getCollection(collName); const stats coll.stats(); const indexList coll.getIndexes(); const risks []; if (stats.capped stats.max) { risks.push(capped collection 且设置了 maxDocumentDB 的 max 行为与 MongoDB 不一致); } indexList.forEach(idx { if (idx.textIndexVersion || idx.weights) { risks.push(集合 ${collName} 包含 text index(${idx.name})DocumentDB 不支持全文索引); } if (idx.collation) { risks.push(索引 ${idx.name} 定义了 collationDocumentDB 对 collation 支持有限); } }); print(risks.length ? [风险] ${collName}:\n ${risks.join(\n )} : [通过] ${collName}); });脚本逻辑不复杂coll.stats()能拿到capped和max标志getIndexes()能看每个索引类型。textIndexVersion出现说明这是文本索引collation字段出现说明索引带了排序规则。跑完后你会得到一张“哪些集合有风险”的清单接下来按集合逐个处理就行。再看应用代码。用 grep 扫一遍仓库找出哪些 MongoDB 高级特性被实际调用过grep -rEn watch\(|changeStream|mapReduce|\\$text|createIndex\(.*text|retryWrites \ --include*.js --include*.ts --include*.java --include*.py ./src | head -50\\$text在正则里匹配字面量$text对应全文查询retryWrites是驱动参数DocumentDB 对可重试写支持有限扫出来之后在连接串里显式关掉。这一步能避免你把代码迁到 DocumentDB 之后才发现某个核心接口跑不通。2.3 特性对照表哪些能用哪些必须换方案功能MongoDBDocumentDB建议替代方案CRUD / 聚合管道完整支持大部分支持直接在 DocumentDB 上跑多文档事务4.0 起完整支持支持但存在集合数等限制压测验证事务场景优先控制单集合内完成Change Streams支持不支持或受限用 AWS DMS 做变更同步或用应用层轮询MapReduce支持不支持改写为 aggregation pipeline$text全文索引支持不支持换用 Amazon OpenSearch 或外部全文检索TTL 索引支持支持注意字段必须是 Date 类型Capped Collection支持支持不要依赖 max 精确上限行为Collation支持有限支持尽量去掉 collation或按大小写敏感重新设计索引这张表是我每次写迁移方案都会贴在文档开头的。注意一个细节TTL 索引在 DocumentDB 里能用但过期只认 Date 类型的字段如果原库存的是字符串时间迁移后要先把字段类型转成 ISODate。Capped Collection 也一样别把它当成无限容量的环形队列用DocumentDB 对它的实现比较保守超过上限后行为不如 MongoDB 那么可预期。3. 迁移到 DocumentDBDMS 与 mongodump 两条路线怎么选3.1 在线迁移选 AWS DMS业务不停服的唯一选择如果线上读写不能停唯一靠谱的路是 AWS Database Migration Service。DMS 先做 full load再切到 CDC持续变更捕获源库的 MongoDB 副本集不停目标 DocumentDB 数据追平后直接切换应用。这条路的难点在配置源 endpoint、目标 endpoint、复制实例、迁移任务四层都要打通网络要在同一个 VPC 或通过专线可达安全组互相放行 27017 端口。DMS 的任务创建用 CLI 大致是这个样子aws dms create-replication-task \ --replication-task-identifier migrate-app \ --source-endpoint-arn arn:aws:dms:us-east-1:123456789012:endpoint:source-mongo \ --target-endpoint-arn arn:aws:dms:us-east-1:123456789012:endpoint:target-docdb \ --replication-instance-arn arn:aws:dms:us-east-1:123456789012:rep:replication-instance \ --migration-type full-load-and-cdc \ --table-mappings file://table-mappings.json \ --replication-task-settings file://task-settings.json--migration-type full-load-and-cdc表示先全量再增量。表映射文件里用 MongoDB 的规则要按 database/collection 来定位对象{ rules: [ { rule-type: selection, rule-id: 1, rule-name: all-collections, object-locator: { schema-name: appdb, table-name: % }, filters: [] } ] }table-name: %是通配把 appdb 库下的所有集合都选进来。DMS 的 CDC 阶段会去读 MongoDB 的 oplog所以源库副本集必须开启了 oplog并且复制实例的存储空间要够放变更日志。老版本 MongoDB 的 oplog 如果太小full load 期间业务写入量大oplog 被覆盖CDC 就会断。我见过因为 oplog 只有 2GB 导致 DMS 同步中断的案例解决办法是提前把源库 oplog 调大或者用--max-full-load-task这类参数控制并发。3.2 离线迁移选 mongodump / mongorestore命令与参数允许停服的话我一般用 mongodump 导出、mongorestore 灌入比 DMS 简单直接也少一层复制实例的支出。导出时用 archive 单文件压缩避免生成几万个散落的 BSON 文件mongodump --urimongodb://user:passsource-host:27017/?authSourceadmin \ --gzip --archivemongo-backup.dump导入到 DocumentDB 时连接串必须带ssltrue并指定副本集名rs0。DocumentDB 强制 TLS证书从 AWS 官方 CA bundle 下载后放到本地mongorestore mongodb://docdb-user:passdocdb-prod.cluster-xxxx.us-east-1.docdb.amazonaws.com:27017/?ssltruereplicaSetrs0readPreferencesecondaryPreferred \ --archivemongo-backup.dump --gzip --drop --numParallelCollections4 \ --sslCAFile rds-combined-ca-bundle.pem参数分几层说--drop会在导入前清空同名的目标集合防止重复数据--numParallelCollections4控制并发集合数你想快一点可以调到 8但 DocumentDB 实例的 CPU 会明显飙高建议从 4 起步。ssltrue是必须的replicaSetrs0也是必须的少了它驱动不知道去发现副本集成员。还有readPreferencesecondaryPreferred让导入过程中的读请求走从节点主节点专注写导入性能会稳定一些。这里有个容易被忽略的点mongodump 导出时会把索引定义一起带上但 DocumentDB 不认 text index所以导入前最好用下面这条命令先把目标库里所有 text index 删掉mongosh mongodb://docdb-user:passdocdb-prod.cluster-xxxx.us-east-1.docdb.amazonaws.com:27017/?ssltruereplicaSetrs0 \ --eval db.getCollectionNames().forEach(c { c.getIndexes().filter(i i.textIndexVersion).forEach(i c.dropIndex(i.name)); })删掉之后再按 DocumentDB 的索引规范重建避免导入过程中碰到不支持的索引类型直接失败。3.3 迁移后的数据校验不能只看行数数据灌完很多人都只对比 count 就宣布迁移完成。count 对得上不能说明字段值没丢我一般会再跑一个校验脚本先比 count再对_id列表做哈希对比抽样或全量都行。脚本如下from pymongo import MongoClient from hashlib import md5 src MongoClient(mongodb://user:passsource-host:27017/?authSourceadmin) dst MongoClient(mongodb://user:passdocdb-prod.cluster-xxxx.us-east-1.docdb.amazonaws.com:27017/?ssltruereplicaSetrs0readPreferencesecondaryPreferred) for coll_name in [users, orders, events]: s_coll src[appdb][coll_name] d_coll dst[appdb][coll_name] s_cnt s_coll.count_documents({}) d_cnt d_coll.count_documents({}) print(f[{coll_name}] counts: src{s_cnt} dst{d_cnt} diff{s_cnt - d_cnt}) # 大集合别全量 to_list这里演示用 _id 的哈希做快速核对 s_ids md5(str([doc[_id] for doc in s_coll.find({}, {_id: 1}).limit(100000)]).encode()).hexdigest() d_ids md5(str([doc[_id] for doc in d_coll.find({}, {_id: 1}).limit(100000)]).encode()).hexdigest() print(f id-hash: src{s_ids} dst{d_ids} match{s_ids d_ids})脚本逻辑是取前 10 万条的_id列表做 MD5源和目标一致说明集合顺序和数量基本对上。真实生产环境数据量大时我会按_id范围分段每段一万条一段段比 MD5。另一个更快的办法是两边各自跑$sample抽几千条文档逐条转成 JSON 后做深度比对。注意DocumentDB 的只读节点读到的数据可能有复制延迟校验时最好使用主节点 endpoint 或把readPreference临时设为primary否则会看到“源库有、目标库还没有”的假差异。4. 连接读写分离Cluster Endpoint、副本集 URI 与连接池参数4.1 理解三类 Endpoint为什么不能用 primary 一把梭DocumentDB 集群有三种连接地址Cluster Endpoint 指向当前主节点负责写和强一致读Reader Endpoint 在多个只读副本之间做负载均衡适合读多写少的报表查询Instance Endpoint 指向某个具体节点主要用于运维排查。很多人只用一个 Cluster Endpoint把所有读都打过去导致主节点成为瓶颈只读副本闲着。更隐蔽的问题是副本集发现机制。DocumentDB 的 URI 里固定要写replicaSetrs0驱动连上 Cluster Endpoint 后会去发现集群里的其他节点。如果你把readPreference设成secondaryPreferred驱动就会把读请求分发到副本节点。但注意这个“分发”是驱动自己维护的拓扑视图决定的不是 AWS 的负载均衡器所以你在 Reader Endpoint 上设置readPreferenceprimary是无效的因为 Reader Endpoint 后面只有只读节点驱动会发现没有可用主节点。4.2 Python / Node.js 连接 URI一组合适的参数值Python 驱动PyMongo连 DocumentDB 的标准 URI 长这样from pymongo import MongoClient uri ( mongodb://docdb-user:xxxxdocdb-prod.cluster-xxxx.us-east-1.docdb.amazonaws.com:27017/ ?ssltrueretryWritesfalsereplicaSetrs0 readPreferencesecondaryPreferredmaxPoolSize50socketTimeoutMS90000 ) client MongoClient(uri, appnamedocdb-best-practice) db client[appdb]Node.js 驱动配置几乎一样只是把useNewUrlParser和useUnifiedTopology这两个历史选项显式带上更稳const { MongoClient } require(mongodb); const uri mongodb://docdb-user:xxxxdocdb-prod.cluster-xxxx.us-east-1.docdb.amazonaws.com:27017/?ssltrueretryWritesfalsereplicaSetrs0readPreferencesecondaryPreferredmaxPoolSize50socketTimeoutMS90000; const client new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true, }); client.connect().then(() { const coll client.db(appdb).collection(users); });关键参数逐一说ssltrue是硬性要求少了直接拒绝连接retryWritesfalse是为了避免驱动默认开启 MongoDB 的 retryable writesDocumentDB 对这类重试机制支持不完整开启后极端情况下会出现重复写readPreferencesecondaryPreferred让读默认走副本maxPoolSize是每个连接池的上限经验值 50 左右过高会把实例的 CPU 和内存吃满过低则容易在流量高峰触发等待。socketTimeoutMS我习惯给 90 秒DocumentDB 的某些聚合查询比较慢阈值太短会被驱动误判为死连接。4.3 连接池与 Serverless 场景几个容易忽略的参数连接池不是越大越好。DocumentDB 每个实例的可用内存有限连接池越大各驱动缓存的连接越多空闲连接占着内存和文件描述符。我见过一个业务把 maxPoolSize 调到 300实例还是 r5.xlarge结果还没到流量高峰内存先被连接堆满。合理的做法是先按“并发请求数 / 单个请求数据库耗时”估算比如 1000 QPS、单次查询 5 毫秒那 50 个连接足够。Lambda 场景要另说。每次函数实例冷启动新建数据库连接都会多几百毫秒延迟常见做法是把连接对象放到函数外面做全局复用let client; async function getClient() { if (!client) { client new MongoClient(uri, { maxPoolSize: 5 }); await client.connect(); } return client; } exports.handler async (event) { const c await getClient(); return c.db(appdb).collection(users).findOne({ userId: event.userId }); };检查全局连接是否被回收可以看函数的Init阶段耗时如果每次调用 Init 都在 1 秒以上说明连接没有复用。还有一种被打压的套路Lambda 并发高时连接池maxPoolSize设得太大反而触发 DocumentDB 的 max connections 限制这时候把池子降到 510配合 RDS Proxy 做连接多路复用稳定性更好。5. 避坑笔记DocumentDB 最常见的翻车点和排查方法5.1 无索引的全集合扫描CPU 瞬间打满现象业务高峰期DocumentDB 主节点 CPU 从 10% 直接跑到 100%所有写操作面临阻塞代码里明明是单条记录查询但执行计划显示 COLLSCAN。原因find()的过滤字段没有索引。DocumentDB 的存储和计算分离实例 CPU 负责执行查询无索引扫描意味着要把整个集合的数据页从存储拉回到内存逐条过滤IO 和 CPU 都翻倍加上连接池几十个请求并发实例立刻被打满。解决先用 explain 确认执行计划再补索引。db.users.explain(executionStats).find({ org_id: abc, status: active }); db.users.createIndex({ org_id: 1, status: 1 }, { background: true });explain(executionStats)的输出里看stage字段值是COLLSCAN就是全集合扫描IXSCAN才是走索引。createIndex的background: true表示后台创建但 DocumentDB 实际执行时索引构建还是会占用实例资源我一般安排在低峰期跑。索引字段顺序也有讲究等值条件放前面、排序字段放后面和 MongoDB 的复合索引设计规则一致。5.2 排序和聚合把内存打爆看到 sort 报错先别急着加内存现象应用日志里出现Sort exceeded memory limit聚合管道报Exceeded memory limit for $group甚至直接ExceededMemoryLimit异常。原因DocumentDB 继承了 MongoDB 的内存排序上限默认 32MB当排序字段没有索引时查询引擎在内存里排序超过阈值就报错。有人以为升配实例内存就解决了其实完全没用这个限制不是物理内存是查询执行引擎给排序操作分配的上限。解决优先给排序字段建索引临时可通过allowDiskUse让聚合管道把中间结果写到磁盘。db.orders.aggregate( [ { $sort: { created_at: -1 } } ], { allowDiskUse: true } );allowDiskUse: true是后悔药能跑通但不代表效率高磁盘排序比内存排序慢一个数量级。根本解法是建对应索引db.orders.createIndex({ created_at: -1 })。注意如果排序字段上已经有一个索引但查询条件里的等值字段也在同一个复合索引里位置放错会导致排序仍无法使用索引这在explain输出里能看到SORT和SORT_KEY_PATTERN的提示。5.3 读写分离配置没生效主节点还是压满现象URI 里已经写了readPreferencesecondaryPreferred但从监控看主节点 CPU 依然很高从节点几乎空闲。原因最常见是应用代码里某个查询显式指定了读偏好覆盖了连接串配置。PyMongo 里写collection.find().read_preference(ReadPreference.PRIMARY)或者 Node.js 里用setReadPreference(primary)都会把请求打回主节点。另一个原因是secondaryPreferred的 fallback 机制当驱动认为没有可用从节点时会自动降级到主节点读如果从节点数太少、或从节点负载太高被驱动标记为不可用读流量就会悄悄回到主节点。解决全库搜一遍read_preference、readPreference和.readPref(的调用把显式设置全部去掉统一走连接串。同时把 Reader Endpoint 单独拆出来给查询类应用用架构上是双保险。验证是否生效可以看监控里的DatabaseConnections和ReadIOPS从节点的 ReadIOPS 明显上升主节点持平就是分离成功。5.4 TTL 索引不生效过期数据为什么还在现象给集合建了 TTL 索引expireAfterSeconds也设了但文档过了时间依然在集合只增不减。原因DocumentDB 的 TTL monitor 每 60 秒检查一次这个机制和 MongoDB 相同但如果你测试时只在 TTL 之后等了十几秒看不到删除属正常。更常见的两个原因一是字段存的是字符串而不是 DateTTL 监控只认日期类型字符串直接跳过二是 TTL 索引建到了非日期字段上或者建了复合 TTL 索引DocumentDB 不支持复合 TTL 索引只支持单字段。解决先确认数据类型再重建索引。db.sessions.find({ expire_at: { $type: string } }).count(); db.sessions.updateMany( { expire_at: { $type: string } }, [{ $set: { expire_at: { $toDate: $expire_at } } }] ); db.sessions.createIndex({ expire_at: 1 }, { expireAfterSeconds: 0 });expireAfterSeconds: 0表示秒级过期文档里expire_at到达当前时间后后台任务就把它删掉。这里用聚合更新语法[{$set: ...}]把字符串转成日期注意老版本 MongoDB 驱动可能不支持这种更新写法先用 Python/Node 跑脚本逐批转换也行。还有一个坑是 capped collectionMongoDB 和 DocumentDB 都不允许在 capped collection 上建 TTL 索引如果你的集合是 capped要先把capped去掉才能建。6. 故障演练与备份恢复调 PITR 参数用一次切换验证连接池6.1 调对连续备份与 PITR 窗口DocumentDB 的自动快照每天一次默认保留一天只靠快照最多恢复到某个日粒度的时间点。要拿到秒级恢复能力必须在集群上开启连续备份并把备份保留期调长。我一般把保留期设成 7 天既能追溯一周内任意时间点又不会让备份存储费用膨胀太多。恢复操作在控制台选“还原到时间点”指定一个精确到秒的时间即可恢复出来的新集群 endpoint 会变化应用连接串要同步改。6.2 触发一次故障切换验证连接池是否自动恢复做完上面这些还有一道必做的验收强制主备切换。用 CLI 触发aws docdb failover-db-cluster \ --db-cluster-identifier docdb-prod \ --target-db-instance-identifier docdb-prod-us-east-1b切换期间主节点会短暂不可写连接池里已有的连接会被服务端关闭客户端驱动需要重新同步拓扑。下面这个探活脚本可以帮你记录“断连多久、自动恢复多久”import time from pymongo import MongoClient uri mongodb://docdb-user:xxxxdocdb-prod.cluster-xxxx.us-east-1.docdb.amazonaws.com:27017/?ssltrueretryWritesfalsereplicaSetrs0readPreferencesecondaryPreferred c MongoClient(uri, maxPoolSize5, socketTimeoutMS5000) while True: t0 time.time() try: c.admin.command(ping) print(f[ok] {time.time() - t0:.3f}s) except Exception as e: print(f[fail] {type(e).__name__}: {e}) time.sleep(1)第一次做这个演练时我的连接池在切换后 10 秒内就恢复了但应用层报了一大批写入失败。原因是写操作命中的主节点连接被断开后驱动没有立即重连而是等 socket 超时后来我把socketTimeoutMS调低到 5 秒、并把重试逻辑加在应用侧才把整体恢复时间控制在 2 秒内。从那以后我把故障切换演练当作任何一次 DocumentDB 上线的前置条件也建议你把它写进发布清单。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑