前言
随着业务发展,MongoDB 中的核心业务流(如停车流水、订单日志等)会逐渐演变成几千万甚至数亿级别的超大集合。为了保障核心查询性能和控制存储成本,冷热数据分离成为了必经之路。
核心迁移方案全景对比
根据不同的业务特征和基础架构能力,冷热分离有以下主流架构选型。
基于 Change Stream / FlinkCDC 实时同步 + 异步清理
做法:
- 使用 Flink CDC 监听 MongoDB 的 Oplog(Change Stream)。
- 将符合"冷数据"规则的记录实时过滤,并写入下游的归档库(可以是另一个低成本 MongoDB 实例、MySQL 或对象存储)。
- 在 MongoDB 侧,为该表建立 TTL(Time-To-Live)索引,或者写一个低频定时任务,在业务低峰期以极小的批次(如每次 500 条)进行平滑删除。
优势:
- 极致安全: 借助 Flink 的 Checkpoint 机制,即使任务崩溃,恢复后也能从上一个点位继续消费,不会漏数据。
- 无更新丢失风险: CDC 捕获的是最终变更流。如果一条冷数据在搬运途中突然被业务更新,Oplog 会原样记录,目标端也会随之更新。
- 性能隔离: 数据的"搬运"动作彻底脱离了 MongoDB 的查询引擎,源库只需要安心服务线上业务即可。
- 读写分离,搬运过程不占用数据库本身的计算资源,数据一致性好。但是以上优势针对此次停车流水冷热备份场景不太明显。
**劣势:**技术架构较复杂。
| 评估维度 | 方案 A:Flink CDC 实时同步 + 异步清理 | 方案 B:定时任务微批(读+写+删) |
|---|---|---|
| 技术复杂度 | 高。需维护 Flink 集群、CDC Connector 及状态管理(Checkpoint)。 | 低。编写普通 Java/Python 脚本配合 XXL-JOB 等调度器即可。 |
| 源库性能损耗 | 极低。CDC 直接顺序读取 Oplog,不走查询引擎,对源库几乎无感。 | 较高 。需频繁执行带条件的 find 和索引扫描,可能与业务争抢 CPU 和 I/O。 |
| 数据安全性 (一致性) | 极高。基于日志的 Exactly-Once 语义,无论中途断电还是重启,都能精准接续。 | 中等。高度依赖脚本的幂等逻辑设计,极端宕机情况下需人工介入对账。但此次同步的都是废弃的流水表,就算真丢了也不心疼,也不用费心去找回来。 |
| 并发修改冲突 | 完美解决。数据若在迁移瞬间被业务修改,CDC 会捕获最新状态。 | 存在隐患。脚本读出数据后、删除前的这个时间差内,若业务发生了 Update,这部分更新会被永久丢失。本次同步的都是流水数据,不会频繁更新。 |
按时间分表与集合重命名(物理隔离,最快释放空间)
如果冷热规则是严格按照"时间"划分的(比如只保留最近 3 个月的数据),不要去原表里删数据,而是直接按周期建表。
- 做法:
- 业务端按月或按季度写入不同的集合,例如
order_202605,order_202606。或者利用分库分表中间件(如 ShardingSphere 的逻辑)在应用层做路由。 - 当某个月的数据变冷时,直接通过
db.collection.renameCollection("order_202512", "order_202512_cold")将其重命名,或直接使用drop()删除整个集合。
- 业务端按月或按季度写入不同的集合,例如
- 优势:
rename和drop都是元数据级别的操作,瞬间完成,没有任何 Oplog 负担,且drop会立刻向操作系统释放磁盘空间。 - **劣势:**数据库会出现一堆时间表,不好管理,且需经常频繁的建表。
使用原生的 Time Series 时间序列集合(针对 5.0+ 版本)
如果数据特征是机器指标、日志、事件流等持续写入且带时间戳的数据,强烈建议迁移到 MongoDB 原生的 Time Series 集合。
- 做法: 建表时指定按时间字段分区,并直接设置
expireAfterSeconds。 - 优势: MongoDB 底层会自动对数据进行列式存储压缩,并且内部按照时间桶(Time Bucket)管理数据。过期清理时直接丢弃整个桶,效率极高,完全规避了逐行删除的性能瓶颈。
- 劣势: 生产mongo版本为
4.4.29,如果使用此种方案,需要升级mongo,把原有数据迁移到新集合,改动太大。
兜底方案:定时任务代码搬运和删除。
如果目前受限于业务逻辑,只能采用最初的"查询 -> 插入 -> 删除"方案,请务必遵循以下平滑处理原则来写脚本:
- 强制分批与延时(休眠): 绝对不能一次性
deleteMany几十万条数据。应该利用_id范围或时间索引进行分页,每次bulkWrite删除 500 - 1000 条,然后在代码中强制sleep几百毫秒。这叫"细水长流",留出资源让 Oplog 同步和业务查询呼吸。 - 避免使用大偏移量的 Skip: 查冷数据时,基于上次查询的最大
_id继续向后查(find({_id: {$gt: last_id}, status: "closed"}).limit(1000)),绝对不能用.skip(100000),否则查询本身就会把库查挂。 - 定期执行 Compact: 搬运完毕后,在一个周末的低峰期,在 Secondary 节点上逐个执行
db.runCommand({ compact: "your_collection_name" })回收磁盘空间,最后再主备切换处理原来的 Primary。
故根据以上描述,建议使用定时任务微批处理数据。
疑问解答
大量删除会触发锁表或性能损失吗?
结论:不会"锁表",但会带来严重的性能灾难。
MongoDB 默认使用 WiredTiger 存储引擎,采用的是文档级锁(行级锁),而不是表级锁。因此,删除操作本身不会阻塞整个集合的读写,但对于超大表的大规模删除,会带来以下致命的性能损失:
- Oplog 暴涨拖垮主从同步: MongoDB 是通过 Oplog 来维持高可用(HA)集群的主从同步的。你每删除一条文档,就会生成一条 Oplog 记录。海量删除会导致 Oplog 瞬间激增,挤占网络带宽,并极易导致 Secondary 节点同步延迟,甚至因 Oplog 覆盖而导致节点脱节。
- **解答:**所以采用定时任务微批处理可以较好缓解此问题,而且正式集群是单节点。
- 极高的 CPU 和 I/O 消耗: 删除数据需要频繁更新 B-Tree 索引。百万或千万级别的删除会导致内存中的缓存页被大量换出,磁盘 I/O 飙升,直接影响线上正常查询请求的响应时间。
- **解答:**所以采用定时任务微批处理可以较好缓解此问题。
- 磁盘空间不会自动释放(碎片化): 在 WiredTiger 引擎中,
delete操作只是将数据页标记为"空闲",并不会立刻将磁盘空间还给操作系统,反而会留下大量碎片。要回收空间,后续还需要通过高耗时的compact命令进行碎片整理,而compact期间会阻塞所在节点的读写。- 解答: 删除操作确实并不会真的磁盘释放空间,只是在WiredTiger 引擎中标记为空闲,需要手动
compact释放磁盘物理空间,但是被标记的。MongoDB 的 WiredTiger 存储引擎绝对不会 在后台自动执行compact命令。compact是一个高耗能、在旧版本甚至会锁集合的操作,必须由运维或开发人员手动发起。 - 如果不手动整理,磁盘空间会一直被无限制占用吗? 在操作系统层面,是的;但在 MongoDB 内部,不是。这是很多人对 MongoDB 的一个误解。当你删除了历史停车流水数据后,操作系统看到的 mongod 磁盘占用确实完全没有变小。但是,这些被删除的数据所释放的空间,在 MongoDB 内部并没有消失,它们会被移入一个叫做 Free List(空闲列表) 的内部字典中。当产生新的数据时,MongoDB 会优先复用这部分 Free List 中的空闲空间,而不会去向操作系统申请新的磁盘切片。
- 结论: 只要每天删除的数据量与每天写入的数据量基本平衡,MongoDB 的磁盘占用就会维持在一个稳定的水位,不会无限膨胀 。因此,不需要频繁甚至根本不需要去执行手动
compact。
- 解答: 删除操作确实并不会真的磁盘释放空间,只是在WiredTiger 引擎中标记为空闲,需要手动
- 事务超时机制: MongoDB 的事务(Transaction)默认有 60 秒的生命周期限制(
transactionLifetimeLimitSeconds)。超大表的批量搬运极易超时,导致不断回滚,永远无法完成任务。- **解答:**采用定时任务微批处理可以较好缓解此问题。
- 内存撑爆(OOM)风险: 事务在提交前,所有的修改(包括海量的 Delete 操作产生的 Oplog)都会驻留在内存中。大批量删除会瞬间吃光 WiredTiger 引擎的 Cache,导致数据库整体卡死。
- **解答:**采用定时任务微批处理可以较好缓解此问题。
- 跨系统无法保证事务: 如果你的冷数据是迁移到另一个 MongoDB 集群、MySQL 或大数据平台,MongoDB 原生的事务根本无法跨越异构系统保证 ACID。
- **解答:**采用定时任务微批处理可以较好缓解此问题,且是同步到当前MongoDB集群的另外一个集合。