做服务器运维的,谁还没被磁盘打满吓过几回。我印象最深的是一次周五晚上,项目群里突然有人喊游戏登录不上,后台一查,磁盘直接100%,数据库写不进去,登录接口全线超时。排查了半天,罪魁祸首就是 /var/log 下面几个月没管的日志,光一个 access.log 就占了快 200G。从那次之后,我把日志清理这件事正经提上了日程,写了套删除脚本一直跑到现在,再没因为日志把磁盘塞满过。这篇就把我当时的完整思路、脚本解析和踩过的坑整理出来,给和我一样刚接触 Linux 运维、还处在“练气期”的朋友做个参考。
这篇文章适合谁看?简单说就是三类人:一是刚接手项目服务器、发现磁盘使用率天天告警的运维新手;二是想写个日志清理脚本但不知道从哪下手、怕写错把数据删了的同学;三是已经在用 logrotate 但遇到各种奇怪问题、想找个更可控方案的人。我会把脚本逐行拆开讲清楚,包括常用的 find 参数、脚本保护机制、定时任务配置和网上很少有人讲的坑,你照着抄基本就能用。
1. 日志是怎么一步步把磁盘吃满的
先说个很多人容易忽略的事实:日志不是“偶尔”把磁盘填满的,只要服务在线,它就在稳定增长。项目里的日志来源非常多,Nginx 访问日志、Java 应用的 log4j 输出、Python 进程的 print 重定向、数据库慢查询、系统自带的 /var/log/messages,哪个进程日志写疯了,都能在一夜之间把一块 100G 的数据盘塞得满满当当。
我那次事故的具体原因也很有代表性:某个后台服务改了日志级别,从 info 调成了 debug,结果线上用户一多,异常堆栈和调试信息像瀑布一样往日志文件里刷,一天写了 60G。这个场景你经历一次就永远忘不掉——半夜两点手机告警短信狂响,ssh 上去敲 df -h 一看,/ 分区已经 98% 了,再用 top 一查,所有依赖磁盘写入的服务都在卡死边缘。
1.1 磁盘告警后的第一波排查
遇到磁盘快满,我的排查顺序基本是固定的,这里也分享给各位:
df -h先看是哪个分区满了,是系统盘/还是数据盘/data。du -sh /var/log看日志目录总大小,如果项目有专门的日志盘/日志目录,直接du -h --max-depth=2 /var/log 2>/dev/null | sort -rh | head -20定位大目录。- 锁定到具体的大文件之后,再用
ls -lh看文件大小、stat看最后修改时间,判断这个文件是不是还在持续写入。 - 如果发现文件已经被删了但磁盘空间没释放,用
lsof +L1 | grep deleted找一下是哪个进程还持有句柄。这个场景很经典,后面我会单独讲。
这套排查三板斧看着简单,但关键时刻能救命。我建议每个运维都把这几个命令练熟,不要等到磁盘 100% 才慌慌张张去百度。
1.2 日志清理思路要在出问题之前定好
那次事故处理完,我做的第一件事不是去研究什么高深技术,而是坐下来想一个问题:日志到底要留多久、怎么删才安全?
这个问题的答案没有标准,跟业务类型强相关。游戏项目的日志,通常需要保留一段时间来排查玩家反馈的问题,但也不需要留一年,半个月到一个月的量级就足够了。我当时按磁盘容量倒推算了一笔账:项目一天产生大概 1G 日志(这是根据 nginx 访问量和应用日志量粗估的),磁盘剩余 100G,那保留 30 天大约占用 30G,完全可控,还能留出余量给数据库 binlog 和其他临时文件。于是我就把“保留 30 天”定成了清理阈值。
这里有个经验要说一下:不要等磁盘报警了才去清,更不要手动 rm -rf 一些看着不顺眼的日志文件。日志清理最好做成一个自动化的、可追溯的、安全兜底的机制,这就是我下面要说的脚本方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 日志清理方案选型:为什么最后用自研脚本
想解决日志清理,最“正规”的方案其实是 logrotate,但实际跑过生产的同学都知道,logrotate 配置说简单也简单,说麻烦也很麻烦。我在当时那个项目里没有直接用 logrotate,而是选择自研脚本,主要基于三点现实原因。
2.1 logrotate 与自研脚本怎么选
先做个小对比,你就能明白我的选择逻辑:
| 维度 | logrotate | 自研清理脚本 |
|---|---|---|
| 配置复杂度 | 每个应用都要写配置,日志路径、轮转大小、保留份数都要单独调 | 只需要维护一个 bash 脚本,改起来很直观 |
| 对历史遗留路径的适配 | 项目日志分散在 /var/log、/data/logs、/home/project/logs,需要配置很多规则 |
脚本里用一个变量把所有目录统一列出来就行 |
| 删除策略控制 | 按大小/日期/份数控制,比较标准但灵活度低 | 用 find 参数可以精确到“按修改时间”“按文件名匹配”“按 0 字节判断”,更可控 |
| 新手学习成本 | 要理解 daily/daily、rotate、copytruncate、compress 这些概念 | 只要会写 shell 和 find,基本没有额外成本 |
实话实说,logrotate 在成熟业务里是更主流的选择,尤其是针对单个应用做日志切割,它非常优雅。但如果你像我一样,接手的是一个日志路径一堆、格式一路放飞的项目,自研脚本就是更快的解法。尤其在“练气期”阶段,一个能看懂、能随时改的脚本,比一个封装完好的工具更让我有安全感。先求可控,再求优雅。
2.2 删除策略的核心:按修改时间而不是按文件名
定删除策略时,很多人第一反应是“按文件名里的日期删”——比如删掉 access.log.20241201 这种带日期的文件。这个思路能跑通,但不通用。很多应用日志文件是固定名字,比如 app.log,里面内容一直追加,没有日期后缀;就算有日期后缀,文件名里的日期和文件实际最后修改时间也未必一致,如果某个日志文件 12 月 1 日之后就没再写过,但文件名还是 access.log.20241201,按文件名删就有误差。
我最后定的策略很简单:用文件 mtime(修改时间)来判断,删除超过 N 天未被修改的 *.log 和 *.out 文件。一个日志文件超过 30 天没有被任何进程写入,说明对应的业务早就没在用了,删掉的风险极低。逻辑清晰,服务端也认这套。
3. 日志删除脚本逐段拆解
脚本我写过好几个版本,下面这个是我后来稳定在用的,注释写得比较全,也加了保护机制。第一次接触 find 删除的朋友可以先拿它练手,跑熟了再按自己需求改。
3.1 完整脚本先放出来
bash复制#!/bin/bash
# clean_old_logs.sh
# 用途:清理指定目录下超过保留天数的 *.log / *.out 文件
# 安全设计:默认 DRY_RUN(只打印不删除),加 --exec 才会真正执行
set -u
SCRIPT_NAME="clean_old_logs"
LOG_DIRS="/data/logs /var/log/myapp /home/project/logs" # 按实际项目改
DEFAULT_KEEP_DAYS=30
KEEP_DAYS="$DEFAULT_KEEP_DAYS"
DRY_RUN=1
CLEAN_LOG="/var/log/clean_old_logs.log"
LOCK_FILE="/tmp/${SCRIPT_NAME}.lock"
# 简单参数解析
for arg in "$@"; do
case "$arg" in
--exec) DRY_RUN=0 ;;
--days=*) KEEP_DAYS="${arg#*=}" ;;
*) echo "未知参数: $arg"; exit 1 ;;
esac
done
# 校验保留天数是纯数字
if ! [[ "$KEEP_DAYS" =~ ^[0-9]+$ ]]; then
echo "保留天数必须是数字: $KEEP_DAYS"
exit 1
fi
# 防重复执行
if [ -f "$LOCK_FILE" ]; then
old_pid=$(cat "$LOCK_FILE" 2>/dev/null)
if kill -0 "$old_pid" 2>/dev/null; then
echo "[$(date '+%F %T')] 已有清理任务在跑(PID $old_pid),退出"
exit 0
else
rm -f "$LOCK_FILE"
fi
fi
echo $$ > "$LOCK_FILE"
# 核心清理函数
do_clean() {
local dir="$1"
if [ ! -d "$dir" ]; then
echo "[SKIP] 目录不存在: $dir"
return
fi
echo "[INFO] 扫描目录: $dir (保留 ${KEEP_DAYS} 天)"
find "$dir" -type f \( -name "*.log" -o -name "*.out" \) \
-mtime +"$KEEP_DAYS" -print | while read -r f; do
if [ "$DRY_RUN" -eq 1 ]; then
echo "[DRY_RUN] 待清理: $f"
else
echo "[DELETE] $f" >> "$CLEAN_LOG"
rm -f "$f"
fi
done
}
echo "[$(date '+%F %T')] 清理开始,模式: $([ $DRY_RUN -eq 1 ] && echo DRY_RUN || echo EXEC)"
for dir in $LOG_DIRS; do
do_clean "$dir"
done
rm -f "$LOCK_FILE"
echo "[$(date '+%F %T')] 清理结束"
使用方法很简单:
bash复制# 第一次先试运行,只看哪些文件会被清理
chmod +x clean_old_logs.sh
./clean_old_logs.sh
# 确认列表没问题后,再真正执行
./clean_old_logs.sh --exec
# 想临时只删 15 天前的,用这个
./clean_old_logs.sh --days=15 --exec
这个脚本的核心逻辑就一句话:在指定的几个目录里,找到修改时间超过 N 天的日志文件,打印出来,确认后再删除。 但细节是比较多的,下面逐段讲。
3.2 参数详解:find 的四个关键点
先看那段最核心的 find 命令:
bash复制find "$dir" -type f \( -name "*.log" -o -name "*.out" \) -mtime +"$KEEP_DAYS" -print
这四个点必须理解到位,不然很容易写出“能跑但会删错”的脚本:
-type f:只处理普通文件,绝对不会误删目录。如果你不加这个,find 会把匹配到的目录也列出来,后面如果用的是rm -rf就等着哭吧。\( -name "*.log" -o -name "*.out" \):匹配多个后缀。注意括号在 shell 里要转义,-o表示“或”,不加括号的话,-mtime会只作用于最后一个-name,逻辑直接错掉。这是 find 命令最经典的坑之一。-mtime +"$KEEP_DAYS":表示“修改时间超过 N 天”。注意+30和30的含义不一样:+30是严格大于 30 天(准确说是 30×24 小时之前),30是匹配 30~31 天这个区间,-30是 30 天以内。写的时候一定要带+。-print:先只打印,不删除。配合脚本默认的DRY_RUN=1,保证你第一次跑的时候只是“看看”,不会直接下手。
关于文件名通配符,还有一个细节你必须知道:find 的 -name 参数匹配的是“文件名”,不是“路径”。如果你想排除某个子目录,比如 /data/logs/pay 里的日志不清理,就要用 -not -path "*pay*"。这种需求在正式项目里很容易遇到,到时候别忘了。
3.3 保护机制:防止删错目录、删错文件
删文件这件事,错误代价是不可逆的。所以这个脚本里我特意塞了好几层保护,初学者抄的时候千万别嫌啰嗦。
第一层保护是默认 DRY_RUN。脚本默认只打印 [DRY_RUN] 待清理: 列表,什么都不删。只有你明确加了 --exec,它才真正执行 rm -f。这能防止你“准备把脚本写进 crontab 时手滑直接执行”这种低级事故。
第二层保护是目录白名单 + 目录存在性判断。LOG_DIRS 变量是写死在脚本里的,执行 find 之前还会判断目录是否存在,不存在就跳过。这防止了变量被清空时,find 扫描到 /. 甚至整个文件系统——这种事故在网上的删库跑路故事里出现过太多次了,都是变量为空惹的祸。
第三层保护是锁文件。脚本开头检查 /tmp/${SCRIPT_NAME}.lock,如果文件存在且对应进程还活着,就说明上一次清理还没跑完,直接退出。这个机制在 crontab 里非常有用,避免前一次 find 还在扫大量文件、下一次定时任务又启动,导致磁盘 IO 被拉满。
第四层保护是删除日志记录。真正执行删除时,每个文件都会写入一行 [DELETE] 文件路径 到 /var/log/clean_old_logs.log。以后如果发现某个日志丢失了、或者业务报障说某天的日志查不到了,你能直接查这个文件追溯,而不是两眼一抹黑。
4. 定时任务与落地执行
脚本本身写好了只是第一步,真正让它发挥作用的,是挂到 crontab 里按计划跑。这个环节也有不少坑,我一个个说。
4.1 crontab 配置与执行频率
我用的是每天凌晨 3 点跑一次,配置长这样:
bash复制crontab -e
# 加入下面这一行
0 3 * * * /opt/scripts/clean_old_logs.sh --exec >> /var/log/clean_old_logs.log 2>&1
为什么选凌晨 3 点?因为游戏项目这个时候是在线人数最低、业务写日志最少的时段,即使 find 扫描大量文件、短暂占一些磁盘 IO,对玩家的影响也最小。如果你管的是跨时区业务或者海外项目,记得把执行时间往后挪,比如凌晨 5 点,原则就是避开高峰期。
执行频率一天一次足够了,不需要一小时一次。日志清理不是越频繁越好,频繁跑 find 只会增加无谓的 IO 压力。你只要保证“每天能清掉前一天产生的量”,就是健康的节奏。
4.2 定时任务跑不起来的三个经典原因
新手最容易在这个环节翻车,我的经验是以下三个问题占了 90% 的排障时间:
- 脚本没有可执行权限。
chmod +x /opt/scripts/clean_old_logs.sh这步忘了的话,crontab 里不管你怎么写都不会执行。手动执行没问题,定时任务却静默失败,经验不足的人能排查一下午。 - 脚本没有写 shebang 或换行符不对。
#!/bin/bash必须放在第一行。如果你在 Windows 上编辑过脚本再传到 Linux,文件会带 CRLF 换行,直接执行会报bad interpreter。用sed -i 's/\r$//' clean_old_logs.sh或dos2unix处理一下。 - crontab 的 PATH 环境变量和终端不一样。cron 执行时的 PATH 通常只有
/usr/bin:/bin。如果脚本里用到了其他目录下的命令,比如/usr/local/bin/flyway,一定要在脚本开头手动export PATH=/usr/local/bin:/usr/bin:/bin:$PATH。我这个脚本只用了 find、rm、date、cat 这些系统自带命令,所以没踩这个坑,但你必须心里有数。
4.3 效果验证与后续运维
写完 crontab 之后不要就不管了,至少观察一周。我的习惯是每天早上到公司先看一眼 /var/log/clean_old_logs.log 末尾,确认定时任务真的跑了、删除记录是正常的。如果你想确认 crontab 是否执行过,可以看系统 cron 日志:
bash复制grep CRON /var/log/syslog | tail -20
另外,脚本里那个 clean_old_logs.log 自身也会越来越大,建议定期清理,或者干脆在脚本里加个判断,超过 50M 就把前面的内容清掉。这种“清理日志的日志”听上去有点黑色幽默,但你不处理它,它早晚也会把磁盘填满。
5. 踩坑实录:这些坑我都替你们踩过
最后一个部分,分享几个我在日志清理这条路上亲身踩过的坑。有些是脚本本身的坑,有些是运维里绕不开的经典问题,都有很强的借鉴意义。
5.1 删了文件磁盘空间却没有释放
这个坑太典型了,很多人第一次遇到都会懵。现象是:脚本明明把日志文件 delete 了,删除记录里也有,但 df -h 一看,磁盘使用率完全没降。
原因在于:Linux 删除文件,只有在没有任何进程持有该文件句柄的情况下,空间才会真正释放。如果你的应用进程还在往那个文件里写东西(或者只是打开了句柄没关),日志文件虽然从目录里看没了,但空间一直占着,直到进程重启或文件句柄被关闭。
排查命令是 lsof +L1 | grep deleted,看到哪一行就是哪个进程占着。解决办法一般是重启对应的服务,或者用 nginx -s reopen 这类优雅回收方式。这个场景提醒我一个事:脚本只能删文件,不能解决句柄占用,写日志的应用是不是正常优雅写日志,才是根因。
5.2 mtime 和 -name 通配符的坑
有一次我发现脚本把一些“看起来不该删”的文件列进了 DRY_RUN 列表里,排查发现是 -name "*.log" 没加引号导致的。shell 会在执行 find 之前先把 *.log 通配符展开成当前目录下的所有 log 文件,展开完之后传给 find 的就不再是通配符了,匹配逻辑全乱。
所以再次强调:find 里的 -name 参数一定要加引号。另外,如果你用 -mtime +30 想删 30 天前的日志,但日志文件每天都被写入,mtime 就会一直刷新,那么它永远不可能被匹配到——这个逻辑其实是符合预期的:最近还在写的日志本来就该保留,只有超过 30 天没写过的旧日志才该清。
5.3 删除范围扩大化怎么防
有朋友照着网上模板写脚本,把路径写成了 find / -name "*.log" -mtime +30 -delete,这基本就是在赌命了。系统目录、程序安装目录里的日志文件都可能遭殃,更别说 / 下还有大量你没想过的隐藏目录。
我的做法是:脚本里的目录白名单永远写绝对路径,不写 / 和家目录这种太宽的范围。并且第一周只跑 DRY_RUN,把所有“待清理”的文件都人工过一遍,确认没有把数据库备份、配置文件、程序脚本之类的东西误伤,再开启真正的定时删除。不要相信“脚本跑完再检查”,删除这种事一旦发生就回不了头,必须前置确认。
经历过那次磁盘告警事故,又把这套脚本从无到有搭起来之后,我体会最深的一点是:运维不是等到故障发生了再去“救火”,而是提前把可能出问题的地方都堵住。日志清理就是这样一件小事,但它背后涉及磁盘容量规划、脚本安全、定时任务、进程句柄等一堆知识点。我写这篇博客的时候刻意把这些细节都讲到位了,因为这些都是我在“练气期”阶段踩过坑才学到的。你如果也用得上,建议先在自己测试机器上把脚本跑一周,确认输出列表没问题,再上生产环境。我这套脚本后续还会继续演进,比如加上磁盘使用率动态判断、异常通知等,等落地个把月稳定了再来分享。
