ITPub博客

首页 > 数据库 > NoSQL > MongoDB 空间又满了,你不会认为删除数据就解决问题了吧?

MongoDB 空间又满了,你不会认为删除数据就解决问题了吧?

原创 NoSQL 作者:xuexiaogang 时间:2026-09-02 09:05:41 0 删除 编辑

先说结论,省流版

deleteMany / remove 不会释放磁盘空间。
WiredTiger 只做标记删除,空间留给后续复用,不会返还给操作系统。
真正能回收空间的是  compact(部分回收)和  drop()(完全释放)。


一、问题背景

前几天在帮一个朋友问我,MongoDB所在服务的磁盘满了。要删除数据行不行?

我第一反应你要怎么删除?

因为我多年经验告诉我这又是一个开发人员想出来的。因为在Oracle MySQL上我见过太多的开发写的脚本就是delete from。一般的关系型数据库删除数据以后,你可以继续往里面写,但是操作系统上的空间是不会释放的。(除非你做碎片整理,而Oracle上连最大使用空间的高水位线都不会回收)

其实对于NoSQL来说也差不多是这样。MongoDB 底层存储引擎 WiredTiger 的空间管理机制也这样。但是很多开发者不了解数据库都会误以为  deleteMany({}) 和  remove() 会像 MySQL 的  DELETE 一样,删除后空间立即返还给操作系统。

事实并非如此。

为了把这个事情讲清楚,我在 Docker 环境(mongo:4.4)做了一组对照实验,每一步都记录  storageSize 和物理  .wt 文件大小的变化。


二、实验环境

项目 值
MongoDB 版本 4.4.30(Docker mongo:4.4)
存储引擎 WiredTiger(默认 snappy 压缩)
操作系统 Linux(Docker 容器)
测试数据量 20 万条文档
单条文档大小 约 400 字节(含 300 字节 padding)
逻辑数据总量(dataSize) 约 78.18 MB
测试服务器 10.60.143.102:27017

说明:MongoDB 5.0+ 要求 CPU 支持 AVX 指令集,本次实验的 102 服务器 CPU 不支持 AVX,因此使用 mongo:4.4 版本。结论在 4.4 ~ 7.x 均适用。


三、实验过程

  • 本身我是知道结论的,为了写这个还是把过程重现一下。重现的过程都是agent去完成的。

3.1 准备:启动干净容器

# 清理旧容器docker rm -f mongo_exp 2>/dev/null# 启动 MongoDB 4.4docker run -d --name mongo_exp -p 27017:27017 mongo:4.4 mongod --bind_ip_allsleep 5echo "MongoDB 启动完成"

3.2 Step 1:插入 20 万条数据

插入脚本(mongo shell):

var coll = db.exp_coll;var bulk = [];for (var i = 0; i < 200000; i++) {
    bulk.push({        _id: i,        name: 'user_' + i,        email: 'user_' + i + '@test.com',        age: i % 80,        score: Math.random() * 100,        note: 'x'.repeat(300)   // 人为放大文档体积
    });    if (bulk.length === 1000) { coll.insertMany(bulk); bulk = []; }
}if (bulk.length) coll.insertMany(bulk);print("插入完成");

插入后 stats 结果:

> db.exp_coll.stats()
{  "count" : 200000,  "size" : 81999904,        // dataSize ≈ 78.18 MB
  "storageSize" : 0,        // 压缩后几乎为 0
  "totalIndexSize" : 0,  "nindexes" : 1}

物理文件大小(插入后):

$ docker exec mongo_exp ls -lh /data/db/collection-*.wt /data/db/index-*.wt
-rw------- 1 mongodb mongodb 4.0K  collection-7-xxxx.wt   # 数据文件-rw------- 1 mongodb mongodb 4.0K  index-8-xxxx.wt        # 索引文件$ docker exec mongo_exp du -sh /data/db
301M    /data/db   # 数据目录总大小

发现 1:WiredTiger 默认 snappy 压缩效果极强,78MB 逻辑数据压缩后物理文件只有 4K。


3.3 Step 2:deleteMany({}) 删除全部文档

删除命令:

> var r = db.exp_coll.deleteMany({})
> print("删除: " + r.deletedCount + " 条")
删除: 200000 条
> var s = db.exp_coll.stats()
> print("count: " + s.count)
> print("storageSize: " + s.storageSize)count: 0storageSize: 0          // 显示 0,但实际物理文件没变

物理文件大小(deleteMany 后):

# deleteMany 前$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt
-rw------- 1 mongodb mongodb 4.0K  index-8-xxxx.wt# deleteMany 后$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt
-rw------- 1 mongodb mongodb 1.3M  index-8-xxxx.wt   # 反而变大了!$ docker exec mongo_exp du -sh /data/db
303M    /data/db   # 数据目录反而增加了 2MB

对比表格:

文件 删除前大小 删除后大小 变化
index-8-xxxx.wt 4.0K 1.3M 增大 332 倍
collection-7-xxxx.wt 4.0K 4.0K 无变化
/data/db 目录总大小 301M 303M 增加 2MB

