前言

随着业务发展,MongoDB 中的核心业务流(如停车流水、订单日志等)会逐渐演变成几千万甚至数亿级别的超大集合。为了保障核心查询性能和控制存储成本,冷热数据分离成为了必经之路。

核心迁移方案全景对比

根据不同的业务特征和基础架构能力,冷热分离有以下主流架构选型。

基于 Change Stream / FlinkCDC 实时同步 + 异步清理

做法:

  1. 使用 Flink CDC 监听 MongoDB 的 Oplog(Change Stream)。
  2. 将符合“冷数据”规则的记录实时过滤,并写入下游的归档库(可以是另一个低成本 MongoDB 实例、MySQL 或对象存储)。
  3. 在 MongoDB 侧,为该表建立 TTL(Time-To-Live)索引,或者写一个低频定时任务,在业务低峰期以极小的批次(如每次 500 条)进行平滑删除。

优势:

  1. 极致安全: 借助 Flink 的 Checkpoint 机制,即使任务崩溃,恢复后也能从上一个点位继续消费,不会漏数据。
  2. 无更新丢失风险: CDC 捕获的是最终变更流。如果一条冷数据在搬运途中突然被业务更新,Oplog 会原样记录,目标端也会随之更新。
  3. 性能隔离: 数据的“搬运”动作彻底脱离了 MongoDB 的查询引擎,源库只需要安心服务线上业务即可。
  4. 读写分离,搬运过程不占用数据库本身的计算资源,数据一致性好。但是以上优势针对此次停车流水冷热备份场景不太明显。

劣势:技术架构较复杂。

评估维度 方案 A:Flink CDC 实时同步 + 异步清理 方案 B:定时任务微批(读+写+删)
技术复杂度 。需维护 Flink 集群、CDC Connector 及状态管理(Checkpoint)。 。编写普通 Java/Python 脚本配合 XXL-JOB 等调度器即可。
源库性能损耗 极低。CDC 直接顺序读取 Oplog,不走查询引擎,对源库几乎无感。 较高。需频繁执行带条件的 find 和索引扫描,可能与业务争抢 CPU 和 I/O。
数据安全性 (一致性) 极高。基于日志的 Exactly-Once 语义,无论中途断电还是重启,都能精准接续。 中等。高度依赖脚本的幂等逻辑设计,极端宕机情况下需人工介入对账。但此次同步的都是废弃的流水表,就算真丢了也不心疼,也不用费心去找回来。
并发修改冲突 完美解决。数据若在迁移瞬间被业务修改,CDC 会捕获最新状态。 存在隐患。脚本读出数据后、删除前的这个时间差内,若业务发生了 Update,这部分更新会被永久丢失。本次同步的都是流水数据,不会频繁更新。

按时间分表与集合重命名(物理隔离,最快释放空间)

如果冷热规则是严格按照“时间”划分的(比如只保留最近 3 个月的数据),不要去原表里删数据,而是直接按周期建表。

  • 做法:
    1. 业务端按月或按季度写入不同的集合,例如 order_202605, order_202606。或者利用分库分表中间件(如 ShardingSphere 的逻辑)在应用层做路由。
    2. 当某个月的数据变冷时,直接通过 db.collection.renameCollection("order_202512", "order_202512_cold") 将其重命名,或直接使用 drop() 删除整个集合。
  • 优势: renamedrop 都是元数据级别的操作,瞬间完成,没有任何 Oplog 负担,且 drop立刻向操作系统释放磁盘空间
  • 劣势:数据库会出现一堆时间表,不好管理,且需经常频繁的建表。

使用原生的 Time Series 时间序列集合(针对 5.0+ 版本)

如果数据特征是机器指标、日志、事件流等持续写入且带时间戳的数据,强烈建议迁移到 MongoDB 原生的 Time Series 集合。

  • 做法: 建表时指定按时间字段分区,并直接设置 expireAfterSeconds
  • 优势: MongoDB 底层会自动对数据进行列式存储压缩,并且内部按照时间桶(Time Bucket)管理数据。过期清理时直接丢弃整个桶,效率极高,完全规避了逐行删除的性能瓶颈。
  • 劣势:生产mongo版本为4.4.29,如果使用此种方案,需要升级mongo,把原有数据迁移到新集合,改动太大。

兜底方案:定时任务代码搬运和删除。

