尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux下MongoDB集合定时清空:方案选型与cron落地指南

Linux下MongoDB集合定时清空:方案选型与cron落地指南 聊一个运维场景里特别高频的需求Linux 服务器上的 MongoDB某个集合的数据需要定时清空。日志采集集合、临时结果集合、测试环境的中间表甚至某些脱敏前的缓存库都属于典型的“只写不读、占着磁盘不干活”的数据。放着不管迟早把磁盘撑爆然后所有业务一起遭殃。我前阵子刚处理过一个类似场景从清空方式选型到 cron 配置再到排查兜底整个链路都捋了一遍。这篇就把 Linux 定时清空 MongoDB 集合的完整操作说明整理出来顺便把每个关键步骤背后的“为什么”也讲明白。先说清楚一件事MongoDB 里的“集合”Collection对应关系型数据库里的“表”里面是一篇篇 JSON 形态的文档Document跟 Java 里的 Collection、概率论里的集合完全是两码事。这套操作其实适合三类人看刚接手 MongoDB 运维的新手、需要给测试环境做数据重置的开发同学、还有那些要定期清理日志集合的 SRE。无论你用 CentOS、Ubuntu 还是其他基于 systemd 的发行版下面的方案都能直接照着做。1. 需求场景与方案选型1.1 这个需求最典型的三种场景我接触到的定时清空需求基本脱不开下面三类。第一类是日志/监控类集合。比如业务服务每秒钟写入访问日志到 MongoDB保留两周就够了更早的数据没有任何价值。这种需求往往要求“清空一部分”而不是“清空全部”但简化版本就是定期把旧数据全干掉。第二类是临时存储或者缓存类集合。比如大数据任务的中间计算结果、用户批量导入的临时表每周跑完一轮就该清一次不然磁盘水位只升不降。第三类是测试环境。开发同学希望每天早上有个干净的数据环境这时候需要定时重置集合让自动化测试跑起来更稳定。这三类场景对“清空”的定义其实是不一样的有的只要求删除集合里的全部文档但集合本身、索引、校验规则都要保留有的则完全不在乎连集合一起删掉重建都行。所以第一步不是急着写命令而是先想清楚你这个集合过后还要不要继续用、索引能不能重建。这决定了你选哪种清空方案。1.2 清空方案的选型对比deleteMany 与 drop我见过不少人上来就是db.collection.deleteMany({})因为教程里最常出现的就是这句。但实际落地的时候deleteMany 还真不一定是最优解。先把四个常用方案放在一起对比方案做什么保留什么性能特点推荐场景deleteMany({})删除集合内全部文档保留集合本身和全部索引全表扫描加逐条删除量大时压力明显集合需要继续使用索引重建成本高drop()直接删除整个集合什么都不保留索引、校验规则一起没极快走文件回收路径集合可重建索引可在脚本里重建TTL 索引文档到达指定时间自动过期删除保留集合和索引后台持续清理约 60 秒触发一次日志类、有时效性的数据capped collection环形集合达到上限自动覆盖最旧文档保留集合结构插入顺序固定自动覆盖无需额外任务固定容量窗口的日志队列大多数运维场景下drop()其实比deleteMany({})划算得多。原因很简单deleteMany 是逐条删除文档每一笔删除都要走一遍存储引擎的写路径几十万条文档删下来请求耗时和锁竞争都会给实例造成压力而 drop 是把整个集合的文件直接释放掉速度能差两个数量级。代价是集合上的索引、validator、collation 设置全都没了后续写入前必须重新建索引。反过来也一样。如果目标集合承载着在线业务只是需要清空数据后立刻复用比如任务跑完后中间表马上要接收新数据这时候重新建索引会有窗口期线上请求容易受影响那老老实实用deleteMany({})分批删反而更稳。我个人的选型习惯是完全没用的日志集合直接写db.getCollection(xxx).drop()还要继续写的业务集合用分批删除绝不裸跑一条大 deleteMany数据有时效性、能接受延迟清理的优先 TTL 索引因为连定时任务都省了。1.3 选型前必须确认的另外两件事方案确定了动手之前还有两件事要确认很多人就是在这一步翻的车。第一件事目标集合到底有多大。如果只有几百条、几千条deleteMany 和 drop 差别不大要是几十 GB 甚至上百 GBdeleteMany 会把实例拖到慢查询报警drop 则瞬间释放空间。建议先跑一下db.getCollection(xxx).stats().size看看集合物理大小再决定。第二件事清空之后磁盘空间能不能还给操作系统。这里有个容易误解的点deleteMany 删完文档之后MongoDB 底层用的是 WiredTiger 存储引擎被删除的空间通常会标记为空闲复用块但数据文件大小不会立刻变小操作系统层面看磁盘占用还是那么多。只有 drop 集合或者对集合做 compact才能真正把文件空间缩回来。如果你的目的是“腾磁盘空间”那基本只有 drop 或 TTL 水位管理能真正解决问题。2. 环境准备与基础操作2.1 MongoDB 安装与服务确认如果你服务器上还没装 MongoDB先把环境准备好。以 Ubuntu 和 CentOS/Rocky 为例网上有一堆安装教程这里就不重复贴包安装的完整步骤了但有几个点容易被新手忽略。第一每个大版本对应的软件源地址不一样不要直接抄老博客里的源配置。第二装完之后先确认 mongod 进程真的拉起来了systemctl status mongod看状态或者ps -ef | grep mongod。第三命令行客户端的名字——新版本里是mongosh不再是老的mongo很多教程没更新抄下来直接command not found。我实际踩过这个坑照着旧教程写了脚本挂到 crontab结果一直报mongo: command not found查了半天才发现机器上装的是新版 MongoDBshell 工具换成了 mongosh。所以配置脚本之前先跑一下mongosh --version确认客户端存在顺便确认它的绝对路径后面定时任务里会用到。2.2 连接实例与检查集合连接 MongoDB 的命令基本是mongosh mongodb://127.0.0.1:27017/yourdb --username youruser --password yourpass --authenticationDatabase admin这里专门提一下--authenticationDatabase admin意思是用户名是在 admin 库里认证的。很多人把用户名建在业务库认证库却写成 admin会导致Authentication failed。认证库到底是什么取决于你创建用户时候选的库别凭感觉填。连接上之后先确认目标集合show collections // 或者 db.getCollection(your_collection).countDocuments() // 查看占用空间 db.getCollection(your_collection).stats().size注意老版本里常用的count()在新版 MongoDB 中已经废弃请使用countDocuments()。如果你在脚本里拿不同的 db 实例比如当前已经在 testdb 下却想操作 logdb 里的集合用db.getSiblingDB(logdb)切换跟在 mongo shell 里use logdb是同一个效果但脚本里更清晰。另外提一句如果目标是分片集群清空操作要格外小心。drop 一个正在分片的集合会涉及分片元数据的清理时间可能比预期长很多deleteMany 批量删除也会在多个 shard 上产生大量操作。这些操作在分片集群上的行为比单机复杂建议先在测试分片环境验证过再上生产。2.3 写脚本前的统一约定写清理脚本的时候我建议统一用 js 文件加 mongosh--file参数来执行而不是在 crontab 里硬拼一长串--eval。原因很简单--eval里既要处理引号转义又要处理换行一旦逻辑复杂一点就非常容易出错排查起来脑子都大。比如你写一个clean_tmp.jsconst targetDb db.getSiblingDB(mydb); const targetCollection tmp_data; const startTime new Date(); print([clean-start], startTime.toISOString()); if (targetDb.getCollectionNames().includes(targetCollection)) { targetDb.getCollection(targetCollection).drop(); print([clean-done] dropped collection:, targetCollection); } else { print([clean-skip] collection not exists:, targetCollection); }然后执行mongosh mongodb://127.0.0.1:27017/admin --quiet --file /usr/local/bin/clean_tmp.js这样脚本逻辑清晰方便调试也方便后续加日志、加判断。getCollectionNames()返回数组先判断再 drop能避免集合本身不存在时产生的无用报错信息虽然不影响执行但日志里那些红字很干扰排查。3. 核心实现三种可行的清空方案3.1 直接 drop速度最快但要考虑索引重建如果确定这个集合删掉之后可以重建那drop()是最干净利落的方案。MongoDB 的 drop 操作在单机副本集上的速度非常快几 GB 的集合也能在秒级响应原因是它直接释放底层文件并清理元数据。但注意一个细节drop 之后原来集合上的索引全部没了代码里如果还在对某个字段做 unique 约束或排序查询性能会骤降甚至报错。所以生产环境的清理脚本一般会写成 drop 之后立刻重新建索引const targetDb db.getSiblingDB(mydb); const colName tmp_log; targetDb.getCollection(colName).drop(); // 重新创建集合并建立索引 targetDb.createCollection(colName); targetDb.getCollection(colName).createIndex({ created_at: 1 }, { expireAfterSeconds: 86400 * 7 });这样既拿到了 drop 的释放速度又保证业务侧下一次写入前索引已经就绪。如果不想删集合只想删完所有文档那用 deleteMany 或分批删除。3.2 deleteMany保留集合结构和索引的稳妥路径当集合还需要继续承载业务读写不能删了重建时就需要用 deleteMany。小集合直接跑db.getCollection(tmp_collection).deleteMany({});这种写法简单粗暴但一旦集合量级上来了一条命令把几十万甚至几百万文档全删掉巨大的写压力一样让 mongod 够呛。比如你测试环境每天要清一个百万级文档集合deleteMany 执行期间 CPU 和锁等待都会明显升高甚至影响同实例上其他集合的读写。更稳妥的写法是分批删除。基本思路是先拿出一批文档的_id然后按这批_id删除循环到删空为止function batchDelete(collectionName, batchSize) { const col db.getCollection(collectionName); let total 0; while (true) { const batch col.find({}, { _id: 1 }).limit(batchSize || 1000).toArray(); if (batch.length 0) break; const ids batch.map(d d._id); const result col.deleteMany({ _id: { $in: ids } }); total result.deletedCount; if (result.deletedCount batch.length) break; sleep(100); } print([batchDelete] total deleted:, total); } batchDelete(tmp_collection, 2000);用_id作为游标是因为 MongoDB 默认给你建好了_id唯一索引按它筛选是走索引的效率高。中间加一个sleep(100)控制节奏是防止循环删除过程中 Mongo 实例的 CPU 被一口气打满。这个函数在几百万文档的集合上跑过稳定性和对实例的影响都比一条裸 deleteMany 好很多。还有一个容易被忽略的问题就是删除过程中业务还在写入怎么办。如果一边清空一边写入清完之后很快就会产生新数据看起来集合永远删不干净。这种场景最好跟业务方约定一个维护窗口或者至少说明这个清理动作本身只负责处理存量数据。3.3 TTL 索引让数据自己到期自动消失如果集合结构不允许你每天手动清库业务又希望数据保留一段时间之后自动删除那 TTL 索引是最优雅的方案。原理很简单给集合里的某个日期字段建一个 TTL 索引MongoDB 后台会周期性扫描这个字段发现当前时间减去字段值超过expireAfterSeconds后自动删除对应文档。举个例子日志集合里每个文档有个ts字段希望保留 7 天那就建这样一条索引db.getCollection(access_log).createIndex({ ts: 1 }, { expireAfterSeconds: 7 * 24 * 3600 });MongoDB 的 TTL 清理线程大约每 60 秒跑一次所以删除不是实时的会有一定的延迟但对日志类场景来说完全够用。注意几点TTL 索引字段必须是单个日期字段或者包含日期的数组字段不能拿多个字段组合条件判断过期。如果集合里大量数据同时过期TTL 线程删除时一样会产生写压力建议评估一下业务低峰时段是否允许这种批量删除。TTL 索引建在非日期字段上不会生效建好后可以用db.collection.getIndexes()检查确认类型是expireAfterSeconds。删除任务占用的线程是 MongoDB 内部的不依赖外部 cron停机维护时要留意这个自动清理线程的行为。如果你的场景是“数据有时效性过期后自动清理”TTL 索引其实比外部 crontab 更省心。我之前维护过一个访问日志库集合按天滚动配合 TTL 索引后磁盘水位长期稳定在 80% 以下不需要额外定时任务干预。3.4 可选的隐藏方案capped collection除了上面三种方案还有一种比较少见的做法把集合建成 capped collection也就是固定大小的环形集合。插入新文档时如果集合容量已满MongoDB 会自动覆盖最旧的文档相当于天然实现了滚动清理。创建方式db.createCollection(server_log, { capped: true, size: 1073741824, max: 500000 });这个方案适合容量固定、只需保留最近一段时间的场景而且完全不需要 crontab 参与靠集合自身的“头尾覆盖”机制就能搞定。缺点也很明显不允许删除单条文档、更新文档时不能超过原始大小、集合容量到了上限之后只会覆盖旧数据不会增长。所以生产环境如果不是特别适合这个模型不建议硬套。4. 定时任务crontab 与 systemd timer 两种落地方式4.1 先把清理脚本包装成可执行的 shell不管用 cron 还是 systemd timer最终入口都是一个可执行的 shell 脚本。建议脚本写成这样#!/bin/bash LOG_FILE/var/log/mongo_clean/clean.log mkdir -p /var/log/mongo_clean echo $(date %Y-%m-%d %H:%M:%S) clean start $LOG_FILE MONGO_SH/usr/bin/mongosh MONGODB_URLmongodb://127.0.0.1:27017/admin $MONGO_SH $MONGODB_URL --quiet --file /usr/local/bin/clean_tmp.js $LOG_FILE 21 echo $(date %Y-%m-%d %H:%M:%S) clean end $LOG_FILE这里有三个关键点mongosh 要用绝对路径。cron 环境变量 PATH 默认非常基础未必包含/usr/bin更未必包含你自己安装的 mongosh 目录。不用绝对路径定时触发时极大概率报command not found。日志目录自己要提前建好脚本里mkdir -p就是为了避免第一次运行因为目录不存在导致写日志失败。错误也要写进日志21把标准错误重定向到同一个文件方便排错。写完之后别急着挂定时任务先手动执行一遍chmod x /usr/local/bin/clean_tmp.sh bash /usr/local/bin/clean_tmp.sh cat /var/log/mongo_clean/clean.log如果手动执行正常再进下一步。这个习惯能帮你避开一大半 cron 不执行的问题。4.2 crontab 配置与常见误区crontab 配置本身不难crontab -e然后加一行0 2 * * * /usr/local/bin/clean_tmp.sh表示每天凌晨 2 点执行。五个字段依次是分钟、小时、日、月、星期0 2 * * *就是每天 2:0030 3 * * 1是每周一 3:30*/10 * * * *是每 10 分钟。具体值班表格式网上一搜就有挂定时任务之前确认好自己不紧张就没什么问题。几个特别容易踩的坑cron 运行时的用户身份。你用crontab -e添加的是当前用户的定时任务如果你用 root 身份添加脚本就以 root 执行用普通用户添加脚本就以该用户执行。如果脚本里连接 MongoDB 需要认证注意用户身份会不会影响读取凭据文件或者环境变量。时区问题。cron 用的是系统时区不是中国时区的服务器凌晨 2 点的概念对不上。可以用timedatectl set-timezone Asia/Shanghai调整系统时区然后date确认。cron 服务本身有没有在跑。systemctl status crond或service cron status不同发行版服务名不一样CentOS 上叫 crondUbuntu 上叫 cron。环境变量缺失。除了 PATHHOME、LANG、TZ这类在交互 shell 里正常的环境变量在 cron 里都可能为空。如果脚本里依赖了HOME/root之类的路径要么在脚本开头显式 export要么都写成绝对路径。加上清理前先记录日志、清理后检查文档数的习惯定时任务的可靠性会高很多。4.3 systemd timer更可控的现代化方案如果你所在的服务器用的是 systemd现在主流发行版基本都是可以考虑用 systemd timer 替代 cron。优势是日志管理统一走 journald还能设置“如果机器当时关机开机后补执行”这种策略。首先写一个 service 单元文件比如/etc/systemd/system/mongo-clean.service[Unit] DescriptionClean MongoDB tmp collection [Service] Typeoneshot ExecStart/usr/local/bin/clean_tmp.sh然后写配套的 timer 文件/etc/systemd/system/mongo-clean.timer[Unit] DescriptionTimer for MongoDB clean task [Timer] OnCalendar*-*-* 02:00:00 Persistenttrue [Install] WantedBytimers.target这里OnCalendar*-*-* 02:00:00表示每天凌晨 2 点Persistenttrue表示如果错过了计划时间比如机器在 2 点关机系统启动后会补执行错过的任务。启用和查看systemctl daemon-reload systemctl enable --now mongo-clean.timer systemctl status mongo-clean.timer systemctl list-timers | grep mongo-cleanjournal 日志直接看journalctl -u mongo-clean.service -n 50相比 cronsystemd timer 对时间表达式的要求更严格但错误提示也更清晰。我个人偏好用 systemd timer 管理需要持久化日志的清理任务用 cron 管那些简单的一次性触发两者并不冲突。5. 常见问题与排查实录5.1 定时任务没执行第一步先看什么挂完定时任务第二天发现没清先别急着怀疑 MongoDB大多数问题出在环境层面。按下面的顺序排查手动执行脚本看能不能跑通。手动能跑通说明脚本本身没问题问题出在调度或环境。看 cron 日志。CentOS/RHEL 系在/var/log/cronUbuntu/Debian 系通常混在/var/log/syslog里用grep CRON /var/log/syslog能看到调度记录。确认定时任务有没有真的写入。crontab -l输出当前用户任务列表看看那一行在不在。确认系统时间对不对。cron 按系统时间触发服务器时区错了凌晨 2 点的概念就完全错位。手动执行报command not found吗如果是在脚本里显式指定 mongosh 的绝对路径。这些都是我实际排查中遇到频率最高的点。有一次帮同事定位脚本写的好好的crontab 也加上了就是不动最后发现 cron 服务被安全策略停掉了systemctl start crond起来之后马上恢复正常。5.2 认证、连接与磁盘问题的速查表清空任务执行过程中报错也很常见我把典型的几种整理成一张速查表症状可能原因解决办法MongoNetworkError: connect ECONNREFUSEDmongod 没启动或监听地址不是 127.0.0.1systemctl start mongod检查net.bindIp配置Authentication failed认证库不对或密码错误确认创建用户时选的认证库用--authenticationDatabase admin重试command not foundcron 环境 PATH 缺失mongosh 用绝对路径或脚本开头 export PATHPermission denied脚本没有执行权限chmod x /usr/local/bin/clean_tmp.sh清理完磁盘空间没变deleteMany 不释放文件大小用 drop 或 compact 回收物理空间TTL 索引不生效字段不是日期类型或者 expireAfterSeconds 是负数检查字段类型getIndexes()查看索引定义集合删除后业务写入报错drop 后集合不存在连接还没重建与业务约定维护窗口清理后立即重建集合和索引第 5 行值得多说一句。我见过很多新手清理完集合df -h一看磁盘还是满的就以为没删掉。其实 WiredTiger 对已删除的空间会做复用文件本身并不立即收缩。如果你需要严格看到磁盘容量变化drop 这种释放文件的方案是首选如果是 deleteMany则要做好“空间暂时不回给操作系统”的心理准备。5.3 并发执行与锁竞争问题定时任务偶尔会出现一种隐蔽情况上一次清理还没跑完下一次触发时间又到了两个清理任务同时执行轻则日志混乱重则资源竞争把实例拖垮。特别是集合数据量大、单次删除耗时长的时候这种情况很容易出现。对策很简单在 shell 脚本里加一个锁文件利用flock保证同一时间只有一个实例在执行#!/bin/bash LOCK_FILE/var/lock/mongo_clean.lock exec 9$LOCK_FILE if ! flock -n 9; then echo $(date %Y-%m-%d %H:%M:%S) another clean process is running, exit exit 1 fi LOG_FILE/var/log/mongo_clean/clean.log mkdir -p /var/log/mongo_clean echo $(date %Y-%m-%d %H:%M:%S) clean start $LOG_FILE /usr/bin/mongosh mongodb://127.0.0.1:27017/admin --quiet --file /usr/local/bin/clean_tmp.js $LOG_FILE 21 echo $(date %Y-%m-%d %H:%M:%S) clean end $LOG_FILEflock -n意思是获取不到锁就立即返回失败而不是傻等。这样就算两个 cron 任务同时被触发也只有一个能真正执行清理另外一个看一眼锁文件就退出日志里记录一行“另一个任务正在清理”就够了。5.4 误清空事故的兜底建议最后分享一个比较重要的兜底习惯清理任务本质上是一个有损操作一旦目标集合名写错清错数据就是事故。我的做法是在脚本里加“保护开关”比如只在特定集合名列表内执行清理const allowedCollections [tmp_log, tmp_result]; const target tmp_log; if (!allowedCollections.includes(target)) { print([abort] collection not in allowlist:, target); quit(1); }这种保护对新手尤其有用能避免手滑把用户表、订单表写进清理名单。另外上线一个新的清理任务前几次建议先备份一次集合数据比如用mongodump --collectionxxx导出一份观察一两周确认逻辑稳定后再去掉备份步骤。数据无小事能多留一手就多留一手。我在实际维护中还有一个小技巧清理任务跑完后顺手把当前集合的文档数、集合物理大小、磁盘水位一起写进日志。这样每次清理的效果都留痕某一天发现数据增长异常一翻日志就能定位到是清理没执行还是写入量暴涨。定时清空 MongoDB 集合这件事技术难度不高最容易出事的往往是细节——选型选错、路径写错、权限不够、时间对不上。把这些细节都处理掉剩下的就是一个安安静静跑在凌晨的 cron 任务而你终于可以不用半夜爬起来看磁盘报警了。
返回列表