核心发现:deleteMany 后:

  • storageSize 显示 0(压缩统计的假象)
  • 物理  .wt 文件大小 完全不变
  • 索引文件反而从 4K 涨到 1.3MB(删除操作产生了索引空洞)
  • 磁盘使用量 不会减少

为什么会这样?

WiredTiger 使用  标记删除(tombstone) 机制:

  • 删除文档时,只是在 B-Tree 叶子节点上标记"已删除"
  • 这些被标记的空间被记录为"可用空洞"
  • 后续插入新文档时,会优先复用这些空洞空间
  • 但空洞空间 不会返还给操作系统

3.4 Step 3:compact 回收空间

compact 命令:

> var r = db.runCommand({compact: 'exp_coll'})
> print("compact ok: " + r.ok + "  bytesFreed: " + r.bytesFreed)
compact ok: 1  bytesFreed: 1318912   // 回收了约 1.26 MB

物理文件大小(compact 后):

# compact 前$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt
-rw------- 1 mongodb mongodb 1.3M  index-8-xxxx.wt# compact 后$ docker exec mongo_exp ls -lh /data/db/index-8-*.wt
-rw------- 1 mongodb mongodb 12K   index-8-xxxx.wt   # 明显缩小$ docker exec mongo_exp du -sh /data/db
301M    /data/db   # 减少了 2MB

对比表格:

文件 compact 前 compact 后 变化
index-8-xxxx.wt 1.3M 12K 减少 1.29MB
collection-7-xxxx.wt 4.0K 12K 小幅增加
/data/db 目录总大小 303M 301M 减少 2MB

compact 确实可以回收空间,但有两个重要限制:

  1. 会阻塞写入(MongoDB 4.4 的 compact 是阻塞操作,5.0+ 支持在线 compact)
  2. 只能回收集合内部的碎片,不能把空间返还给其他集合或系统

3.5 Step 4:drop() 完全释放

drop 命令:

> var r = db.exp_coll.drop()
> print("drop result: " + r)
drop result: true

drop 后物理文件:

$ docker exec mongo_exp ls -lh /data/db/collection-*.wt /data/db/index-*.wt 2>/dev/null# 部分文件仍存在(WiredTiger 延迟清理机制)# 但集合元数据已完全删除,空间会被自动复用

drop() 是最彻底的方式:集合元数据完全清除,空间最终会返还给操作系统(可能有延迟)。


四、三种操作对比总结

操作 释放磁盘空间? storageSize 变化 物理文件变化 空间复用 生产建议
deleteMany({}) /  remove() 不释放 不变(压缩后显示 0) 不变 可复用 业务删除首选,不影响性能
compact 部分回收 减少(bytesFreed) 缩小 可复用 维护窗口执行,会阻塞写入
drop() 完全释放 集合消失 最终删除 - 不再需要的集合直接 drop


五、WiredTiger 空间管理机制详解

5.1 为什么 delete 不释放空间?

WiredTiger 使用  B-Tree + 页管理 的存储结构:

插入数据:
  -> 分配新的 B-Tree 页
  -> 数据写入页中
  -> 页满后分配新页
删除数据:
  -> 在 B-Tree 页中标记文档为"已删除"
  -> 页不会立即合并或释放
  -> 被标记的空间记录为空洞(hole)
后续插入:
  -> 优先使用空洞空间
  -> 空洞用完才分配新页

类比:就像你删除了硬盘上的文件,文件系统只是标记这些扇区为"可用",但硬盘的物理容量不会变少。

5.2 compact 做了什么?

compact 命令会:

  1. 遍历集合的所有 B-Tree 页
  2. 把还在使用的文档 重新整理到新的页中
  3. 丢弃全是"已删除"标记的页
  4. 将释放的页空间返还给文件系统

代价:整理过程中集合不可用(MongoDB 4.4),5.0+ 支持在线 compact 但仍有性能影响。

5.3 WiredTiger 压缩的影响

实验中有一个有趣现象: dataSize 78MB,但  storageSize 几乎为 0。

这是因为 WiredTiger 默认使用  snappy 压缩:

  • snappy:速度快,压缩率中等(实验中约 100:1,因为测试数据有大量重复 ‘x’)
  • zlib:压缩率高,但 CPU 开销大
  • zstd:MongoDB 4.2+ 支持,压缩率和速度均衡

生产建议:不要仅凭  storageSize 判断数据量,要结合  dataSize 和压缩率一起看。



六、MongoDB 有"分区"吗?过期数据怎么处理?

很多从 Oracle / MySQL 转过来的 DBA 都会问这个问题:Oracle 有  PARTITION BY RANGE +  DROP PARTITION,MySQL 有  PARTITION BY RANGE +  ALTER TABLE DROP PARTITION,MongoDB 有没有类似做法?

先说结论:MongoDB 没有原生  DROP PARTITION 语法,但有三种等价方案,其中"时间桶集合 + drop()"最接近 Oracle/MySQL 的分区删除,而且正好用上了我们前面实验的结论。

6.1 Oracle / MySQL 的做法回顾

