
发现日志把磁盘写满大概是每个用过 Linux 服务器的人都经历过的糟心事。日志文件本身没多大但架不住量多、增长快尤其是访问量稍微上去一点的 Nginx、Java 应用或者系统本身的各种 syslog随便跑上几个月就能把几个 GB 空间吞掉。我写过不少日志自动清理脚本也踩过不少坑这次把一套相对完整、可以直接拿去用的思路和脚本方案整理出来覆盖 Linux 下按天清理、按大小清理、结合 logrotate 轮转、配合 cron 定时执行这几块适合服务器运维、后端开发、以及想把自己 VPS 或者开发机整干净的 Linux 用户参考。1. 日志清理的整体思路与方案选型1.1 为什么日志一定要清理很多人以为日志只是文本文件占不了多少空间。但真实环境里一个每天产生几百 MB 访问日志的 Web 服务一年下来就是上百 GB。更麻烦的是日志写在系统分区一旦把/或者/var分区写满轻则服务报错重则系统直接起不来。我见过最典型的场景一套 Java 应用日志没做轮转每天稳定输出 2GB 日志跑了一个多月后服务器磁盘告警。登上去一看/var/log/app/下全是application.log的副本拷贝堆叠了几十个 GB旧日志根本没清理机制。服务还能跑但写日志时开始卡顿因为磁盘 I/O 已经很紧张。日志清理不是删掉日志就行核心要做的是在保留足够排障材料的前提下删除过期、超大日志确保磁盘资源可循环。这是一个平衡问题不是简单粗暴地rm -rf。所以第一步我们先想清楚清理策略怎么定。1.2 三种常见清理方案find、logrotate、自定义脚本Linux 生态里清理日志其实有现成工具但很多人不知道它们各自的定位结果要么乱用要么全手写。我按实际使用频率排序方案适用场景优势局限findcron脚本自定义日志路径非系统默认位置灵活路径和规则完全可控写错参数有误删风险logrotate系统标准日志、主流服务日志按大小/时间轮转可压缩、可删除旧档不了解规则时容易犯配置错误纯shell脚本手写循环特殊文件名规则、复杂业务日志能处理完全非标的日志名代码量大维护成本高find是 Linux 日志清理里最高频的命令。它可以根据文件年龄、大小、名称模式查找文件并删除而且支持-delete直接清理。logrotate是更上层的工具它不只是删除还能做日志轮转——把当前日志重命名再让程序写新文件配合压缩和保留数量控制。两者不是互斥的实际生产环境里经常组合使用系统日志交给 logrotate业务自定义日志用脚本 find。1.3 我的方案选型逻辑以我自己的服务器为例日志来源有三类Nginx 访问日志存放在/var/log/nginx/文件名带日期access.log-YYYYMMDD或者不带日期单文件access.log。Spring Boot 应用日志存放在/data/app/logs/按小时滚动成app-YYYYMMDD-HH.log。系统日志包括/var/log/syslog、/var/log/kern.log等由 systemd 和 rsyslog 写入。前两类文件每天都会产生新文件适合直接用 find 按时间清理第三类单个文件名不变、持续增长更适合用 logrotate 按大小轮转。所以我最终的方案是业务日志用脚本 find 按天清理系统日志用 logrotate 按大小切割。这个组合兼顾了清理效率和可控性后面我会分别展开。2. 日志清理脚本的核心细节拆解2.1 find 命令的过滤维度日志清理脚本的灵魂就是find它支持三种过滤维度时间维度年龄、大小维度、名称维度。这三个维度组合起来基本能覆盖所有日志清理需求。按时间清理核心参数是-mtime。-mtime 30表示找到修改时间超过 30 天前的文件精确一点说是 30×24 小时之前。注意这里是修改时间日志文件一旦被程序写入就会刷新 mtime所以持续写入的日志不会误删只有停止更新超过 30 天的文件才会被选中。-mmin可以按分钟单位精确到更短时间比如-mmin 1440等价于-mtime 1。按大小清理用-size。-size 100M表示 100MB 以上的文件。搭配-name可以锁死日志格式比如-name *.log或者-name app-*.log避免把非日志文件一并清掉。这三个参数单独用都没问题组合使用时要注意逻辑运算规则。find 的多个条件是与的关系也就是同时满足才会命中。如果想表达超过 30 天 并且 超过 100MB直接拼参数就是与逻辑。如果想表达超过 30 天 或者 超过 100MB需要加-o并用括号分组。这个细节我见过太多人踩坑后面会在常见问题里专门说。命令行示例就是find /var/log/nginx -name access.log-* -mtime 30 -type f -delete这条命令的意思是在/var/log/nginx下找文件名以access.log-开头、修改时间超过 30 天前、并且是常规文件-type f的文件然后直接删除。-type f必须加否则如果目录名恰好匹配会把目录删掉。2.2 清理时间粒度怎么定日志清理的时间参数不是随便拍的。核心原则是保留周期要覆盖住最长排查周期。比如线上故障排查可能需要回溯半个月前的日志业务审计要求保留三个月安全日志甚至要保留半年以上。实际确定保留周期时我会先问自己几个问题出问题时我需要看多久之前的日志通常 7~30 天够用。这个日志对应磁盘空间多大如果单日日志 500MB保留 30 天就是 15GB磁盘要扛得住。有没有合规要求涉及计费等场景建议保留更久。常用参考值系统日志保留 30 天Nginx 访问日志保留 30 天业务应用日志保留 7~14 天如果磁盘充裕可以 30 天审计类日志保留 180 天以上。清理天数在脚本里做成变量方便后续调整。2.3 保留文件数量和磁盘水位怎么控制只看时间有一个问题某天日志文件异常暴涨比如程序死循环写日志一天产生 20GB那么 7 天后这个文件还在磁盘可能早就爆了。所以健壮的清理脚本必须叠加大小或数量控制。-size 2G可以兜底超过 2GB 的日志不管几天前的都处理掉删除或清空。这里需要注意的是如果一个文件正在被程序写入直接rm会导致句柄泄漏——程序还在往已删除的文件里写磁盘空间不会释放得重启进程才释放。更稳妥的做法是先truncate -s 0清空文件内容让程序继续写空间立即释放。但只适用于正在写入的单个文件轮转后的历史日志直接删没问题。数量控制则用ls -t | tail -n $N这类管道实现比如保留最新 10 个日志文件其余删除。我的脚本里一般同时用时间和大小双重保险因为纯粹的保留 N 天策略在大日志场景下不够灵敏。3. 完整清理脚本实现与定时部署3.1 初版基线脚本按保留天数清理先给一套能直接落地的基础版本逻辑很简单遍历日志目录删除修改时间超过设定天数的文件。#!/bin/bash # 日志清理脚本 - 按天数清理版本 LOG_DIRS(/var/log/nginx /data/app/logs) RETENTION_DAYS30 for dir in ${LOG_DIRS[]}; do if [ -d $dir ]; then find $dir -type f -name *.log -mtime ${RETENTION_DAYS} -delete echo $(date %Y-%m-%d %H:%M:%S) cleaned $dir (${RETENTION_DAYS} days) else echo WARN: $dir not exists fi done这个版本简单可靠-delete删除文件-name *.log确保只操作.log后缀文件。运行一下会有类似输出2025-01-10 03:00:00 cleaned /var/log/nginx (30 days) 2025-01-10 03:00:00 cleaned /data/app/logs (30 days)直接执行可能没有输出因为没有文件被删这正常。想看实际删了什么可以先把-delete换成-print试运行一遍。注意这里我刻意不用rm -rf因为find -delete更安全。rm -rf配合变量一旦路径拼错很容易把系统目录干掉find -delete至少会严格限定在起始目录范围内。3.2 进阶版时间 大小 排除项日常用到最多的还是这个进阶版它整合了三种策略超期删除、超大文件清空、关键文件白名单保护。#!/bin/bash # 日志清理脚本 - 时间大小策略 # 用法: ./log_cleanup.sh [dry-run|real] MODE${1:-dry-run} # 默认试运行加上 real 才是真删 LOG_BASE_DIRS(/var/log/nginx /data/app/logs) RETENTION_DAYS30 MAX_FILE_SIZE2G PROTECT_FILES(access.log error.log) # 正在写入的当前日志文件 for dir in ${LOG_BASE_DIRS[]}; do [ -d $dir ] || { echo SKIP: $dir not found; continue; } # 策略1按时间清理过期日志 find $dir -maxdepth 2 -type f \ \( -name *.log -o -name *.log-* -o -name *.log.* \) \ -mtime $RETENTION_DAYS \ ! -name $(printf %s\n ${PROTECT_FILES[]} | sed s/[.[\*^$/]/\\/g | paste -sd |) \ -exec rm -f -- {} 2/dev/null # 策略2超大文件清空保留当前句柄 find $dir -maxdepth 2 -type f \( -name *.log -o -name *.out \) \ -size $MAX_FILE_SIZE -exec truncate -s 0 {} \; 2/dev/null done echo done at $(date %Y-%m-%d %H:%M:%S)这个脚本里最需要注意的点是truncate -s 0而不是rm。如果某个文件超过 2GB 且仍在被 Java 进程持续写入rm掉它会导致进程句柄泄漏磁盘空间不释放。truncate -s 0能把文件内容清空但保留文件句柄写入进程继续往这个文件里写空间立即释放而且日志文件还能保持可写状态。PROTECT_FILES是给当前正在写入的最新日志文件设例外。比如 Nginx 的access.log是当前文件每天轮转前的旧文件叫access.log-20250101。如果不保护access.log本身find -mtime 30同样可能把它删掉比如程序超过 30 天没写入。我在脚本里通过! -name排除。对于完全不需要保护文件名的场景可以去掉这行减少复杂度。3.3 清理 Nginx 日志轮转后的文件Nginx 属于比较特殊的一类因为生产环境通常配合 logrotate 做访问日志轮转。轮转后的文件命名可能是access.log.1、access.log.2.gz也可能带日期。如果你用我的查找表达式-name *.log.*能命中access.log.1这种但注意-name *.gz不是.log后缀需要单独处理。我的建议是Nginx 场景把清理规则分成两组# 删除普通历史日志 find /var/log/nginx -type f -name *.log.* -mtime ${RETENTION_DAYS} -delete # 删除压缩后的旧日志 find /var/log/nginx -type f -name *.gz -mtime ${RETENTION_DAYS} -delete注意access.log.1可能是二进制旋转产生的纯文本也可能已经是 gzip 压缩。无损材料在磁盘上没意义直接清理或压缩即可。如果磁盘紧张还可以搭配logrotate配置里的compress选项让历史日志自动 gzip能省 80% 左右空间。3.4 搭配 logrotate 处理系统日志/var/log/syslog、/var/log/auth.log这类系统日志不会自动滚动日期后缀会无限增长。系统自带的 logrotate 默认每晚执行一次但你必须在/etc/logrotate.d/下做自定义配置比如新建/etc/logrotate.d/myapp/data/app/logs/*.log { daily rotate 14 compress delaycompress missingok notifempty copytruncate dateext }这段配置的含义每天轮转一次保留 14 份历史文件超过的删除历史日志用 gzip 压缩delaycompress表示轮转后的第一个文件先不压缩因为可能还有进程未写完copytruncate是关键选项它先复制当前日志内容到新文件再清空原文件不影响正在写的进程适合 Nginx 和 Java 这种不重新打开日志文件的程序。logrotate 的执行方式本身不用你写脚本系统 cron 会在每天某个时间自动跑/etc/cron.daily/logrotate。你只需要保证配置文件语法正确并手动测试logrotate -d /etc/logrotate.d/myapp # 调试模式只看执行逻辑 logrotate -f /etc/logrotate.d/myapp # 强制轮转一次第一次配置 logrotate 特别容易忽略权限问题。logrotate 通常以 root 运行如果它轮转一个属于某应用用户的日志文件生成的轮转文件属主可能变成 root应用无法继续写。所以配置文件的 create 指令要显式指定用户和组例如create 0640 www-data adm。3.5 用 crontab 让清理脚本定时跑脚本写好了还必须自动化不然和手动删没区别。Linux 下最简单的定时执行就是 crontab。# 编辑当前用户 crontab crontab -e # 每天凌晨 2:10 执行清理脚本 10 2 * * * /opt/scripts/clean_logs.sh real /var/log/clean_logs.log 21cron 的格式是分 时 日 月 周。10 2 * * *表示每天 2:10。选凌晨执行是因为日志量相对低避免清理动作和高峰期写入冲突。末尾的重定向把脚本输出写到/var/log/clean_logs.log方便回看。real参数是我脚本里区分试运行和真删除的控制。第一次部署永远建议先 dry-run 跑几天确认清理结果符合预期再切到 real。你可以在 cron 里直接带 real也可以在脚本里把 MODE 默认值写成 real但第一次务必手动 dry-run。3.6 脚本权限与路径统一约定脚本落地前做三件事给定可执行权限、放到固定目录、统一日志输出。chmod x /opt/scripts/clean_logs.sh路径统一很重要。不同机器上日志目录可能不同脚本开头定义数组后续维护时只改目录列表、天数即可不需要动逻辑。我一般还会把脚本适配成同时支持手动指定目录的参数形式方便临时清理特定路径./clean_logs.sh real /some/custom/logdir这个 shell 脚本里加一个$2参数判断即可很小的改动但灵活性提升明显。还有一点如果脚本里用了相对路径或者环境变量比如 JAVA_HOMEcron 执行时的环境变量和手动执行不同脚本里所有路径尽量用绝对路径。4. 日志清理实操中的常见问题和排查方法4.1 find 多条件与逻辑表达这是最容易出问题的地方。find多个条件默认是与想表达或必须显式加-o而且括号要在 shell 里转义或直接用小括号配合find的 syntax。比如这个命令查找 7 天前 或 大于 100M 的日志文件。find /data/logs -type f \( -name *.log -o -name *.log.* \) \( -mtime 7 -o -size 100M \)不写-o的话会变成既 7 天前 又 大于 100M结果可能一个都删不了。另一种常见问题-name的匹配是文件名整体匹配不是子串包含。-name access*能匹配access.log和access.log.1但-name *.log*也能匹配.logrotate之类的文件所以要尽量把模式写精确。我习惯在关键命令上先试运行find /data/logs -type f -name *.log -mtime 30 -print如果输出列表没问题再把-print换成-delete。永远不要在不确定的情况下一上来就-delete或-exec rm。4.2 符号链接和时间戳的坑find默认不跟随符号链接-type f只匹配普通文件。如果日志目录通过 ln -s 软链接指向其他分区find /path/linkdir默认不会进入链接目标。这时候要么用-L跟到链接目标要么直接把真实路径写进脚本。我用后者因为软链接路径可能被误删或者指向改变。mtime 那个坑比链接更隐蔽如果你用mv或touch修改了一个日志文件的时间mtime 会变成当前时间导致-mtime 30认为它是新文件而不清理。比如某些日志凌晨会被 logrotate 复制并改名复制后的文件 mtime 是新时间原来时间点的判定就失效了。排查为什么某个大日志没被清理时优先ls -l --time-stylefull-iso看一眼真实时间。4.3 文件被占用导致磁盘空间不释放Java 应用和绝大部分服务进程在日志轮转或删除后文件句柄仍然指向已删除的 inode。此时df -h显示空间满但du -sh看目录却占用很小就是这个原因。处理方式# 找出占用已删除文件的进程 lsof | grep deleted # 列表中标记 deleted 的文件就是罪魁祸首 # 对应 PID 的业务进程如果允许重启重启后空间立刻释放更优雅的方案是前面提到的用truncate -s 0清空而不是删除。生产环境我通常是先用truncate处理大日志和当前日志再用rm清理已轮转的历史文件双保险。还有一点清理后记得看磁盘回收情况。du -sh和df -h都看一下确认空间真正释放了。有些文件系统删除文件后空间会稍有延迟才收敛。4.4 cron 环境与脚本输出问题很多人脚本手动执行一切正常放进 cron 却报错或没生效。常见原因有三类cron 的 PATH 环境极小脚本里的命令必须全路径或用绝对路径比如find写成/usr/bin/find或者开头先export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin。cron 输出默认发邮件如果不想收邮件在命令末尾加 /var/log/clean_logs.log 21把错误一并记录到文件。时区问题。服务器默认 UTC 的话cron 凌晨 2 点按 UTC 执行国内时间就是早上 10 点清理高峰可能和业务高峰撞上。部署前先date确认时区。我在脚本里加了一句环境初始化保证 cron 执行时也不缺这缺那export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin这行放在 shebang 下面一行简单有效。4.5 怎么验证清理效果脚本跑完验证结果的核心指标是磁盘占用趋势和清理命中数量。执行后先看 dfdf -h /var /data分别看/和挂载的数据盘。如果空间明显下降说明命中生效。再翻日记find /data/app/logs -type f | wc -l du -sh /data/app/logs文件数量和目录占用大小是最直观的验证。如果脚本删了 300 个文件但目录还是很大多半是当前日志文件超大或占用进程没释放。此时du -sh和lsof | grep deleted组合排查。4.6 紧急情况磁盘已经满了怎么办磁盘已经满时find命令可能没法正常执行因为系统连创建临时文件都困难。这时候先用最简单的命令释放一点空间# 查看最占空间的前 10 个日志文件 du -ah /var/log /data/logs 2/dev/null | sort -rh | head -20 # 临时清空最大日志文件安全不删除文件 truncate -s 0 /data/app/logs/application.logtruncate -s 0是为数不多在磁盘满时还能安全执行的操作。它不创建新文件、不消耗额外空间只是把文件内容清零。清空后进程继续写入容错性最好。等磁盘有富余了再去跑完整的清理脚本和 logrotate。执行完临时清空后建议立刻排查为什么日志会撑满磁盘是某程序死循环是日志级别配置太低还是保留天数太长把根因处理掉否则清理完过两天又满。5. 我对日志清理的几点经验5.1 保留时长宁多勿少但大小上限宁严勿松日志保留时间设太长会占磁盘太短又怕故障时找不到线索。我的经验是常规业务日志至少保留 7 天生产环境 30 天打底但单文件大小上限一定要设硬限制比如 2GB。时间宽松、大小严格的组合既能满足排查需求又能防住日志暴涨。5.2 先加日志后删日志这句话听着绕但其实很关键。任何清理脚本上线前先确认有执行日志可查。你删了哪些文件、什么时候删的、命中数量多少都要留痕。我把脚本输出统一写到/var/log/clean_logs.log每次 cron 执行都有记录出问题时能回溯到底执行了什么。清理动作本身如果没有日志出了误删事故连排查的抓手都没有这个教训值得重视。5.3 定期手动检查一次自动化不等于可以完全不管。我建议每两到三个月中旬手动跑一次df -h、看一眼清理日志确认脚本参数还适合当前业务量。业务量翻倍的时候原来 30 天 2GB 的策略可能需要缩到 14 天 1GB不主动调整的话磁盘还是会慢慢吃紧。我个人现在碰到新服务器第一步就是看有没有日志清理策略没有就先布一个。这件事花不到二十分钟但它替你守住磁盘的底线。希望这套脚本和思路对你也有用。