1. 日志清理这事,为什么能单独写一期
先说点实在的。服务器跑着跑着,磁盘满了,服务挂了,登录上去一看,/var/log/messages 好几个 GB,nginx 的 access.log 都大到用 cat 打开都卡半天的程度——这种场面,干过运维的都懂。
日志文件是典型的“平时没人管,出事要人命”的东西。它不会像业务代码那样在某个版本突然崩溃,而是日复一日、一点一点地蚕食磁盘空间。等你发现磁盘告警的时候,往往已经晚了半拍:数据库写不进数据了,应用报 No space left on device 了,甚至整个系统因为无法写入关键文件直接卡死。这时候你慌慌张张跑上去删几个大文件应急,表面上磁盘是腾出来了,但问题根本没解决——明天它还会满,后天它还会满,你总不能天天守在机器前面人工删吧?
所以,日志删除脚本就成了运维工具箱里的常备品。这期是“东方仙盟”系列的练气期内容,定位很明确:不讲那些花里胡哨的日志分析平台,也不谈 ELK 那套重量级方案,就踏踏实实把 Linux 环境下日志清理脚本怎么设计、怎么写、怎么部署、怎么避坑,从头到尾理一遍。适合刚接触服务器运维的初学者,也适合那些一直在手工删日志、想把自己从重复劳动里解放出来的半路出家运维。
这期的标题叫“日志删除实操脚本解析”,重点落在“实操”和“解析”四个字上。也就是说,不光是给你一个能跑的脚本就完事了,还会跟你解释每一个参数为什么这么写、每一段逻辑解决的是什么问题、哪几个地方是只有踩过坑的人才写得出来的。这样才能做到举一反三——理解了原理之后,不管你的业务日志放在哪个目录、叫什么名字、保留多少天,你都能自己改出一个符合需求的清理脚本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 写日志清理脚本之前,先搞清楚日志是怎么增长的
2.1 磁盘空间是怎么被日志吃掉的
很多新手对日志的膨胀速度没有概念。我举个例子:一个中等流量的 Web 服务,access.log 每天可能产生 500MB 到 1GB 的数据,这还只是访问日志。如果业务再把接口的输入输出参数打到 debug 级别的日志文件里,一天几个 GB 都很正常。
更隐蔽的是那些“不常看但一直写”的日志。系统自带的 cron 日志、内核日志、认证日志,单个文件可能不大,但架不住文件多、时间长。再加上很多应用自己维护一套日志目录,比如 Tomcat 的 catalina.out、Java 服务的 logs/ 目录、Nginx 的 logs/ 目录——每个目录下都是成堆的日志文件。这些日志如果没人管,最终结果就是磁盘空间被一点一点吃干抹净。
日志增长不是线性的,往往伴随着突发流量、程序报错循环、Debug 未关等问题出现陡增。比如某天代码出了个 bug,异常信息每秒打几百条,一个下午就能把几十 GB 的磁盘塞满。所以日志清理不能是“想起来才做”,必须形成机制、定期执行。
2.2 日志轮转(logrotate)和日志删除是两码事
聊日志清理,避不开 logrotate。这是 Linux 系统自带的日志轮转工具,它的工作原理是:定期把当前日志文件重命名、压缩、再新建一个空日志文件让程序继续写。比如 /var/log/messages 会被轮转成 messages.1、messages.2.gz 等,配合 rotate 参数控制保留多少份。
很多新人会有个误解:有了 logrotate,是不是就不用手动清理了?并不是。logrotate 解决的更多是“日志文件拆分和归档”的问题,它根据配置来决定保留几份历史日志,但默认配置往往只是针对系统日志。像 Nginx、Tomcat、业务应用等自定义日志路径,logrotate 默认并不覆盖,需要你另外写配置。就算配了 logrotate,一些应用因为日志文件句柄被占用,轮转之后日志继续往旧文件里写,导致磁盘空间还在涨,这时候依然需要脚本兜底。
所以一个成熟的日志管理方案,通常是 logrotate 做第一道轮转、脚本做第二道清理兜底。这期讲的脚本,偏重的就是把“过期日志”找出来并删掉这个过程,你可以把它看作日志管理里的最后一公里。
2.3 不清理日志到底会发生什么,说几个真实事故
我见过不止一次因为日志不清理导致的事故,简单说两个。
第一个是某台应用服务器,Java 服务把日志打到 catalina.out,这个文件默认是不轮转的,只要进程不重启就一直写。结果运行了大半年,文件涨到 40 多 GB,最终磁盘 100% 打满。更要命的是,因为系统无法写入任何新文件,SSH 登录都受影响,在线排查极其痛苦。最后是重启服务、日志文件被释放后才腾出空间,但服务中断了十几分钟,这个事故本来完全可以在半小时内用一条清理命令避免。
第二个是数据库服务器。MySQL 的错误日志和慢查询日志没做清理,磁盘满之后数据库直接拒绝写入,业务方那边立刻炸锅。这种场景最尴尬的是:数据库进程还活着,但已经不可用了,你只能冒着风险去删日志。如果在磁盘还没满的时候,日志清理机制就提前把过期数据删掉,后面这些事故都不会发生。
一句话总结:日志清理不是可有可无的优化项,而是服务器运维的保底操作,是必须要在上线清单里有的东西。
3. 日志删除脚本的设计思路与核心命令选择
3.1 这一步要解决什么问题
写脚本之前先想清楚需求。日志删除脚本的核心需求非常简单:找到指定目录下、符合条件(比如超过 N 天)的日志文件,然后删掉它们。
但需求往下拆,就没那么简单了:
- 哪些目录下的日志要清理?系统日志目录?应用日志目录?
- 匹配什么文件名模式?
*.log?*.log.*?access.log-20250101这种? - 保留多少天?7 天?30 天?90 天?
- 删掉之前要不要先压缩归档?
- 是按文件的最后修改时间判断,还是按文件名里的日期判断?
- 脚本是不是要支持手动指定目录和天数?
- 要不要考虑安全机制,防止误删?
想清楚这些问题,脚本写的才有章法。否则你随手写一条 rm -rf 套个 find,一旦路径写错、通配符写错,生产环境删错文件,那就是事故级别的问题。
3.2 主力命令:find 的用法与参数详解
日志清理脚本的最核心命令就是 find。它负责按条件搜索文件,是整套脚本的“眼睛”。
拿最基础的一条命令来说:
bash复制find /var/log -name "*.log" -type f -mtime +30 -exec rm -f {} \;
这条命令做了四件事:
find /var/log:指定从/var/log目录开始搜索。-name "*.log":匹配文件名以.log结尾的文件。注意这里用的是通配符,配合引号防止 shell 自作主张扩展。-type f:只匹配普通文件,这个条件很重要。不加的话,可能会把目录、软链接等也列出来,非常容易误删。-mtime +30:匹配最后修改时间在 30 天以前的文件。
接下来是 -exec rm -f {} \;:对每个匹配到的文件执行 rm -f,{} 是 find 找到的文件名的占位符,\; 表示命令结束。
find 的几个时间参数,这里值得展开讲一下,因为这是最容易用错的地方:
-mtime:按文件内容的最后修改时间过滤,是日志清理最常用的。-ctime:按文件状态改变时间过滤,比如权限变化、硬链接数变化也会触发,常用于排查文件被改动的情况。-atime:按最后访问时间过滤,对日志文件来说参考意义不大。-mmin:按分钟来过滤,适合调试的时候用。比如-mmin +30表示 30 分钟以前修改过的文件。
-mtime +30 的语义很微妙,它并不是“正好在第 30 天被修改”,而是“最后一次修改发生在 24 到 48 小时之前的那个整数天”。实际上,find 会自动把天数乘以 24 小时来换算,所以 -mtime +30 表示的是“最后修改时间距今超过 720 小时(30 天)”。如果你需要精确到“超过 29 天”这种边界值,直接用 -mtime +29 反而更接近直觉。
提示:这里又是初学者容易看不懂的点。
-mtime +30匹配的是“超过 30 天整”的文件,而不是“恰好 30 天前”。由于find是拿当前时间和文件时间做差再整除 24 小时,边界上会有 1 天的误差。如果要更精确,可以使用-mmin按分钟计算,或者用! -newermt "30 days ago"的方式。不过对日志清理这个场景来说,1 天的误差完全可以接受,没必要过度设计。
3.3 为什么不用 rm 直接删除,而是用 find + exec
新人最容易犯的错误是直接写 rm -rf /var/log/*.log 这种命令。看着简单,但有几个致命问题:
- 通配符
*可能匹配不到任何文件,这时 shell 会原样把*.log传给rm,导致删除失败或者报错,如果配合了-rf参数,危险系数直线上升。 - 日志文件可能不在当前这一级目录,而是在子目录里,通配符只能匹配一层目录,无法递归处理。
- 无法按时间条件筛选,做不到“只删 30 天前的”,会把所有的都干掉。
所以 find 在这种场景下几乎不可替代。它具备递归搜索的能力,支持多维度的条件筛选,还可以用 -exec 或 -delete 对匹配结果执行后续操作。用 find 先看再删、先列出再执行,是运维的稳妥做法。
find 还有一个比 -exec rm 更省事的方式,就是 -delete 参数:
bash复制find /var/log -name "*.log" -type f -mtime +30 -delete
-delete 直接从 find 内部删除文件,不需要再调用外部 rm 命令,效率更高。但它有个限制:只能用 -path、-mtime 这类“安全”的筛选条件,不能配合 -exec 之外的复杂动作,而且不加 -type f 的时候有把空目录也一起删掉的风险。所以在生产脚本里,我更习惯用 -exec rm -f 配合先干跑验证的方式,逻辑更直观、可控性更强。
3.4 脚本语言为什么选 Bash
这个问题几乎是送分题。日志清理是一个标准的系统批处理任务,Bash 脚本在 Linux 环境里有天然的优势:不需要额外安装解释器、语法简单、跟系统命令集成度高、可以直接挂在 crontab 里跑。相比 Python 或者 Perl,Bash 写这种任务脚本更轻量,部署成本几乎为零。
当然,如果你的清理逻辑特别复杂,比如要从数据库读取配置、要往消息队列发送通知、要做异常告警推送,那用 Python 写会更顺手。大部分运维环境里,Bash 仍然是此类脚本的主流选择,因为它足够简单,简单到崩溃后任何一个接手的人都能快速看懂并修改。
4. 一份可直接落地的日志清理脚本全解析
4.1 脚本骨架:变量、函数、主逻辑
直接上一份完整的脚本,然后一行一行拆开讲。这份脚本参考了我在生产环境验证过的写法,兼容性比较好,也留出了扩展空间:
bash复制#!/bin/bash
# ==============================================================
# 日志清理脚本 log_clean.sh
# 功能:删除指定目录下超过指定天数的日志文件
# 适用:Linux 全系发行版(RHEL / CentOS / Ubuntu / Debian)
# 版本:1.0
# ==============================================================
# 脚本中的变量定义
LOG_DIR="/var/log"
KEEP_DAYS=30
LOG_NAME_PATTERN="*.log"
DRY_RUN=${DRY_RUN:-true} # 默认开启试运行,确认无误后改为 false
# 颜色输出(方便阅读执行结果)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
# 日志输出函数,统一带时间戳
log_info() {
echo -e "${GREEN}[INFO]$(date '+%Y-%m-%d %H:%M:%S') $*${NC}"
}
log_warn() {
echo -e "${YELLOW}[WARN]$(date '+%Y-%m-%d %H:%M:%S') $*${NC}"
}
log_error() {
echo -e "${RED}[ERROR]$(date '+%Y-%m-%d %H:%M:%S') $*${NC}"
}
# 清理函数
# 参数 1:目标目录
# 参数 2:保留天数
# 参数 3:文件名模式
clean_logs() {
local dir="$1"
local days="$2"
local pattern="$3"
if [ ! -d "$dir" ]; then
log_error "目录 $dir 不存在,跳过"
return 1
fi
log_info "开始清理目录 $dir 中超过 ${days} 天的 $pattern 文件"
# 第一步:列出符合条件待删除的文件
local file_list
file_list=$(find "$dir" -type f -name "$pattern" -mtime "+${days}" 2>/dev/null)
if [ -z "$file_list" ]; then
log_warn "目录 $dir 下没有符合清理条件的文件"
return 0
fi
# 第二步:逐个遍历删除
local count=0
local freed_space=0
while IFS= read -r file; do
# 统计文件大小
local size
size=$(stat -c%s "$file" 2>/dev/null || echo 0)
freed_space=$((freed_space + size))
if [ "$DRY_RUN" = "true" ]; then
log_info "[试运行] 待删除文件: $file (大小: $size 字节)"
else
rm -f "$file" && log_info "已删除: $file (释放: $size 字节)"
fi
count=$((count + 1))
done <<< "$file_list"
if [ "$DRY_RUN" = "true" ]; then
log_info "试运行结束,共 $count 个文件待删除,预计释放空间: $((freed_space / 1024 / 1024)) MB"
else
log_info "清理完成,共删除 $count 个文件,释放空间: $((freed_space / 1024 / 1024)) MB"
fi
return 0
}
# 主逻辑
main() {
log_info "===== 日志清理脚本开始执行 ====="
# 可以在这里添加多个调用,每个目录一行
# 注意:从安全角度考虑,每个目录独立执行清理,避免一个出错影响其它
clean_logs "$LOG_DIR" "$KEEP_DAYS" "$LOG_NAME_PATTERN"
clean_logs "/var/log/nginx" "$KEEP_DAYS" "*.log"
clean_logs "/home/app/logs" "$KEEP_DAYS" "*.log.*"
log_info "===== 日志清理脚本执行结束 ====="
}
# 执行入口
main "$@"
这份脚本在结构上做了四个分层:全局变量、输出函数、清理函数、主逻辑。每一层各司其职,后续要改目录、改保留天数、改匹配规则,都只需要动顶部变量或者在 main() 里加一行调用,可维护性很好。
4.2 关键代码段逐行拆解
这里挑几个最关键的部分来讲,没有含糊的空间。
文件列表的收集用命令替换
bash复制file_list=$(find "$dir" -type f -name "$pattern" -mtime "+${days}" 2>/dev/null)
这里把 find 的结果赋值给变量 file_list,后续用 while read 循环遍历。2>/dev/null 是把 find 的报错信息(比如目录无权限)吞掉,避免脚本执行过程中产生一堆噪音输出。这个 2>&1 重定向的知识点,Linux 命令大全里常提,但真正在脚本里用对地方的人不多。
遍历文件的方式用 while read 而不是 for
bash复制while IFS= read -r file; do
这里用 while IFS= read -r file 而不是 for file in $file_list,有两个原因:
- 如果文件名包含空格,
for循环会把一个文件名拆成多个词,导致误删。虽然日志文件名带空格的情况很少,但兼容性这块不该省。 read -r能防止反斜杠等特殊字符被转义。IFS=是告诉 read 不要修剪行首行尾的空白字符。这些都是 Shell 脚本入门时不太会讲、但生产环境真正用得上的细节。
文件大小统计用 stat
bash复制size=$(stat -c%s "$file" 2>/dev/null || echo 0)
stat -c%s 可以取到文件大小,单位是字节。如果文件在 find 列出后又被并发任务删掉了,stat 会报错,后面的 || echo 0 就会兜底返回 0,避免脚本因为报错中断。
试运行开关 DRY_RUN
这个设计我在很多脚本里都会加。默认 DRY_RUN=true,脚本只会列出来“如果要删会删哪些文件”,但不会真正动手。等人工确认没问题了,再把它改成 false 跑一遍。生产环境做变更,先试跑再执行,看着多一道手续,其实是在帮你挡掉误删的大坑。
4.3 脚本的变量设计与扩展思路
脚本顶部的三个变量是整套脚本的“旋钮”:
bash复制LOG_DIR="/var/log"
KEEP_DAYS=30
LOG_NAME_PATTERN="*.log"
想清理别的目录,改 LOG_DIR;想改保留周期,改 KEEP_DAYS;想匹配不同文件名,改 LOG_NAME_PATTERN。如果你的环境里,不同目录要保留的天数不一样,完全可以在 main() 里把参数显式传进去,比全局变量更灵活:
bash复制clean_logs "/var/log/nginx" 14 "*.log"
clean_logs "/data/applogs" 60 "app.log-*"
这种写法,等于把“清理指定目录下超期日志”这件事封装成一个函数,之后想加清理任务就加一行调用,不需要复制一大段代码。这种逐步演进的脚本设计思路,也是从练气期走向筑基期、金丹期的必经之路。
5. 实操演示:怎么验证、怎么部署、怎么定时执行
5.1 先造一批测试数据
写完了脚本先别慌着上生产,先在测试环境或者临时目录里验证。最简单的办法,是自己造一批不同时间戳的日志文件,验证脚本是不是真的能按时间把它们筛出来。
我建议这样操作(注意是在临时目录里,别一上来就拿 /var/log 开刀):
bash复制# 建立测试目录
mkdir -p /tmp/log_test
# 创建 10 个 .log 文件
for i in $(seq 1 10); do
touch -d "$((30 + i)) days ago" /tmp/log_test/test_$i.log
done
# 再创建几个近期的
for i in $(seq 1 3); do
touch /tmp/log_test/recent_$i.log
done
这里用了 touch -d "N days ago" 来把文件修改时间设置成 N 天以前,快速模拟出历史上不同时间的日志文件。测试的时候可以临时把 KEEP_DAYS 改成 20、30 等不同的值,看看 find 的筛选结果是否符合预期。
5.2 试运行模式验证
把脚本里的 DRY_RUN 保持为 true,先执行一次:
bash复制bash log_clean.sh
正常输出会是类似这样的:
code复制[INFO]2025-01-15 14:30:01 ===== 日志清理脚本开始执行 =====
[INFO]2025-01-15 14:30:01 开始清理目录 /var/log 中超过 30 天的 *.log 文件
[WARN]2025-01-15 14:30:01 目录 /var/log 下没有符合清理条件的文件
[INFO]2025-01-15 14:30:01 开始清理目录 /var/log/nginx 中超过 30 天的 *.log 文件
[WARN]2025-01-15 14:30:01 目录 /var/log/nginx 下没有符合清理条件的文件
[INFO]2025-01-15 14:30:01 开始清理目录 /home/app/logs 中超过 30 天的 *.log 文件
[WARN]2025-01-15 14:30:01 目录 /home/app/logs 下没有符合清理条件的文件
如果某次执行环境里真的有符合条件的日志,试运行模式会列出所有带删除的文件、大小和预计释放的空间,但不会真正删除。这个输出天然就是一次“变更演练”,建议每次新增清理目录或者调整保留天数时,都先跑一遍试运行,确认无误再关掉开关正式执行。
5.3 正式执行
确认试运行结果符合预期后,把脚本里的 DRY_RUN 改成 false:
bash复制DRY_RUN=false bash log_clean.sh
或者直接编辑脚本文件,把默认值改成 false。执行后看到“已删除”和“释放空间”的相关日志,就说明清理逻辑是通的。
注意:第一次在生产环境执行日志删除脚本之前,建议先备份一份脚本到安全位置,同时确认当前 shell 环境没有
alias rm='rm -i'这类交互式别名,免得脚本卡在“是否确认删除”的提示上,导致无法自动运行。
5.4 挂到 crontab 定期执行
日志清理脚本的价值在于定期自动执行,而不是每次手动跑。Linux 下的定时任务,标准方案就是 crontab。
打开当前用户的 crontab:
bash复制crontab -e
加入一行,比如每天凌晨 3 点执行一次:
cron复制0 3 * * * /usr/local/bin/log_clean.sh >> /var/log/log_clean.log 2>&1
这里有几层意思:
0 3 * * *表示每天 3 点整执行。/usr/local/bin/log_clean.sh是脚本的绝对路径。crontab 里的 PATH 经常和交互 shell 不同,所以脚本内尽量不要依赖相对路径,命令也尽量写绝对路径。>> /var/log/log_clean.log 2>&1是把脚本的标准输出和错误输出都追加到日志文件里,方便事后追溯执行情况。
等 crontab 里的这条任务跑过一轮,记得去检查 /var/log/log_clean.log 里面的输出。定时任务里最容易出现的问题,就是脚本明明配了但没有执行权限、或者脚本里的命令在 crontab 的 PATH 里找不到。
6. 定时任务的执行细节与 crontab 环境陷阱
6.1 crontab 环境差异是新手重灾区
crontab 里执行脚本,环境变量跟手动执行时有很大差别。手动在终端里跑 bash log_clean.sh,会加载 /etc/profile、~/.bashrc 等环境配置文件;但在 cron 环境里,这些文件基本不会被加载。直接表现是:
PATH变量可能只有/usr/bin:/bin,如果你的脚本用到/usr/local/bin下的命令,会报command not found。- 某些环境变量(比如
JAVA_HOME、PYTHONPATH)为空,可能导致脚本依赖的运行时缺失。
规避方法很简单:
- 脚本里用到命令尽量写绝对路径,比如
/bin/rm、/usr/bin/find。 - 或者在脚本开头显式重新定义 PATH:
bash复制export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
这一步看着繁琐,但在生产环境的 cron 任务里属于常规操作。很多脚本“手动执行一切正常,一挂到 crontab 就各种报错”,十有八九就是环境变量没设置对。
6.2 执行权限和文件属主要确认好
如果你把脚本放在 /usr/local/bin/log_clean.sh,要给脚本加执行权限:
bash复制chmod +x /usr/local/bin/log_clean.sh
否则 crontab 调用脚本时会提示 Permission denied。另外,脚本的所有者也要注意。如果 crontab 是 root 用户的,那脚本就归 root 执行,目录访问权限基本不是问题;如果是普通用户的 crontab,脚本能访问的路径就要受限于该用户的权限,可能清理不了 /var/log 这种需要 root 权限的目录。
6.3 定时任务执行频率怎么定才合理
日志清理的频率没有标准答案,但有一个原则:清理频率要匹配日志增长速度。
日志量大的系统,建议每天执行一次,甚至一天多次。日志量小、磁盘空间充裕的系统,每周执行一次也够用。我见过有些团队偷懒,只在磁盘告警时才手动跑一下脚本,这种做法治标不治本。定时任务的成本极低,把它设成每天凌晨执行,几乎不会对系统性能造成什么影响。
有的生产环境为了错峰,会把清理任务安排在业务低峰期,比如凌晨 2-4 点。这个可以结合业务情况自行决定,没有绝对的好坏。
7. 常见问题与排查技巧实录
7.1 为什么脚本明明跑了,日志文件却没被删掉
这是日志清理脚本最常见的困惑,我把它拆成几个根因来排查:
第一个原因:find 的条件没匹配上。最常见的坑是 -mtime +30 的语义理解偏差。如果文件最后修改时间是“恰好 30 天前”,find 在计算的时候可能会有 1 天的舍入误差,导致文件没被选中。这时候可以把保留天数改成 29 或者用 -mmin 来调试验证。
第二个原因:脚本的执行用户没有目录权限。如果你的 crontab 是普通用户,但要清理的日志目录属于 root 或者其他用户,find 会因为没有读权限而找不到文件,即使找到了也无法删除。查看脚本执行日志,如果发现 find 报错,多半就是这个原因。
第三个原因:文件在被删除后又被重建。这种情况发生在日志文件被进程持续占用的场景。你用脚本删除了日志文件,但写日志的进程仍然持有那个文件句柄,日志会继续写入到已经被 “删除” 的 inode 上,磁盘空间并不会真正释放。这种问题在 Java 应用的 catalina.out 上非常常见。解决思路是:删除后用 > /path/to/log 重新创建一个空文件并让进程继续写,或者重启应用进程。
7.2 日志文件被进程占用,删了不释放空间怎么办
这个问题值得单独拎出来讲,因为它不是脚本写错,而是 Linux 文件系统的经典机制:进程持有文件句柄时,删除文件只是删除了目录项,文件占用的磁盘空间要等句柄关闭后才释放。
实际场景是这样的:catalina.out 一直有 Java 进程在写,你用 rm 删了它,df -h 看到磁盘空间并没有变化,因为 Java 进程还握着那个文件的 fd(文件描述符)。想要确认,可以用 lsof | grep deleted 查看哪些已删除的文件仍然被进程占用:
bash复制lsof | grep deleted
这种被占用日志的清理,更稳妥的做法是用 cat /dev/null > /path/to/log 这种方式“清空文件”而不是“删除文件”。清空操作不会删除目录项和句柄,进程可以继续往同一个文件里写,磁盘空间也会立刻释放:
bash复制> /var/log/tomcat/catalina.out
所以这类日志,脚本里不应该用 rm,而应该用 truncate 的方式。后面我会专门讲一下兼容这种场景的脚本写法。
7.3 find 命令报错:Permission denied
find 在执行时如果遇到没有权限的子目录,会持续输出 Permission denied 的错误。脚本里虽然加了 2>/dev/null 把这些错误吞掉了,但你可能还是想知道哪些目录是无权限访问的。排查的时候,可以临时去掉重定向,或者用 find ... -print 结合 2>&1 人工检查。
生产环境出现这种情况,优先确认脚本是以哪个用户身份执行的。如果清理目标是 /var/log 下的一些系统日志,普通用户确实没有权限,这时要么改用 root 用户跑 crontab,要么给脚本配置 sudo 权限。
7.4 误删了重要日志怎么办
日志清理脚本最让人紧张的就是误删。比如 find 的 -name 条件写得太宽,把业务的数据文件也当作日志删掉了。这种情况一旦发生,慌是没有用的,先做紧急止损:
- 第一时间用
lsof | grep deleted看看还有没有进程持有被删文件的句柄,如果有,可以尝试从/proc/<pid>/fd/下恢复文件内容。 - 如果有定期备份,从未备份恢复。
- 如果都没有,那就只能接受现实了——但也说明你的日志清理流程缺了一个前置步骤:确认脚本匹配到的文件确实是日志文件。
这也是为什么我一直强调,脚本发布前要用试运行模式先验证几轮,以及在 find 的匹配条件里,尽可能用更精确的文件名模式,避免太宽泛的 * 通配符。
7.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 脚本执行后没有文件被删除 | -mtime 边界值误差,恰好卡在临界天 |
调大保留天数或用 -mmin 验证 |
合理设置保留天数,比如 30 天用 -mtime +29 |
| 磁盘空间没有释放 | 日志文件被进程占用 | lsof | grep deleted 确认 |
用 truncate 清空文件代替删除 |
| crontab 执行报 command not found | cron 环境 PATH 与 shell 不一致 | 手动执行脚本看报错 | 脚本内显式定义 PATH,或命令写绝对路径 |
| crontab 执行提示 Permission denied | 脚本没有执行权限 | ls -l 查看权限 |
chmod +x 添加执行权限 |
普通用户无法删除 /var/log 下日志 |
权限不足 | 查看脚本执行日志 | 调整 crontab 用户或使用 sudo |
| find 输出大量 Permission denied | 目录访问权限受限 | 临时去掉 2>/dev/null |
明确执行用户权限范围 |
8. 进阶:从只删文件到优雅处理被占用的日志
8.1 高危场景:catalina.out 这类文件怎么清
前面提到,对于被进程占用的日志文件,直接 rm 并不能释放磁盘空间。所以要有一个专门处理这类文件的策略:对占用的日志,用 truncate 而不是 rm。
在 Bash 里清空一个文件最简洁的方式是:
bash复制: > /var/log/tomcat/catalina.out
或者写得更直白一点:
bash复制truncate -s 0 /var/log/tomcat/catalina.out
两条命令作用都是把文件内容清空,但文件本身、inode、文件句柄都还在,所以 Tomcat 还能继续往这个文件里写日志,磁盘空间则立刻被释放。
8.2 给脚本加上“删除前检查文件大小”的逻辑
前面提供的脚本,是“只要超过 N 天就删”。但在一些场景里,你可能会希望在满足时间条件的基础上,再加一层判断:文件太大也先处理。比如一个 .log 文件昨天刚轮转过,但今天又涨到几个 GB,超过 30 天的是它的旧备份,明天清理也来得及;但如果直接按天删除,可能刚轮转的文件还没到时间,反而把近期的日志误删了。
改进的方法是,在删除条件里加上“文件大小超过某个阈值”的判断:
bash复制find "$dir" -type f -name "$pattern" -mtime "+${days}" -size +100M -exec rm -f {} \;
-size +100M 的意思是只处理大小超过 100MB 的文件。这个参数在清理大文件场景里很实用,相当于多了一道保险,避免删掉虽然时间久但体积很小的关键小日志。
8.3 先压缩再删,保留一条退路
删除日志文件意味着数据永久丢失。如果你对数据还存有一丝疑虑,一个比较稳妥的策略是:超过保留天数的日志,先压缩归档到另一个目录,再过一段时间才真正清除。
具体做法是把清理逻辑拆成两步:
- 第一步:把超过 30 天的
.log文件gzip压缩成.log.gz。 - 第二步:把超过 90 天的
.log.gz文件删除。
这样就把“短期可查”和“真正释放空间”两个目标解耦了,数据安全性更好,但代价是需要额外的磁盘空间来存放压缩包。这个方案在日志量大的系统上要考虑磁盘成本,日志量小或者不那么敏感的业务,直接一步删也可以。
8.4 处理日志目录里除 .log 之外的各种变体
实际环境中,日志文件名千奇百怪:
access.logaccess.log-20250101app.log.2025-01-01catalina.2025-01-01.logerror_20250101.lognginx.access.log.gz
如果你的脚本只匹配 *.log,很多变体日志根本不会被处理。更稳妥的做法,是用更宽泛的模式匹配,比如:
bash复制find "$dir" -type f -name "*.log*" -mtime "+${days}" -exec rm -f {} \;
注意:*.log* 会把 access.log、access.log-20250101、access.log.gz 都匹配进来,覆盖面更全,但同时也会把一些“名字里带 log 但不是日志文件”的东西误伤。所以还是建议在实际环境里先 find 出来看看匹配范围,确认没有异常文件再执行删除。
9. 我踩过的几个坑,和后来养成的习惯
9.1 第一坑:写脚本不设试运行开关,上线第一次就跑偏
我最早写清理脚本的时候,没有试运行这个概念,觉得“写个 find 加 rm 就完事了”。结果有一次 find 的目录路径写错了一个字母,把另一个业务目录里的日志文件全删了,好在那个目录里的日志不是核心数据,事后恢复起来差点没折腾死人。
从那以后,我的所有删除类脚本里都保留了 DRY_RUN 这个开关,默认开、先跑一遍白名单确认,再关掉执行。这个习惯现在也推荐给团队里的每一个新运维,成本极低,收益极大。
9.2 第二坑:没有给脚本执行过程留日志
有一阵子,脚本虽然挂上了 crontab,但输出都被直接丢弃了。结果就是“脚本到底有没有执行、删了多少文件、有没有报错”,全靠猜。等磁盘又满了才发现,脚本因为某个目录权限问题已经静默失败一周了。
后来给 crontab 里的调用统一加了日志重定向:
cron复制0 3 * * * /usr/local/bin/log_clean.sh >> /var/log/log_clean.log 2>&1
每次执行都会留下痕迹,要追溯的时候 grep 一下 [INFO]、[WARN]、[ERROR] 就能看到完整的执行过程。一个脚本如果连自己的运行日志都不写,那跟“裸奔”没有区别。
9.3 第三坑:用 for 循环遍历 find 结果,遇到带空格文件名直接翻车
前面提到过,for file in $(find ...) 在处理带空格的文件名时会把一个文件拆成多个词。有一次测试环境里恰好有个日志文件叫 error 2025-01-01.log,脚本执行后直接把 error 当成了要删的文件,把 2025-01-01.log 当成了另一个文件,误删路径还不一样。排查了一下午才定位到问题。
从那以后,涉及文件遍历的脚本,我统一用 while IFS= read -r 循环,不用 for。这个习惯在 shell 脚本入门教程里可能不会重点讲,但实用性极强。
9.4 第四坑:裸跑 rm -rf,没加 -- 或者没用 -f
有些脚本新手喜欢直接写:
bash复制rm -rf /var/log/$pattern
如果 $pattern 是空的,这个命令就变成了 rm -rf /var/log/,后果不用我说,任何人都无法承受。而加上 find 后,即使匹配条件为空,也不会执行删除操作;再加上试运行开关,基本杜绝了这种“灾难性误删”。
提示:这是拿命换来的经验。在脚本里,凡是涉及删除的命令,至少要做到三件事:一是用
find先筛选,二是用变量参数控制目录和条件,三是保留试运行开关。三者缺一不可。
10. 最后补充一个稳定运行的细节
脚本写完之后,建议顺手加一个“脚本自身状态检查”的机制。最简单的方式,是在脚本执行结束时输出一条退出状态码,方便外部监控采集:
bash复制rc=$?
if [ $rc -eq 0 ]; then
log_info "脚本执行成功,退出码 $rc"
else
log_error "脚本执行失败,退出码 $rc"
fi
exit $rc
如果公司内部有监控告警系统,甚至可以把这个脚本的执行结果直接上报。对一个长期稳定运行的定时任务来说,它能否持续正常工作,和它本身清理日志的效果同样重要。一个静默失败的清理脚本,跟没有清理脚本没什么两样。
日志清理这个事,技术含量不算高,但涉及删除操作、涉及磁盘空间、涉及业务连续性,必须把每个细节都打磨到位。理解了 find 的参数用法、熟悉了脚本的调试步骤、掌握了 crontab 的部署方式,再把自己的脚本从简单粗暴的“rm -rf”逐步演进到带开关、带日志、带异常处理的生产级脚本,这个过程本身就是从练气期往筑基期迈进的路上,非常扎实的一步。
