
**作者**腾讯云cmongo内核团队-杨亚洲、尹超**关键词**MongoDB、WiredTiger、物理备份、空间回收、备份回档提速MongoDB 常见的备份方式有两种逻辑备份和物理备份。逻辑备份需要逐条遍历集合期间持续消耗 CPU、内存与业务侧的查询带宽TB 级实例往往要跑十几个小时对线上业务影响明显物理备份则直接以文件粒度拷贝 WiredTiger 的.wt数据文件IO 几乎是顺序大块读备份速度通常是逻辑备份的5~10倍以上对业务读写影响小。也正因如此物理备份成为大规模 MongoDB 集群的主流选择。在 MongoDB 的日常运维里物理备份长期以来都有一个隐蔽的副作用承接备份任务的 hidden 节点磁盘占用经常比主节点和普通从节点高出一大截。线上某核心大客户就是典型案例——主节点和普通从节点磁盘占用始终稳定在 1TB 出头而同副本集内承担备份任务的 hidden 节点磁盘却随着备份任务的累计运行悄然膨胀到了 1.5TB膨胀幅度高达50%且备份结束后这部分空间并不会自动回收只能依赖人工介入或重建节点。为了解决该大客户遇到的膨胀问题我们对 MongoDB 存储引擎做了深入分析对 MongoDB 内核物理备份做了深入优化最终形成了一套从 wiredtiger 内核到旁路服务的完整优化链路。结合线上真实业务场景测试验证该优化可以保证线上业务99%的 MongoDB 集群其物理备份膨胀率从之前极端的200%降到5%~10%以内多表场景整体膨胀率可以减少90%。这篇文章想把整个过程讲清楚——为什么会膨胀、我们做了什么、为什么这样做是安全的、以及在不同生产场景下实测的效果。一、背景MongoDB 物理备份的几个长期痛点MongoDB 的物理备份并不是把整个 dbpath 直接 cp 走那么简单。它的核心是 wiredTiger 提供的一个叫 backup cursor 的能力业务连上 mongod打开一个 backup: cursorWT 在那一刻冻结一份 checkpoint 视图给你一份这一刻所有数据文件的清单旁路备份服务拿着这份清单通过网络通路把每个xx.wt文件直接拷贝到 cos 对象存储。整个 cursor 在拷贝期间一直保持打开状态直到所有文件落到 COS 才会关闭。听起来挺合理。但只要在线上跑几个痛点会陆续浮现。**第一个也是最明显的——hidden 节点的磁盘占用膨胀。**backup cursor一旦在 hidden 上打开这个节点的所有.wt文件就开始集体膨胀业务写入越多膨胀越严重。更糟糕的是这部分膨胀出来的空间在 backup 结束之后并不会自动收回。**第二个单表膨胀时间随总数据量线性放大。**backup cursor 是“全或无”语义——cursor 不关全部表都被锁住cursor 一关所有表的状态一起恢复。这意味着即便某表已经拷贝完毕由于还需要拷贝其他表cursor 一直打开因此这个拷贝完成的表还是会持续膨胀。**第三个oplog 文件被无意义地拷贝。**膨胀率越高的集群一般其 update 等写入会较高oplog.wt 占比也会更高oplog 是 MongoDB 的复制日志物理备份恢复时根本用不上它——恢复完成后节点会重新从复制集拉最新的 oplog 追齐。但 backup cursor 默认会把 oplog 也列在文件清单里于是旁路服务老老实实地把它拷到 COS。oplog 是 capped 表不仅用户请求 update、delete 等会膨胀甚至 insert 也会膨胀所以 backup 期间它的膨胀很多时候是所有表里最严重的——典型生产环境这部分数据拷贝既慢又没有回档价值。**第四个膨胀导致备份回档时间越来越长。**膨胀不只是 hidden 节点磁盘账单这一环的事——最终影响旁路服务上传到 COS 的就是这份膨胀后的数据原本 1TB 的集群极端情况实际要上传 23TBCOS 存储费、跨地域复制流量费同比例上涨备份期间 hidden 节点到 COS 的上行带宽也成倍占用多个实例同时备份还会把出口带宽打满挤压其他业务的网络资源回档时同样要把这 23TB 重新从 COS 拉回本地、写盘、由 mongod 加载下载耗时、本地 IO、wiredtiger_open时间全部翻倍本来半小时能回档完的实例被拖到1-2小时以上。一次膨胀hidden 磁盘、COS 存储、网络带宽、回档时长四本账单一起被加价。总结一下MongoDB 的备份痛点其实是一个连环膨胀 → 磁盘成本 → 备份慢 → 膨胀更严重 → 恢复也慢每一环都在给下一环加压。我们的优化要解决的就是这个连环。二、优化收益2.1 测试场景分析腾讯云线上多个膨胀率较高的用户实例可以确认大部分实例为多个大表实例通过腾讯云副本集实例模拟多表用户场景测试写入4个表每个表 100G 左右然后启用备份并且备份期间各个表的读写流量如下文件稳态大小业务写入速率/s说明表1热表102 GB约 800 update800 read频繁update的业务主表典型中型OLTP场景的热点更新表2温表99 GB约 400 update800 read中等更新频率业务次要热表表3流水表101 GB约 1000 insert 1000 readappend-only insert例如订单流水/事件日志/IM消息这种典型的写入型业务表表4冷表98 GB100 read只读2.2 收益对比腾讯云 MongoDB 优化前后的性能对比总结如下:指标优化前**实测**优化后**实测**优化收益备份总耗时~85.1 min~44.7 min缩短44.4%hidden节点峰值占用850.9 GB524.3 GB节省326.6 GB字节数膨胀hidden相比主节点331.8 GB28.8 GB减少91.3%膨胀率严格意义63.9%5.8%减少91%COS实际上传量850.9 GB~524.3 GB节省~326.6 GB−38.4%回档要从COS拉回的数据量850.9 GB~524.3 GB少传~326.6 GB−38.4%2.3 优化总结从上表可以看出通过优化备份链路备份耗时、hidden 节点峰值占用、字节数膨胀、COS 实际上传量、备份期间 COS 带宽占用、回档要从 COS 拉回的数据量等指标都有明显改善。该优化最终收益可以总结如下用户集群存储成本减少38%物理备份膨胀率减少91%COS 存储成本减少38.4%备份回档效率提升44.4%COS 带宽节省节省38.4%三、膨胀原理为什么 backup 一开hidden 节点就开始涨要解决膨胀得先看清楚膨胀的物理过程。这一节会带读者走一遍 WiredTiger 在 backup 期间的内部状态变化把“为什么会膨胀”讲彻底。3.1 WiredTiger 的空间回收链路WT 的存储基本单位是 page每个 page 在落盘时会占用文件里一段连续的 extent可以理解成一段连续的字节区间。一个数据文件xxx.wt由很多 extent 拼起来extent 之间可能有空洞。WT 维护三个核心的 extent 列表理解了这三个列表就理解了整个空间回收逻辑列表含义谁能用它live.alloc本checkpoint周期内分配出去的extent当前checkpoint的活跃pagelive.avail当前可以被新写入复用的空闲extent新写入直接分配live.discard被历史checkpoint引用、暂时不能复用的extent等历史checkpoint被drop后进入下一轮ckpt_avail正常情况下一个 page 被覆盖写update或者删除deleteWT 的处理是这样的老 checkpoint 被 drop是空间回收的唯一触发器。mongo server 内核 checkpoint 默认每60秒触发一次每次 checkpoint 会把“过时”的老 checkpoint 从元数据里 drop 掉被 drop 的 checkpoint 所引用的所有 extent 才能进入下一轮的可复用池。这条链路在没有 backup 的情况下运转得非常顺畅业务一边写WT 一边把老 page 进 discard下一轮 checkpoint 把老 ckpt drop 掉extent 回到 avail 池新写入复用空闲位置——文件大小保持稳态。3.2 backup cursor 是如何打断这条链路的backup cursor 一打开WT 立刻做两件事来“冻住”快照第一件事把 backup start 设成最近一次 checkpoint 的时间戳。这个时间戳是后续所有“是否能 drop 老 checkpoint”判断的依据。第二个判断是膨胀的真正源头它的作用就是“backup期间所有 backup 开始之前就存在的 checkpoint 都不能被删因为旁路服务可能还在通过 backup 视图读这些 checkpoint 引用的数据”。正常情况下extent 可被持续复用如下图所示backup 期间extent 无法复用 → 文件持续膨胀如下图所示膨胀总结老 checkpoint 被 pin → 旧 extent 不能进入复用池 → 新写入只能往文件尾部 append。3.3 oplog.wt 是膨胀最严重的“双冠王”普通用户数据表的膨胀有上限由于普通数据表默认需要保持2个 checkpoint因此最坏情况下文件大小的理论上限大约为稳态的3倍一般受 update、delete 相关操作影响。Oplog 表膨胀上限但 oplog 完全不一样oplog 记录全节点所有写操作——业务写一次 oplog 也写一次。也就是说oplog 的写入速率可能是任何单张用户表的几十倍。更关键的是oplog是capped collectionOplogCapMaintainerThread会持续对 oplog 头部做 range truncate 来维持oplogSize配置上限——这个 B-tree 层的 range truncate 在 backup 期间照常进行给头部 record 打 tombstone、触发后续 reconcile 重写 page但被淘汰 page 占用的旧 extent 在 backup 期间始终卡在live.discard里、进不到live.avail新 oplog entry 找不到任何可复用空间只能持续向文件尾部 append。理解了“wt 文件 size 只增不减”这一点之后主从差异就清晰了——可以对比一下同一时刻、同一副本集内主节点 vs hidden 节点上的 oplog.wt两者业务负载完全一致都收到全量写入OplogCapMaintainerThread也都在正常运行、都在按相同节奏对 oplog 头部做 range truncateB-tree 逻辑 truncate。主节点上没有 backup cursor老 checkpoint 能正常 drop被淘汰 page 占用的旧 extent 通过live.discard → ckpt_avail → live.avail这条链路回到可复用池新 oplog insert 直接覆写文件中段那些被腾出来的空洞文件 size 因此长期稳定在oplogSize配置值附近——稳定的本质是“新写入填回老空洞”不是 WT 把文件截短了而 hidden 节点上 backup cursor 持有老 checkpoint整个老 checkpoint 引用的 extent 都被 pin 住旧 extent 卡在live.discard里出不来、永远到不了live.avail新 insert 找不到可复用空间只能向文件尾部 append于是出现了 oplog 逻辑大小恒定在oplogSize、物理文件 size 线性膨胀的分裂状态。因此oplog 表除了 update 会膨胀用户 insert 写入也会膨胀。更糟糕的是oplog 拷出去之后在物理恢复时根本用不上——如第一章第三个痛点所述oplog 是 MongoDB 的复制日志恢复完成后节点会重新从复制集拉最新的 oplog 追齐备份产物里的那份 oplog.wt 在回档时会被直接丢弃。所以oplog.wt 在很多时候是膨胀最严重 拷贝价值最低的“双冠王”。这是后面优化里会被特殊处理的对象。四、优化过程内核深度优化膨胀的物理原理梳理清楚之后优化方向就比较自然了让外部备份旁路服务告诉 WT “这张表我已经拷完了你可以恢复它的空间回收”。我们把这个能力做成了 WT 的一个新 C API叫WT_CONNECTION::backup_release_checkpoint。围绕这个 API内核侧做了配套改造。整个优化分成三大块。4.1 内核侧改造一精细化释放 checkpoint新增的 API 语义很简单旁路服务每拷完一张表就调一次releaseBackupCheckpoint(ident)通知 WT。WT 把这个 ident 加到一个内部 hash 表里。下一次内部 checkpoint 触发时做“是否能 drop checkpoint”判断时多查一次这个 hash 表——如果命中就跳过backup start的 pin 逻辑让这张表的老 checkpoint 进入正常的 drop 流程extent 进入ckpt_avail新写入复用——膨胀停止。光有 API 还不够旁路侧的拷贝顺序同样关键。一张表从__backup_start到被 release 之间的这段时间就是它的“膨胀窗口”——窗口越长文件被 append 出去的字节越多。所以最优策略是按预估膨胀率从高到低排序优先拷贝膨胀最快的表让它们的窗口尽量短膨胀慢的冷表/只读表放到最后反正它们就算晚 release 也涨不了多少。在我们的实测里4表场景1个 update 热表 1个 update 温表 1个 insert 流水表 1个只读冷表把这个调度策略加上之后hidden 节点磁盘峰值又能再降20%~30%。整个流程可以用伪代码描述如下端到端流程图以本测试用例为例下图把上述“4 张用户表 oplog”的实测场景串成一张时序图体现出“open backup cursor→立刻release oplog→按膨胀率降序拷表→每拷完一张就release一张→最后close cursor”的完整链路图中两个关键差异相对优化前第0步就把 oplog 这张膨胀最快的 capped 表从 backup snapshot 中摘掉避免它在整个备份窗口里一直累积 freed block。第1步每完成一张高膨胀率用户表的上传就立刻对该表调用一次 WT_SESSION::backup_release_checkpoint(uri)让该表的 freed block 在备份还没结束时就开始回流 free list —— 这是 hidden 节点峰值占用从 850.9GB 降到 524.3GB 的直接来源。4.2 内核侧改造二oplog 不备份并第一时间释放 ckpt接下来是 oplog.wt 这个“双冠王”的处理。前面已经论证过oplog.wt 在backup 期间膨胀最严重、拷贝价值最低。我们的处理是两步走。**第一步在 backup 一开始就立即 release oplog**备份方拿到 file_list 之后第一件事不是开始拷贝而是先识别出local.oplog.rs对应 ident通常是collection-X-oplogId.wt立即调用releaseBackupCheckpoint(oplog_ident)。WT 收到通知后下一次内部 checkpoint~60秒内就会把 oplog 的老 ckpt drop 掉oplog.wt 在整个 backup 期间保持稳态大小。**第二步备份流程跳过 oplog.wt 的拷贝**既然 release 之后 WT 可能在任何时候开始改写 oplog 的 extent我们就不能再去拷贝它了。但简单跳过会带来一个新问题MongoDB 的 catalog 元数据_mdb_catalog.wt和 WT 的 metadataWiredTiger.wt里都记录了local.oplog.rs这张集合恢复时如果元数据指向一个不存在的 wt 文件mongod 会启动失败。具体解决优化方法参考我们之前的分享: 《「腾讯云NoSQL」技术之MongoDB 篇:MongoDB 存储引擎备份性能70%提升内幕揭秘》备份端流程图如下(跳过 oplog.wt)恢复端流程图如下4.3 内核侧改造三crash recovery 路径切换前面还有一个潜在问题没处理——backup 期间 mongod 进程异常退出之后的恢复路径。内核逻辑是mongod 启动时检查 dbpath看到WiredTiger.backup元文件就认为这是个 backup 中间状态走 hot backup restore 路径从 backup 开始那一刻的 LSN 重放 WAL。如果 backup 已经持续了几小时这一段 WAL 可能有 几百GB重放下来要数十分钟级。目前内核没有给 mongod 留下“上次进程死亡时是否处于 backup 中”这个信息——WiredTiger.backup元文件本身只能表示“曾经开过 backup”无法区分“backup 还活着”和“backup 已经完成但元文件未清理”两种情况更没法和“crash 自愈”逻辑联动。我们的做法是在 mongod 进程内部引入一个 sentinel 标记文件backup.in_progress下文简称sentinel让进程自己负责打标和清理下次启动时由 mongod 启动路径自检该文件是否残留自动完成WiredTiger.backup的invalidate。整个机制全部内聚在mongod 内部运维侧零介入不需要外部 wrapper、systemd 钩子或人工脚本。sentinel 的三处时机如下五、注意事项备份过程中crash异常处理与解法还有一类隐患是 crash 后 mongod 自身能否正常启动backup 期间若有部分表已 release其老 ckpt 已被 drop、extent 已被新写入覆盖若此时crashdbpath 里WiredTiger.backup元文件仍然残留原生路径会据此走 hot backup restore试图按“backup开始那一刻的视图”打开这些表而它们的老 ckpt 在物理上已不复存在——轻则wiredtiger_open报元数据不一致重则读到错乱数据启动直接失败。也就是说release 优化本身在语义上已经让原生 hot backup restore 路径不再可用必须有配套的启动路径切换。上文介绍的4.3节的 sentinel 方案也把这条路径堵住了mongod 启动时若发现backup.in_progress残留会主动把WiredTiger.backup删除掉强制走 normal crash recovery——只依赖最新checkpoint WAL不再尝试还原任何已被 release 的视图正确性与启动速度一次性兜住。换句话说4.3节的优化不只是“启动快了10x”更是 release 机制能安全上线的必要前提。六、总结这次备份膨胀的优化本质上是把 WiredTiger 的 hot backup 从“全或无”语义升级到“表级粒度”把备份对生产环境的副作用降到最低。结合本文这套表级 release 优化 凌晨低峰期错峰备份的调度策略腾讯云MongoDB 线上99%的实例膨胀率可以从最极端场景下的200%收敛到5%~10%的稳定区间——这意味着 hidden 节点的磁盘 buffer 预留可以从原来的2x大幅压缩到1.1x以内单实例磁盘成本下降约60%TB 级大集群的备份窗口也不再需要为膨胀峰值留冗余。剩余不足1%的极端场景典型如备份期间超大写入压力 单表体量极大我们通过无损在线 compaction 进一步优化解决——在不阻塞业务读写、不影响主从同步的前提下回收备份遗留的碎片空间把这部分尾部实例也拉回到正常水位。该方案的技术细节与生产实践我们将在后续文章中专门分享。**客户价值**前文提到的线上客户多表核心集群在启用本优化后集群在数月间进行了上百次全量物理备份Hidden 节点磁盘占用空间始终保持平稳未再因备份导致的显著空间膨胀、需人工介入清理的情况。整体 Hidden 节点的磁盘空间膨胀率由约50%下降至5%单节点磁盘空间节省约500GB。