如果目前受限于业务逻辑,只能采用最初的“查询 -> 插入 -> 删除”方案,请务必遵循以下平滑处理原则来写脚本:

  1. 强制分批与延时(休眠): 绝对不能一次性 deleteMany 几十万条数据。应该利用 _id 范围或时间索引进行分页,每次 bulkWrite 删除 500 - 1000 条,然后在代码中强制 sleep 几百毫秒。这叫“细水长流”,留出资源让 Oplog 同步和业务查询呼吸。
  2. 避免使用大偏移量的 Skip: 查冷数据时,基于上次查询的最大 _id 继续向后查(find({_id: {$gt: last_id}, status: "closed"}).limit(1000)),绝对不能用 .skip(100000),否则查询本身就会把库查挂。
  3. 定期执行 Compact: 搬运完毕后,在一个周末的低峰期,在 Secondary 节点上逐个执行 db.runCommand({ compact: "your_collection_name" }) 回收磁盘空间,最后再主备切换处理原来的 Primary。

故根据以上描述,建议使用定时任务微批处理数据。

疑问解答

大量删除会触发锁表或性能损失吗?

结论:不会“锁表”,但会带来严重的性能灾难。

MongoDB 默认使用 WiredTiger 存储引擎,采用的是文档级锁(行级锁),而不是表级锁。因此,删除操作本身不会阻塞整个集合的读写,但对于超大表的大规模删除,会带来以下致命的性能损失:

  1. Oplog 暴涨拖垮主从同步: MongoDB 是通过 Oplog 来维持高可用(HA)集群的主从同步的。你每删除一条文档,就会生成一条 Oplog 记录。海量删除会导致 Oplog 瞬间激增,挤占网络带宽,并极易导致 Secondary 节点同步延迟,甚至因 Oplog 覆盖而导致节点脱节。
    1. 解答:所以采用定时任务微批处理可以较好缓解此问题,而且正式集群是单节点。
  2. 极高的 CPU 和 I/O 消耗: 删除数据需要频繁更新 B-Tree 索引。百万或千万级别的删除会导致内存中的缓存页被大量换出,磁盘 I/O 飙升,直接影响线上正常查询请求的响应时间。
    1. 解答:所以采用定时任务微批处理可以较好缓解此问题。
  3. 磁盘空间不会自动释放(碎片化): 在 WiredTiger 引擎中,delete 操作只是将数据页标记为“空闲”,并不会立刻将磁盘空间还给操作系统,反而会留下大量碎片。要回收空间,后续还需要通过高耗时的 compact 命令进行碎片整理,而 compact 期间会阻塞所在节点的读写。
    1. 解答:删除操作确实并不会真的磁盘释放空间,只是在WiredTiger 引擎中标记为空闲,需要手动compact释放磁盘物理空间,但是被标记的。MongoDB 的 WiredTiger 存储引擎绝对不会在后台自动执行 compact 命令。compact 是一个高耗能、在旧版本甚至会锁集合的操作,必须由运维或开发人员手动发起。
    2. 如果不手动整理,磁盘空间会一直被无限制占用吗?在操作系统层面,是的;但在 MongoDB 内部,不是。这是很多人对 MongoDB 的一个误解。当你删除了历史停车流水数据后,操作系统看到的 mongod 磁盘占用确实完全没有变小。但是,这些被删除的数据所释放的空间,在 MongoDB 内部并没有消失,它们会被移入一个叫做 Free List(空闲列表) 的内部字典中。当产生新的数据时,MongoDB 会优先复用这部分 Free List 中的空闲空间,而不会去向操作系统申请新的磁盘切片。
    3. 结论: 只要每天删除的数据量与每天写入的数据量基本平衡,MongoDB 的磁盘占用就会维持在一个稳定的水位,不会无限膨胀。因此,不需要频繁甚至根本不需要去执行手动 compact
  4. 事务超时机制: MongoDB 的事务(Transaction)默认有 60 秒的生命周期限制(transactionLifetimeLimitSeconds)。超大表的批量搬运极易超时,导致不断回滚,永远无法完成任务。
    1. 解答:采用定时任务微批处理可以较好缓解此问题。
  5. 内存撑爆(OOM)风险: 事务在提交前,所有的修改(包括海量的 Delete 操作产生的 Oplog)都会驻留在内存中。大批量删除会瞬间吃光 WiredTiger 引擎的 Cache,导致数据库整体卡死。
    1. 解答:采用定时任务微批处理可以较好缓解此问题。
  6. 跨系统无法保证事务: 如果你的冷数据是迁移到另一个 MongoDB 集群、MySQL 或大数据平台,MongoDB 原生的事务根本无法跨越异构系统保证 ACID。
    1. 解答:采用定时任务微批处理可以较好缓解此问题,且是同步到当前MongoDB集群的另外一个集合。

原文地址: https://www.cveoy.top/t/topic/qHgZ 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录