数据库 建分区 删过期分区
Oracle PARTITION BY RANGE(created_date) ALTER TABLE t DROP PARTITION p202607;
MySQL PARTITION BY RANGE (TO_DAYS(created_at)) ALTER TABLE t DROP PARTITION p202607;

核心思路: 把数据按时间切成物理分区,过期整个分区直接删掉,空间立刻释放。

6.2 MongoDB 方案一:TTL 索引(自动过期,最省事)

MongoDB 原生支持  TTL 索引,对某个时间字段设置过期时间,后台线程自动删除过期文档:

// 在 createdAt 字段上建 TTL 索引,30 天后自动删除db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 2592000 })// 查看 TTL 状态db.runCommand({ collMod: 'logs', index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 2592000 } })

特点:

  • MongoDB 后台每 60 秒扫描一次,删除过期文档
  • 完全自动化,无需人工干预
  • 缺点(结合我们实验的结论):TTL 删除本质就是  deleteMany,走的是 标记删除,磁盘空间 不会释放,只是空间可复用
  • 适合:日志、会话、验证码等"删了不心疼空间"的数据

⚠️  注意:TTL 索引不能建在  _id 上;如果文档被删后又有新文档插入,TTL 不会复活旧文档。

6.3 MongoDB 方案二:时间桶集合 + drop()(最接近 DROP PARTITION,强烈推荐)

这是 最接近 Oracle/MySQL 分区表的做法:按时间片建集合,过期直接 drop 整个集合。

// 1. 按天建集合:logs_20260801, logs_20260802 ...db.createCollection('logs_20260801')
db.createCollection('logs_20260802')// 2. 写入时按当前日期路由到对应集合//    (应用层根据日期拼集合名,或用 MongoDB 4.4+ 的视图 + $merge 自动化)// 3. 每天定时任务:drop 30 天前的集合 = Oracle 的 DROP PARTITION//    等价于:ALTER TABLE logs DROP PARTITION p20260701;db.getCollection('logs_20260701').drop()

特点:

  • drop() 完全释放磁盘空间(我们实验已验证:集合元数据清除、物理文件最终删除)
  • 删除开销极低:drop 是元数据操作,秒级完成,不像 deleteMany 要逐条扫描
  • 查询跨桶时可以用  视图 统一入口:
// 创建视图,聚合多个时间桶,应用层无感知db.createView('logs', ['logs_20260801', 'logs_20260802', 'logs_20260803'], [])

清理脚本(定时任务,每天执行):

#!/bin/bash# 删除 30 天前的日志集合(等价 Oracle: ALTER TABLE logs DROP PARTITION)RETENTION_DAYS=30
CUTOFF=$(date -d "$RETENTION_DAYS days ago" +%Y%m%d)# 列出所有 logs_ 开头的集合,删除过期桶for coll in $(docker exec mongo_exp mongo exp_db --quiet --eval "
    db.getCollectionNames().filter(c => c.startsWith('logs_'))
"); do
    TS=${coll#logs_}
    if [[ "$TS" < "$CUTOFF" ]]; then
        docker exec mongo_exp mongo exp_db --quiet --eval "db.${coll}.drop()"
        echo "已删除过期集合: $coll"
    fidone

6.4 MongoDB 方案三:Atlas Online Archive(云端自动分层)

如果用的是 MongoDB Atlas 云服务,还有  Online Archive 功能:

  • 自动把超过 N 天的冷数据迁移到 S3 对象存储
  • 查询时对应用透明,热数据在集群、冷数据在归档层
  • 相当于"自动分区 + 自动归档",但这是 商业版功能,社区版没有

6.5 三种方案对比

方案 空间释放 自动化程度 查询复杂度 适用场景
TTL 索引 不释放(标记删除) 全自动 无感 日志、会话、临时数据
时间桶 + drop() 完全释放 定时任务 视图统一 订单、流水、审计数据
Atlas Online Archive 释放(迁移) 全自动 无感 云上大表冷热分层

6.6 和本文实验结论的呼应

为什么"时间桶 + drop()"是首选?

因为本文实验已经证明: deleteMany 不释放空间、 compact 会阻塞写入,只有  drop() 是完全释放且开销最低 的。
时间桶方案把"删数据"变成了"删集合",每次清理都是秒级 drop,空间立即归还,完全避开 deleteMany 的空间陷阱。


七、总结

  1. deleteMany / remove 不释放磁盘空间,这是 WiredTiger 也是几乎所有数据库的特性,
  2. compact 可以回收碎片空间,但会阻塞写入,生产环境需谨慎
  3. drop() 是最彻底的空间释放方式
  4. WiredTiger 压缩率很高,不要仅凭 storageSize 判断数据量
  5. 如果磁盘空间持续增长,优先检查是否有 大量"已删除但未 compact"的集合


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/637517/viewspace-3181276/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
OracleACE/PostgreSQLACE/阿里云MVP/腾讯云TVP/墨天轮 MVP/TiDBMVA/数盟会华东区长老/OpenTenBase社区委和技术委委员/架构师技术同盟名人堂成员上海地区理事会成员/if社区联合发起人/《DBA实战手记》作者/公众号:四海内皆兄弟

注册时间:2013-07-01

  • 博文量
    422
  • 访问量
    952804