磁盘爆满不用慌:一套可靠的定时日志清理脚本实战

发布时间:2026/9/16 2:00:20
磁盘爆满不用慌:一套可靠的定时日志清理脚本实战 日志这东西平时安安静静躺在 /var/log 底下等到磁盘满了你才发现它有多能吃。上个月我们一台业务服务器凌晨3点在钉钉群里疯狂报磁盘告警SSH上去一看/ 分区 100% 被占满连 tab 键补全都卡顿。df -h 一看 /var/log/messages 已经 74G一查才发现某中间件 debug 日志开关被误开了三天没看就涨了 60G。那次我是真的顶着眼睛血丝手动清了一小时。后来我痛定思痛把这套定时清理方案写成脚本固化下来也成了这个“每天一个Linux运维脚本”系列里的第13篇。这篇就完整分享一下我的清理思路、脚本实现和一次次踩坑后总结的经验适合所有被日志爆盘坑过的运维、开发尤其是刚接手服务器没多久、看到磁盘告警就慌的新手朋友。这套方案的核心思路很简单不是简单粗暴地在磁盘满了之后去删而是提前用 crontab 挂一个脚本每天定时清理超过保留期限的日志并且在脚本里加好“磁盘空间低于阈值才动手”的保险。但如果你以为“日志清理”就是一句find /var/log -type f -mtime 7 -exec rm -f {} \;那就太天真了真的这么干你会把正在写的日志删掉磁盘空间却一分都不释放甚至可能把数据库 binlog 删了导致主从复制挂掉。真正的难点在于——什么文件能删、什么时候删、用什么姿势删才安全。下面听我慢慢拆。1. 日志为什么会把磁盘吃光先搞清楚根源1.1 最常见的爆盘路径要清理日志你得先知道日志是从哪来的。下面这几个目录和文件类型是我这几年几乎每次处理爆盘都会撞见的老面孔日志类型常见位置膨胀特点系统日志/var/log/messages、/var/log/syslog系统消息和内核日志默认会一直累积任务计划日志/var/log/cron每个 cron 任务执行都会记录任务多时长得很快软件运行日志/usr/local/app/logs/*.log应用自己打的日志最容易失控Web 访问日志/var/log/nginx/access.log、/var/log/httpd/access_log并发高时一天几百 MB 很正常数据库日志/var/lib/mysql/binlog.*、/var/log/mysql/slow.logbinlog 如果没设置过期时间会积攒到让你怀疑人生程序输出文件nohup.out、.out、core.调试期最容易把 nohup.out 写到几个 G这里头最坑的是应用日志。系统日志好歹有 logrotate 帮你轮转但很多业务程序尤其是 Java 系的中间件、自己在代码里写文件日志的 Python 脚本根本不理会系统的日志轮转机制。只要线上代码开了 debug 级别或者某个请求进了死循环疯狂打日志磁盘就是坐上了火箭。我见过一个极端案例有个支付回调服务在异常分支里没有限制日志输出频率出错之后以每秒几百条的速度往 error.log 里写不到四小时 80G 的根分区就满了。1.2 爆盘之后怎么定位到底是谁干的磁盘告警之后不要慌先花一分钟把“谁占了我的盘”这个问题搞清楚。我定位日志大文件的标准动作是三步走# 第一步看分区整体使用情况 df -h # 第二步锁定具体目录的占用排行从根目录逐层往下 du -sh /var/log /usr/local/app /home 2/dev/null # 第三步在工作目录内列出最大的前20个文件 find /var/log -type f -size 500M -exec ls -lh {} \; | sort -k5 -rh | head -20第三步这条命令是定位大日志文件的杀手锏。-size 500M只找超过 500MB 的文件排序后眼睛一扫就知道哪个文件是罪魁祸首。我这里要特别提一个新手容易踩的坑如果你已经用rm -f删掉了一个被进程打开的日志文件但df -h看到磁盘空间还是满的千万别以为是没删掉这个文件其实已经被标记为 deleted空间要等持有它的进程把文件描述符关闭一般要等进程重启才会释放。2. 清理脚本的设计思路与关键决策2.1 为什么不直接写一条 rm 命令了事很多人觉得定时清理不就是在 crontab 里写一行find /var/log -mtime 7 -delete吗我最早也这么干过结果踩了好几个坑。第一日志目录的结构各式各样有直接躺在 /var/log 下的也有躺在好几级子目录里的一条不带-name匹配的 find 可能会把不该删的数据库文件删掉。第二一天之内文件大小可能差别巨大——今天访问量高access.log 一天涨 5G明天低峰期才 200MB只按天数删根本控不住磁盘水位。第三有些日志文件很特殊比如 MySQL 的 binlog 正在被主从复制读取你光看 mtime 超过 7 天就删主从同步当场就断给你看。所以一个合格的清理脚本必须满足几个条件能同时匹配“超期自动删”和“磁盘满了强制清”能区分不同目录的处理策略能在删除前先判断当前磁盘使用率而不会误伤正在使用的重要文件以及每次删了哪些文件、释放多少空间都留有审计记录。2.2 核心策略保空间优先超期文件为参考我在设计脚本时定下了两条主线一是以磁盘空间使用率为第一判断依据只有当使用率超过我设置的阈值比如 80%才触发清理动作低于阈值时最多只做“巡扫超期文件”不删、不惊动二是清理动作优先扫“安全目录”像 /var/log 下的 messages、cron、nginx access 这类纯日志超期限就删而数据库 binlog 目录、带“保留策略”的业务日志脚本只做提醒不直接删。这套“先看水位、再定动作”的逻辑能最大限度降低误删风险。为什么要设 80% 而不是 90% 或 95%这其实是我经过好几次“救火”之后总结出来的安全水位。90% 甚至 95% 再动手系统可能已经出现比较明显的性能问题——比如写入卡顿、监控脚本跑不起来甚至 SSH 登录都费劲。80% 触发清理给脚本执行留足了时间也给真正的业务数据写入留了余量。当然如果你们监控系统比较灵敏60% 预警、75% 清理也可以具体可根据机器磁盘余量和日志增长速度来调。2.3 工具选型find 是主角truncate 是替补脚本里最核心的工具就是find这个命令本身不复杂但有几个参数你得分清楚用错了后果很严重find 参数含义我实际使用中的体会-mtime N修改时间超过 N 天前的文件日志文件不推荐用 atime访问时间因为访问时间不一定可靠-size N文件大小大于 N单位是 c(字节)、k、M、G随手写500M和500m都是合法的-name *.log按文件名匹配非常关键避免把非日志文件也扫进来-daystart从当天零点开始计算时间如果你希望“今天不算进去”建议加上否则会按 24 小时滚动计算-delete直接删除找到的文件它相当于 -exec rm -f {} ;但更高效除了删文件我还会用到truncate -s 0这个操作。它的作用不是删除文件而是把文件清空文件仍然存在inode 不变进程持有它的文件描述符也不受影响。对于 Nginx、Tomcat 这类在持续写入的日志文件直接rm会产生一个非常尴尬的中间态——旧文件被删了但空间没释放新文件又建立了结果/分区还是满的只有重启进程才会释放空间。而truncate -s 0则能当场就让磁盘空间降下来进程也不感知。所以我的脚本是“组合拳”策略可安全删除的老文件用 find rm正在被进程写入的大文件用 truncate。3. 完整脚本带参数可配置的日志清理工具3.1 脚本代码与核心参数解释下面这就是我目前在生产环境跑了好几个月的清理脚本没有花哨功能但每一项配置都是我踩坑后加进去的直接贴出来给大家参考#!/bin/bash # # 日志清理脚本 clear_old_logs.sh # 功能按磁盘空间使用率和日志保留天数双维度清理 # 适用CentOS 7/8/Rocky Linux/Ubuntu 等主流发行版 # # 可配置区域 # 磁盘使用率告警阈值超过则强制清理超期日志单位% DISK_THRESHOLD80 # 各日志目录的保留天数 RETAIN_SYSLOG7 # 系统日志保留7天 RETAIN_APP_LOG3 # 业务应用日志保留3天 RETAIN_NGINX_LOG15 # Nginx/Web访问日志保留15天 # 超过此大小的日志文件即使未到期也触发 truncate 清空单位M SINGLE_FILE_MAX_SIZE2048 # 日志目录列表可自行增删 LOG_DIRS( /var/log /var/log/nginx /usr/local/app/logs /home/apps/logs ) # 执行日志路径 CLEAN_LOG/var/log/clean_logs_history.log # 可配置区域结束 CURRENT_DATE$(date %Y-%m-%d %H:%M:%S) CURRENT_USAGE$(df -h / | awk NR2 {print $5} | tr -d %) LOG_DIRS_LENGTH${#LOG_DIRS[]} function write_log() { echo $CURRENT_DATE [INFO] $1 $CLEAN_LOG } function clean_by_retain_days() { local dir$1 local retain_days$2 if [ ! -d $dir ]; then return 0 fi find $dir -type f \( -name *.log -o -name *.out -o -name *.txt \) -mtime $retain_days -exec rm -f {} \; 2/dev/null } function truncate_oversize_log() { local dir$1 if [ ! -d $dir ]; then return 0 fi find $dir -type f \( -name *.log -o -name *.out \) -size ${SINGLE_FILE_MAX_SIZE}M -exec truncate -s 0 {} \; 2/dev/null } write_log 日志清理脚本启动当前磁盘使用率: ${CURRENT_USAGE}% # 先针对超大文件执行 truncate防止单文件把磁盘打满 for d in ${LOG_DIRS[]}; do truncate_oversize_log $d done # 如果磁盘使用率高于阈值执行按时间删除 if [ $CURRENT_USAGE -ge $DISK_THRESHOLD ]; then write_log 磁盘使用率(${CURRENT_USAGE}%)超过阈值(${DISK_THRESHOLD}%)开始清理超期日志... clean_by_retain_days /var/log $RETAIN_SYSLOG clean_by_retain_days /var/log/nginx $RETAIN_NGINX_LOG if [ -d /usr/local/app/logs ]; then clean_by_retain_days /usr/local/app/logs $RETAIN_APP_LOG fi if [ -d /home/apps/logs ]; then clean_by_retain_days /home/apps/logs $RETAIN_APP_LOG fi write_log 超期日志清理完成 else write_log 磁盘使用率(${CURRENT_USAGE}%)未超过阈值(${DISK_THRESHOLD}%)本次不执行删除仅完成超大文件截断 fi AFTER_USAGE$(df -h / | awk NR2 {print $5} | tr -d %) write_log 日志清理脚本执行完毕当前磁盘使用率: ${AFTER_USAGE}% 解释一下几个关键部分的设计意图。write_log函数把所有动作写进 /var/log/clean_logs_history.log这样清理脚本自己也有日志可查否则脚本删了半天都不知道发生了什么出了问题两眼一抹黑。clean_by_retain_days只匹配.log、.out、.txt三种后缀千万别图省事把-exec rm -f {}作用于整个目录否则一旦误匹配到配置文件、数据文件、业务上传的附件就是真的生产事故了。truncate_oversize_log独立于磁盘阈值执行不管是 50% 还是 80% 使用率只要出现超过 2GB 的单个日志文件就直接截断这样能从根源上防止“单文件撑爆分区”的极端情况。3.2 为什么需要 truncate 和 rm 双策略并存这里我展开讲一下truncate -s 0和rm的区别因为这是整个脚本里最容易被忽略、实际上最重要的设计。truncate -s 0是把文件长度改成 0但文件本身还在ln 链接和 inode 都保留了正在往这个文件里写数据的进程不会感知到文件被“换掉”它继续使用同一个文件描述符空间立刻得以释放。rm则是删掉文件名和 inode 的关联如果进程还开着这个文件句柄那空间不会立刻释放而是等进程 close 文件描述符后才归还。实操中的组合逻辑是这样的Nginx 的 access.log 是我们想保留内容的 Web 日志但我们只想保留最近若干天的内容此时我们按 mtime 超期直接 rm 老文件完全没问题因为 Nginx 的 access.log 只在它被轮转或重启时才会写入新文件句柄但对于 Java 中间件写死的 app.log进程长期持有 FD你 rm 了它、空间又不释放最稳妥的做法就是 truncate。所以脚本的执行顺序是“先 truncate 超大文件再按天删除超期文件”两条线并行磁盘空间绝对有保障。3.3 实际运行的输出效果脚本部署后可以手动执行一次看效果chmod x /usr/local/sbin/clear_old_logs.sh /usr/local/sbin/clear_old_logs.sh执行后查看清理历史日志tail -n 20 /var/log/clean_logs_history.log正常的输出是这样的2025-01-15 03:00:01 [INFO] 日志清理脚本启动当前磁盘使用率: 82% 2025-01-15 03:00:01 [INFO] 磁盘使用率(82%)超过阈值(80%)开始清理超期日志... 2025-01-15 03:00:03 [INFO] 超期日志清理完成 2025-01-15 03:00:03 [INFO] 日志清理脚本执行完毕当前磁盘使用率: 74% 一次清理从 82% 降到 74%这就达到了“防爆盘”的目的。日常磁盘使用率在 60% 左右时执行脚本你会发现它只截断超大文件不删超期文件行为温和得多。4. 定时任务配置与效果验证4.1 crontab 定时方案怎么选频率脚本写好后就得靠 crontab 让它每天都干活。定时策略我建议两类业务日志量大、服务器繁忙的每天凌晨 2 点到 4 点之间跑一次业务低峰在深夜但不希望打扰备份作业的可以避开备份窗口选择中午 12 点半跑一次。我自己的习惯是每天 3 点跑一次因为凌晨的业务量最小清理动作不会影响到用户请求同时也能避开凌晨 1 点常见的全量备份任务。crontab 配置方式crontab -e # 每天凌晨3点整执行日志清理脚本 0 3 * * * /usr/local/sbin/clear_old_logs.sh /dev/null 21这里我建议把脚本输出重定向到/dev/null因为 crontab 默认会把任务的输出通过邮件发给 root时间久了 root 邮箱会被一堆重复日志塞爆。如果你希望保留执行记录在脚本里的 write_log 落地已经完全够用没必要在 crontab 里再写一次重定向文件那样反而会产生两个日志副本这类细节在运维上都是“隐形噪音”。4.2 上线后的验证手段手动模拟到自动监控脚本刚上线时不要直接丢进 crontab 就不管了我的做法是先手动跑三次、看执行日志、确认没有任何误删再加到 crontab 里。具体验证看这几点一是 df -h 的空间水位是否在每次执行后显著下降二是 clean_logs_history.log 里有没有执行错误三是应用系统在脚本执行期间有没有报错告警。为了保险我还会加一个“预防式手电筒”在 crontab 里再挂一条磁盘空间监控命令如果空间使用率超过 90% 就直接在系统里生成一个紧急告警文件配合 Zabbix/Prometheus 之类的监控平台推送到钉钉或企微群0 * * * * [ $(df -h / | awk NR2 {print $5} | tr -d %) -gt 90 ] echo Disk usage 90% | tee /dev/stderr | mail -s Disk Warning youremail.com这样即便清理脚本出了岔子你也能第一时间收到消息人工介入。你可以把它理解成一个双保险——自动清理是常态手段预警是兜底手段两者缺一不可。5. 常见问题与排查技巧实录5.1 删了日志但磁盘空间没变少这应该是新手最容易被劝退的问题。空间没变少的核心原因我前面已经提过了进程持有文件的打开句柄。当你rm -f /var/log/app.log后文件从目录中消失了但某个 Java 进程还开着这个文件描述符文件数据块没有真正释放df -h看到的空间自然不会变化。排查方法# 查看已被删除但仍被进程占用的文件 lsof | grep deleted如果输出的确有进程占用了已删除的日志文件处理方式有两种一种是可以重启对应服务让它释放文件描述符但高峰时段尽量别重启另一种是既然已经删了就让它先挂着等业务低峰或发版窗口再重启。所以我在脚本里对“可能被进程持有”的大文件优先使用 truncate而不是 rm就是为了绕开这个坑。5.2 误删正在写入的日志还好我有备份习惯还有一次我帮同事排查问题他图省事直接在 crontab 里写了条find /var/log -name *.log -mtime 3 -exec rm -rf {} \;结果把 Nginx 正在写的 access.log 给删了。Nginx 在写日志时发现文件没了会报open() /var/log/nginx/access.log failed (2: No such file or directory)而且新请求无法生成 access 日志直到你 reload Nginx 创建新文件。这类事故的教训是一定不要对所有日志文件“一刀切”地按时间删除正在写入的访问日志、错误日志要优先用 truncate 或者日志轮转来处理。我的脚本能安全运行核心就是文件后缀白名单和 truncate 组合策略这两个设计缺一不可。5.3 数据库 binlog 能不能用脚本删这是个高危问题我特别要提醒一句MySQL 的 binlog 绝不建议用这个脚本清。binlog 的命名格式通常是binlog.000001、binlog.000002它们不是 .log 后缀所以脚本默认不会匹配到但如果你手滑把清理规则改宽了或者有人自作主张在脚本里加了一条针对.000001之类的匹配那就真的危险了。binlog 不仅记录了所有更改数据的 SQL 事件做主从复制时从库还要靠它同步数据你这边删了从库还没读到复制直接中断别提多尴尬了。binlog 的正确治理方式是在 MySQL 里设置expire_logs_days参数或者用PURGE BINARY LOGS BEFORE NOW() - INTERVAL 7 DAY;语句安全清理不要用操作系统层的 find rm 去碰它。5.4 脚本运行太频繁导致磁盘 IO 飙高另外一个稍微隐蔽的问题是清理脚本本身也会消耗磁盘 IO。当你的/var/log下文件数量巨大几万个文件时find扫描全目录要遍历 inode执行期间磁盘读 IO 会明显上涨甚至和业务高峰叠加造成服务响应变慢。所以我建议第一把执行时间固定在业务低谷第二在 find 时按需限制目录深度比如-maxdepth 3不去递归扫描过深的目录第三如果文件数量实在太多可以用find ... -mtime N | head -n 500限制单次删除数量分多个周期慢慢消化。这些都是生产环境跑了很久才积累出来的细节。5.5 一个更工程化的替代思路logrotate最后和你聊聊 logrotate。如果你的日志清理需求没那么特殊、应用也遵循系统惯例用 logrotate 会比自研脚本稳妥得多。它支持按大小、按天轮转也可以配置compress压缩旧日志、maxage自动清理超期日志。但 logrotate 也有它的问题它依赖配置文件且是每个日志文件单独配置对几十个业务目录逐一维护也是一件麻烦事而且很多 Java 应用自己实现了日志框架Log4j2、Logback 之类文件写法和系统惯例不一致logrotate 管起来其实很别扭。所以我的建议是系统级日志用 logrotate业务级大日志用自研脚本两边结合各管各的地盘。5.6 问题排查速查表症状可能原因排查方法解决方案删了文件磁盘空间没释放进程仍持有已删除文件句柄lsof #124; grep deleted重启进程或使用 truncate 方案脚本执行后空间反而增长你在清理脚本日志路径下反复输出执行记录cat /var/log/clean_logs_history.log将脚本日志重定向到独立分区或开启 round-robin误删了正在写入的日志文件find 匹配范围过宽检查 find 的 -name 参数增加文件后缀白名单优先 truncate磁盘 IO 高峰期飙高find 扫描大量小文件用time命令统计脚本耗时固定低峰执行、加 maxdepth、限制单次删除数量binlog 被误删导致主从中断脚本匹配规则超出安全范围查看 binlog 被实际的 mtime用 MySQL 自带 purge 命令操作系统层不碰 binlog写在最后的个人体会脚本本身并不复杂真正复杂的是在生产环境里一遍遍踩坑后的“敬畏心”。我见过很多人一开始都觉得删日志而已几句话的事结果不是把正在写的文件删了就是空间没释放被领导质疑“你会不会运维”甚至还有删到一半碰上日志量暴涨脚本自己被 OOM 干掉的。运维这个岗位讲究的从来不是你会多少炫技而是你的脚本在凌晨三点无人值守时能不能老老实实把该干的活干完别添乱。我现在每接手一台新服务器第一件事就是把日志滚动策略和清理任务配好——先用 logrotate 管系统日志再用这个脚本兜底业务目录最后挂上磁盘告警。这套组合拳打下来磁盘爆盘这个老大难问题基本就算提前解决了。你也试试看跑两周之后回来看磁盘趋势图你会感谢现在的自己。