ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux定时清空MongoDB集合实战:从deleteMany到systemd

Linux定时清空MongoDB集合实战:从deleteMany到systemd 你有没有遇到过这种场景一张日志集合以每天几个GB的速度疯涨磁盘告警邮件半夜把你叫醒测试环境里的临时数据没清理隔几天就把页面接口拖垮又或者某个跑批生成的中间结果集合需要在下一轮任务开始前整个重置。这些需求最终都会指向同一个操作——在Linux上定时清空MongoDB的指定集合。这个需求看起来简单但实际操作里藏了不少细节比如用deleteMany还是drop、cron和systemd timer怎么选、认证和副本集模式下脚本怎么写才不踩坑。这篇文章从实际运维角度把定时清空MongoDB集合的完整思路、脚本、调度配置和排障过程都讲透适合正在维护MongoDB的运维、后端开发以及被日志撑爆磁盘的苦主们参考。1. 先从需求说起什么场景下需要定时清空集合1.1 日志类集合的磁盘治理最典型的场景就是日志集合。很多业务会把访问日志、操作日志、消息轨迹直接写进MongoDB写入频率高、字段又不固定用关系型数据库反而别扭。但日志数据的价值往往有时效性比如保留最近7天就行超过7天的历史日志既占磁盘又拖慢查询。这时候如果代码里没有完善的归档清理逻辑运维侧就需要一个定时任务来兜底。定时清空并不是粗暴地删光所有数据有些场景其实是“保留最近N天”。不过如果你只是需要一个干净的集合比如每天凌晨清空当天的临时表那直接全量清空就是最简单可靠的方式。做定时任务之前先想清楚你的数据到底能不能清、需要保留多久、能否容忍清空后瞬间的读写压力这决定了后面选哪种清空姿势。1.2 测试环境与CI/CD的复位需求测试环境是最容易产生“僵尸集合”的地方。每次跑自动化测试可能往库里写入一批测试数据用例结束后又没清理干净日积月累就是几百个集合、几十GB的垃圾。更麻烦的是脏数据会影响下一次测试的断言结果明明代码没问题因为上一次残留数据导致用例失败排查起来非常痛苦。我见过不少团队的做法是每次测试开始前先执行一个清理脚本把指定集合清空。这种场景通常要求“清完马上能用”不能有太长的锁竞争也不能清错集合——毕竟测试库里可能混着一些基础配置数据全清了反而更麻烦。1.3 临时中转集合的周期性重置还有一种场景是业务侧的临时集合。比如一个跑批任务先从消息队列拉数据写入某个集合然后另一个任务读取并处理这批数据处理完以后这个集合就没用了。如果跑批是每天、每周固定执行那在下一轮写入之前清空集合就是顺理成章的事情。这种场景下清空操作往往不是独立的它需要和业务任务编排在一起。你可以在业务代码里做但如果业务代码不好改或者多个任务共用同一个集合那就还是交给Linux定时任务来统一处理。1.4 定时任务要解决的三个问题把需求抽象一下一个可靠的定时清空方案需要回答三个问题清什么是只清空指定数据库下的指定集合还是批量清空多个集合或者是按条件删除部分数据。什么时候清避开业务高峰通常是凌晨2点到4点之间具体要看你的业务曲线。怎么清用deleteMany全删、用drop重建集合、还是依赖TTL索引让数据库自己删除过期数据。这三个问题决定你的脚本和调度配置长什么样。下面我一个个展开说。2. 清空集合的三种姿势与选型逻辑2.1 deleteMany({})最直接的全量删除先用最朴素的方式连接MongoDB执行db.collection.deleteMany({})把所有文档删光。这个方法的好处是集合本身、索引、分片配置都保持不变删完之后集合还能继续用不需要额外的重建动作。但deleteMany不是没有代价。在数据量比较大的情况下它会一条一条地删除文档每删一条都要更新索引、写入操作日志速度慢不说还容易产生锁竞争。删除几万条还好如果是几千万条你可能要等好几分钟才能删完。而且删除操作在MongoDB里是会产生大量磁盘碎片和oplog的如果被清空的集合在副本集里过大的删除操作还会给同步带来压力。所以deleteMany适合两种场景一是集合本身数据量不大百万级以内二是你需要保留集合的索引结构不能接受drop之后重建的成本。2.2 drop集合与立即重建大数据量的最优解数据量大到deleteMany扛不住的时候最实际的做法是直接drop()掉整个集合然后再重新创建。drop操作是元数据级别的速度极快无论集合里有1万条还是1亿条文档都能在瞬间完成。索引丢了也没关系可以在脚本里重建或者提前把索引定义保存好用createIndex恢复。这个方案的缺点是在你drop之后、重建之前任何读这个集合的操作都会报错因为集合不存在了。所以如果这个集合是线上业务在用的你必须确保清空时间窗内没有流量或者在代码里做了容错处理。另外如果是分片集合drop之后再重建需要重新配置分片键和分片策略步骤比普通集合多一些脚本里要记得处理。我个人的选择逻辑很简单数据量小、要求集合不被中断 → deleteMany数据量大、允许短时间不可用 → drop 重建。2.3 TTL索引让数据库自己按时间“定时”清理第三种方式和Linux定时任务无关但它是很多场景下更优的替代方案——用MongoDB的TTL索引让数据库每隔60秒自动扫描一次删除超过指定时间戳的文档。比如你给集合加一个created_at字段然后建索引db.collection.createIndex({created_at: 1}, {expireAfterSeconds: 604800})那7天前的文档就会被自动删除。TTL索引的好处是不需要外部定时器删除是数据库内部完成的粒度精确到秒级别也不会像cron那样有漏执行的风险。但它是“按时间条件删”不是全量清空而且TTL扫描有自己的周期默认60秒一次数据量大时删除性能不一定好。适合“保留最近N天”的场景不适合“每天固定时间清空整个集合”的场景。如果你本来就是想全量清空用TTL思路就得绕个弯可以给所有文档都写入一个统一的过期时间戳然后让TTL去删。但这样做比直接deleteMany更绕收益有限我一般不推荐。2.4 三种方式对照表方式速度索引影响集合可用性适用场景deleteMany({})中等保留索引全程可用百万级以内、需保留索引drop 重建极快索引丢失需重建短时间不可用大数据量、可容忍中断TTL索引后台自动保留索引全程可用按时间保留N天数据3. 写一个靠谱的清空脚本mongo shell还是mongosh3.1 连接字符串与最小脚本骨架很早以前大家用的是mongo客户端命令但MongoDB从5.0开始逐步用mongosh取代老旧的mongoshell。如果你还在用老命令建议尽早切换因为新版本的服务端可能不再兼容老shell我就见过不少人在MongoDB 6.0上执行mongo --version直接报“command not found”的。一个最基础的全量清空脚本长这样:#!/bin/bash # 基础连接信息 MONGO_HOST127.0.0.1 MONGO_PORT27017 DB_NAMEapp_log COLLECTION_NAMErequest_log # 如果开启了认证建议用环境变量传密码避免明文写死在脚本里 MONGO_USER${MONGO_USER} MONGO_PASS${MONGO_PASS} mongosh mongodb://${MONGO_HOST}:${MONGO_PORT}/${DB_NAME} \ --username ${MONGO_USER} \ --password ${MONGO_PASS} \ --quiet \ --eval db.getSiblingDB(${DB_NAME}).${COLLECTION_NAME}.deleteMany({}) # 打印执行结果方便后续排查 if [ $? -eq 0 ]; then echo $(date %Y-%m-%d %H:%M:%S) 清空集合 ${DB_NAME}.${COLLECTION_NAME} 成功 else echo $(date %Y-%m-%d %H:%M:%S) 清空集合 ${DB_NAME}.${COLLECTION_NAME} 失败 2 exit 1 fi有几个细节说明一下getSiblingDB用来切换数据库比直接拼连接串里的库名更明确避免脚本里字符串拼接出错导致连到了错误的库。--quiet可以去掉mongosh的欢迎语和提示符让cron日志干净一些。如果不需要认证去掉--username和--password参数即可。如果集合不存在deleteMany({})不会报错它会正常返回只是删除了0条数据。这个特性可以让你不用在脚本里额外判断集合是否存在。3.2 认证、超时与连接稳定性处理自带认证的MongoDB集群脚本里最怕的是用户名密码配置出错。我不建议把密码明文写在脚本里更不建议直接写在crontab命令里那样任何能读到cron配置的人都能看到你的数据库口令。建议的做法是把账号密码放在一个单独的配置文件里权限设为600脚本运行时通过source或环境变量读取。另一个容易忽视的问题是连接超时。如果MongoDB所在网络不稳定mongosh默认可能会卡很久导致cron任务超时。可以在连接串里加上连接超时参数mongosh mongodb://${MONGO_HOST}:${MONGO_PORT}/${DB_NAME}?connectTimeoutMS5000socketTimeoutMS30000 ...connectTimeoutMS表示建立连接的超时时间socketTimeoutMS表示操作执行的最长等待时间。结合你自己的网络情况设置避免一个卡死的任务占住cron太久。3.3 drop 重建索引的完整脚本如果选择了drop方式脚本里还需要重建索引。比如原来有一个user_id字段上的索引、一个create_time上的复合索引那脚本逻辑就是mongosh mongodb://... --quiet --eval const db db.getSiblingDB(app_log); db.request_log.drop(); db.createCollection(request_log); db.request_log.createIndex({ user_id: 1 }); db.request_log.createIndex({ create_time: 1 }, { expireAfterSeconds: 604800 }); 这里我故意把TTL索引也一起重建因为如果原本有这个索引drop之后如果没有重建过期数据清理就失效了。你可以先查一下原集合的索引列表用db.request_log.getIndexes()输出再对着重建。强调一点drop操作是不可恢复的。如果因为脚本参数写错、连到了生产库、$DB_NAME变量为空之类的原因drop掉一个不该drop的集合数据就是永久丢失。所以下面第六部分专门讲了兜底措施这个不是可有可无的。4. 定时调度cron与systemd timer实战4.1 crontab配置与关键细节脚本写好后让Linux定时执行。最经典的方式是crontab。先看一个例子# 每天凌晨2点30分执行清空脚本 30 2 * * * /opt/scripts/clean_mongo.sh /var/log/clean_mongo.log 21这条cron的意思是每天2:30执行/opt/scripts/clean_mongo.sh标准输出和错误输出都追加到日志文件。如果把日志重定向去掉cron会把输出用邮件发给当前用户很多Linux发行版没配邮件服务输出就丢了出问题了你根本不知道。cron有五个时间字段分别对应分、时、日、月、周*代表任意值。几个常用写法# 每天0点 0 0 * * * /opt/scripts/clean_mongo.sh # 每小时的第10分钟执行 10 * * * * /opt/scripts/clean_mongo.sh # 每周日凌晨3点执行 0 3 * * 0 /opt/scripts/clean_mongo.sh # 每天凌晨2点和下午2点各执行一次 0 2,14 * * * /opt/scripts/clean_mongo.sh4.2 别忘了cron的PATH环境变量很多人写cron脚本时会踩一个大坑脚本里明明用了mongosh命令手动执行一切正常放进cron里却提示command not found。原因很简单cron执行任务时的环境变量和你手动登录终端时不一样PATH里往往没有mongodb的bin目录比如/usr/local/bin或/opt/mongodb/bin。解决方案有几种在脚本开头显式设置PATHexport PATH/usr/local/bin:/usr/bin:/bin:$PATH脚本里使用绝对路径调用mongosh/usr/local/bin/mongosh ...在crontab文件里设置PATH变量PATH/usr/local/bin:/usr/bin:/bin但crontab文件顶部设置的变量可以被该crontab下的所有任务继承。我自己习惯在脚本开头加上PATH导出因为脚本除了被cron调用还经常被手动执行写进脚本里两处通用。4.3 systemd timer更现代的替代方案如果你不想用cron或者想更精细地控制执行时机、失败重试等策略systemd timer是更现代的选择。systemd timer有两个组成部分一个service单元定义要执行什么和一个timer单元定义何时执行。先创建服务单元/etc/systemd/system/mongo-clean.service[Unit] DescriptionClean Mongo Collection Afternetwork.target [Service] Typeoneshot ExecStart/opt/scripts/clean_mongo.sh Userroot StandardOutputjournal StandardErrorjournal再创建定时器单元/etc/systemd/system/mongo-clean.timer[Unit] DescriptionTimer for Mongo Collection Cleanup [Timer] OnCalendardaily OnCalendar*-*-* 02:30:00 Persistenttrue [Install] WantedBytimers.target然后启动并查看状态systemctl daemon-reload systemctl enable mongo-clean.timer systemctl start mongo-clean.timer systemctl list-timers --allsystemd timer的OnCalendar语法比cron更可读Persistenttrue表示如果错过了一次执行时间比如机器当时关机开机后会自动补执行一次这个特性对清空任务来说很关键——宁可我晚点跑不能漏跑。4.4 日志与运行状态追踪用了cron就把脚本的标准输出重定向到日志文件。但光追加日志还不够建议每次执行时把时间戳、数据库、集合、受影响行数都写进去方便事后核对。前面的脚本已经打印了成功和失败信息可以再加一行删除数量统计result$(mongosh ... --eval const res db.getSiblingDB(app_log).request_log.deleteMany({}); print(res.deletedCount); )如果脚本放在systemd里日志用journalctl -u mongo-clean.service查看。我个人习惯在脚本里同时把关键操作写进一个独立的审计日志文件否则如果清理逻辑后续出了性能问题单看journalctl信息不够显眼。5. 我踩过的坑与排查链路5.1 时区错位导致清理时间不对有一次同事反馈明明crontab写的是凌晨2点清理但实际清理却发生在中午12点。查了半天发现那台服务器的系统时区是UTC而业务方一直以为是中国标准时间。cron是根据系统时钟执行的不是根据你脑子里想的时区执行。所以配置时间之前先跑date和timedatectl确认系统时区。如果系统时区不能改而你希望按北京时间凌晨2点执行可以尝试把cron时间换算成UTC对应的时间。但服务器往往不止一个业务在用改时区影响面很大这种办法不够通用。更稳妥的方式是在脚本里通过TZ环境变量强制指定时区输出和判断都按目标时区走。比如export TZAsia/Shanghai5.2 mongosh在cron里“神秘”失败的真实排查过程有个老项目一直用旧版mongoshell后来升级服务器之后cron里的清理任务开始间歇性失败。我当时的排查路径是先看cron日志grep clean_mongo /var/log/syslog发现任务确实执行了但脚本内部报错。手动执行脚本一切正常于是怀疑是环境变量问题。在脚本里加了一行echo $PATH /tmp/debug.log再通过cron跑一次发现PATH里确实少了MongoDB的bin目录。在脚本开头加上export PATH/usr/local/bin:/usr/bin:/bin:$PATH问题解决。注意如果手动执行正常、cron执行失败90%的可能性是环境变量差异剩下10%才是权限、网络或依赖问题。还有一个隐蔽的坑脚本里用了相对路径。比如执行source ./config.sh手动在脚本目录下执行没问题但cron的工作目录默认是你执行crontab -e的那个用户的主目录经常不是脚本所在目录。脚本里一切涉及路径的地方都用绝对路径别偷懒。5.3 副本集与分片场景下的清空注意事项如果你的MongoDB是副本集那么写入操作需要大多数节点确认才算成功。脚本里如果只连到主节点默认writeConcern为1通常不会失败。但清空数据量非常大时主节点删除操作生成的oplog如果超过从节点的同步能力可能造成从节点延迟暴增。更严重的是过大的删除操作会导致oplog覆盖从节点无法追上只能重新全量同步。所以在大数据量清空时建议分批量删除而不是一次性全删。比如while (true) { const res db.collection.deleteMany({}, { limit: 10000 }); if (res.deletedCount 10000) break; }注意deleteMany本身不支持limit参数上面的写法只是示意。实际可以配合查询条件分批处理比如删除一个固定的时间范围循环执行每次删一段。另外分片集群下清空某个分片集合如果直接drop集合分片配置如sh.shardCollection会丢失需要重建分片键。如果只是想临时清空数据且保留分片配置deleteMany是更安全的选择。5.4 认证模式下脚本报权限错误开启鉴权后清理任务的账号需要具备对目标库的readWrite权限。很多人图省事直接用root账号或者用全局readWriteAnyDatabase权限这有安全风险。最小权限原则下可以单独创建一个只用于清理的账号use admin db.createUser({ user: cleaner, pwd: strong_password, roles: [ { role: readWrite, db: app_log } ] })这样即使脚本被泄露影响范围也限制在app_log这个库。6. 兜底与监控别让定时任务成为下一个事故6.1 清空前的备份与白名单机制凡是涉及删除的操作必须有备份和误操作保护。定时清理任务不像手动操作它一旦出错可能连续运行很多天你才发现。我建议在脚本里加一个“保护名单”机制把不允许被清空的集合名之类维护在一个列表脚本执行时先做比对如果目标集合在保护名单里就直接退出并报警。举个例子PROTECT_COLLECTIONSsystem_users,audit_log,config_data for coll in $PROTECT_COLLECTIONS; do if [ $COLLECTION_NAME $coll ]; then echo 目标集合在保护名单中禁止清空退出 exit 1 fi done这样即使有人改错了cron参数比如把环境变量写错、把COLLECTION_NAME指到了核心业务集合脚本也会先拦住。至于备份如果集合数据量不大清空之前用mongodump把集合导出来留一份mongodump --db $DB_NAME --collection $COLLECTION_NAME --out /backup/$(date %F)数据量大的话每天全量备份不清现实至少要做到只清理可重建数据、核心业务数据不清。这个取舍必须在需求阶段明确。6.2 失败与延迟的监控告警定时任务最怕“无声失败”脚本执行了但因为各种原因没清空成功日志也写得模棱两可没人发现直到磁盘告警或数据污染后才暴露。所以脚本里必须有明确的退出状态码和错误输出同时用外部监控来探测清理结果。我常用的做法是清理脚本在成功时输出特定关键字比如CLEAN_SUCCESS然后监控工具对日志进行关键字匹配一旦发现没有这个关键字或者出现了CLEAN_FAILED就触发告警。另外还有一个思路在清理结束后向一个内部消息通道发送心跳如果今天没收到心跳说明任务没跑或者没跑成功。6.3 幂等性与可重复执行定时清空任务的另一层要求是幂等。意思就是不管这次任务之前是否已经清过再执行一次也不会出问题不会引起数据错乱。像deleteMany({})天然幂等——第一次清空之后集合为空第二次再清还是空。drop 重建也基本幂等只要能容忍重建索引的时间。但如果你在清空之后还要往集合里插入一条“今天已清理”的记录那就要注意了重复执行可能会插入多条标记记录。这种情况需要在插入前先删除同类型记录保持在可重复执行的前提下仍然只有一个标记。6.4 从脚本维护角度留好后路脚本上线之后不是一劳永逸的。MongoDB版本升级、认证方式变化、集合名称调整都可能让原本正常的定时任务失效。我给自己定的维护准则是所有清理脚本统一放在一个目录命名带环境标识比如/opt/scripts/mongo_clean_prod.sh避免多环境混用。脚本里对执行前和执行后的集合文档数分别计数输出到一个状态文件方便快速验证清理是否真的生效。不要直接在生产环境测试新脚本先在测试库跑通再切换到生产cron。结合这几条定时清空MongoDB集合这个看似简单的操作就能做到既可靠又安全。你在实际配置时先从数据和业务场景想清楚该用哪种清空方式再选择cron还是systemd timer调度脚本写成带幂等和日志检查的样子上线后观察几天清理结果这套方案才算真正落地。最后提醒一句清理任务不是设置完就不管的定期检查一条日志、看一次集合文档数比任何复杂机制都管用。
返回列表