ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux日志清理实战:从磁盘告警到自动化清理脚本

2026/10/3 8:58:15 拓冰建站 浏览量
Linux日志清理实战:从磁盘告警到自动化清理脚本 磁盘告警那天我记得很清楚。凌晨三点被监控短信吵醒登录一看/分区使用率已经到了 92%再拖几个小时业务就要挂。df -h一查/var/log下面躺着几十个 GB 的日志文件messages和syslog单个就有 20 多 GB。那时候我手里没有现成的清理方案只能先手动truncate几个大文件把水位降下来第二天再熬夜写脚本。那台机器上跑着一堆 Java 服务catalina.out以每天 1GB 的速度膨胀老旧的单体架构本身又没接中央日志所有输出全写在本机磁盘上。手动清一次只能管两三天再往外就是没完没了的重复劳动。所以日志自动清理这个需求不是简单的写个脚本定时删文件而是要把日志的存放位置、增长速率、服务对日志文件句柄的依赖、定时调度、权限边界全部考虑进去。这篇就把我当时整理出来的完整思路和脚本逐段拆开讲适合刚接触 Linux 运维、写过几行 shell 但不知道怎么在服务器上做实操的读者。代码可以直接抄坑我也会一个一个指出来。1. 日志吃满磁盘的常见路径先搞清楚到底清什么写脚本之前最忌讳的就是抄一段find /var/log -type f -name *.log -exec rm -f {} \;就丢到 crontab 里。这种写法在干净的测试机上没问题放到真实生产环境几乎必然出事。我先带你把最常见的日志存放位置和膨胀规律过一遍后面脚本的设计逻辑才能对得上。1.1 主流发行版日志基本都堆在哪里/var/log/messagesCentOS / RHEL 系的主日志内核和大部分服务的输出都往这里写高负载机器一个月能冲到几 GB。/var/log/syslogDebian / Ubuntu 系对应messages的位置行为类似。/var/log/secure登录认证日志暴力破解频繁的机器会快速增长。/var/log/cron计划任务执行日志任务多且输出未重定向时会积累。/var/log/boot.log、/var/log/dmesg启动过程和内核环形缓冲区的落盘固定大小膨胀一般不严重。应用自己的日志/opt/app/logs、/home/www/logs、Tomcat 的catalina.out、Nginx 的access.log和error.log。这类才是真正的重灾区因为应用不会像系统日志那样自带 rotate 策略写多少就留多少。日志路径典型增长速率清理风险/var/log/messages中等高并发机器快需要保证 syslog 服务可写/var/log/secure被扫描时极快删除后 sshd 继续写入旧句柄应用目录下的 *.log可达 GB/天应用持有文件句柄rm 后磁盘不释放Nginx access.log流量大时可 GB/天直接删会导致正在打开的 fd 继续占空间1.2 一个不能删的边界服务仍然持有的文件句柄日志清理里面最反直觉的一点是rm删除日志文件后磁盘空间不一定立刻释放。只要某个进程还握着这个文件的文件描述符文件的 inode 就还活着空间依然被占用。表现就是df一看还是满的du却找不到那个文件。这个现象在处理catalina.out、messages、spring.log这类由长驻进程持续写入的日志时尤其常见。我当时处理第一台机器时就是这样直接rm -f /var/log/messages磁盘使用率纹丝不动。后来查了才知道rsyslog 一直打开着这个文件删除操作只是把目录项抹掉了真正占空间的 inode 还挂在进程上。解决方式不是删除文件而是清空文件内容也就是truncate或: file。清空操作保留文件路径和 inode进程继续往同一个位置写磁盘空间立刻释放。1.3 日志保留策略的取舍日志要留几天这个问题没有标准答案取决于你所在团队的需求有没有审计要求、会不会有人回头查半年前的报错、监控平台能不能长期存储。我自己的实践是分三类处理系统日志messages / syslog / secure保留 7 天因为排查问题大多只看最近一周。应用业务日志保留 1530 天业务侧经常需要对比更长周期的数据。调试日志 / 临时nohup.out保留 3 天这类日志不删就是磁盘杀手。每类日志给一个独立的保留天数用同一个脚本但不同配置来跑比一刀切删 7 天前灵活得多。2. 清理策略对比为什么我不直接rm也不完全交给logrotate市面上现成的日志清理方案不少系统自带的logrotate就能按天轮转、按大小轮转、压缩旧档。那为什么还要自己写脚本因为很多场景下logrotate覆盖不到你没有权限改系统配置、应用日志目录结构混乱、日志文件名不带统一的日期后缀、或者干脆是云服务器镜像里预装的精简系统把 logrotate 卸载了。自己写脚本的价值在于可定制、可审计、可扩展。2.1rm的副作用不止是句柄问题删除文件后日志系统可能会创建一个新文件但新文件的权限、属主、SELinux 上下文如果不对会导致服务写日志失败。尤其 rsyslog 的$FileCreateMode配置和你手动touch出来的默认权限不一致时坑最明显。rm是彻底删除没有回滚余地。万一误删了还在使用的重要日志后续需要回溯时就只能从备份或监控系统里找。删除操作瞬间释放大量 inode 和磁盘块但同时触发大量元数据更新在性能敏感的业务时段会带来微小的 IO 冲击。2.2truncatefind的组合更稳清理日志的正确姿势是找到需要处理的文件后不是删掉它而是把内容清空truncate -s 0 /var/log/messages对正在被写入的日志这个操作是安全的。清空后进程继续在原有句柄上写文件重新从 0 开始增长。对于已经完全不再写入的旧日志比如上了日期的app.log.2025-05-01则可以放心删除。所以脚本的核心逻辑是没有日期后缀、仍在活跃写入的日志一律truncate清空不动文件本身。带日期后缀、且时间超过保留期限的归档文件直接删除。用时间条件判断的find再加上类型限制就能覆盖绝大多数场景find /var/log -type f -name *.log -mtime 7 -exec truncate -s 0 {} \;这里没有rm因为/var/log下的日志基本都是活跃写入文件清空是正确的处理方式。真正要删除的归档日志我会单独用一个目录列表来匹配避免误伤。2.3 和logrotate的分工边界我并不是要把logrotate说成没用。如果服务允许你自己定义轮转规则logrotate当然是首选它的copytruncate、compress、dateext都很成熟。我的建议是系统级日志交给logrotate默认配置不要动。应用日志如果应用本身不做日志切割就用我的清理脚本做兜底。混合方案logrotate每天生成一个带日期的压缩文件清理脚本负责删除超过保留期限的压缩文件。这样的分工下轮转是平滑的历史归档是保留的磁盘不会被掏空也不会出现删了一半正在写的文件这种闹心事。3. 清理脚本逐段拆解参数化设计、防呆保护和执行逻辑下面这个脚本是我实际在用的版本去掉了公司内部路径和敏感信息保留了完整骨架。它的设计目标很简单一个脚本多份配置既能手动跑也能自动跑还要能防误删。3.1 基础骨架与可配置区#!/usr/bin/env bash # --------------------------------------------------------- # log-cleaner.sh # 适用CentOS 7 / 8、Ubuntu 20.04、Debian 派生发行版 # 功能按目录清理过期日志、清空超大日志 # --------------------------------------------------------- set -u # 备份文件保留天数按实际需求调整 DAYS_SYSTEM7 DAYS_APP15 DAYS_DEBUG3 # 超过该大小的活跃日志将被直接清空单位 MB MAX_SIZE_MB1024 # 需要清理的根目录列表 SYSLOG_DIRS/var/log APPLOG_DIRS/opt/app/logs /home/www/logs /data/logs # 只删除归档文件绝不 truncate 的路径 ARCHIVE_ROOTS/var/log /opt/app/logs # 需要保留的文件列表使用通配模式 PROTECT_PATTERNS( /var/log/btmp /var/log/wtmp /var/log/lastlog ) # 日志输出 LOGFILE/var/log/log-cleaner.log log() { echo $(date %Y-%m-%d %H:%M:%S) $* $LOGFILE } clean_system_logs() { local dir$1 local days$2 # 只处理 .log 日志和系统常见日志名 find $dir -maxdepth 1 -type f \ \( -name *.log -o -name messages* -o -name syslog* \) \ -mtime ${days} -exec truncate -s 0 {} \; log cleaned system logs under $dir (retention${days}d) } clean_app_logs() { local dir$1 local days$2 # 处理活跃日志只清空 find $dir -maxdepth 1 -type f -name *.log -mtime ${days} -exec truncate -s 0 {} \; # 处理归档日志只删除明确带时间戳且过期的文件 find $dir -maxdepth 1 -type f \ \( -name *.log.* -o -name *.gz -o -name *.zip \) \ -mtime ${days} -delete log cleaned app logs under $dir (retention${days}d) } # 对超大活跃文件做即时清空 truncate_oversize() { local dir$1 find $dir -maxdepth 1 -type f -name *.log -size ${MAX_SIZE_MB}M \ -exec truncate -s 0 {} \; log truncated log files larger than ${MAX_SIZE_MB}MB under $dir } # 保留保护列表中的文件防止权限和账号数据损坏 protect_files() { local pattern for pattern in ${PROTECT_PATTERNS[]}; do chattr -a $pattern 2/dev/null || true done } main() { # 无参数时输出帮助 if [ $# -lt 1 ]; then echo Usage: $0 {system|app|oversize|all} 2 exit 1 fi case ${1:-all} in system) for d in $SYSLOG_DIRS; do clean_system_logs $d $DAYS_SYSTEM; done ;; app) for d in $APPLOG_DIRS; do clean_app_logs $d $DAYS_APP; done ;; oversize) for d in $SYSLOG_DIRS $APPLOG_DIRS; do truncate_oversize $d; done ;; all) for d in $SYSLOG_DIRS; do clean_system_logs $d $DAYS_SYSTEM; done for d in $APPLOG_DIRS; do clean_app_logs $d $DAYS_APP; done for d in $SYSLOG_DIRS $APPLOG_DIRS; do truncate_oversize $d; done ;; *) echo Unknown option: $1 2; exit 2 ;; esac } protect_files main $这个脚本有几个用心设计的点我逐个说。3.2 关键参数设计的底层逻辑-maxdepth 1限制深度这是防止误删的核心之一。日志目录下通常只有一层文件加上-maxdepth 1就不会递归到子目录的无关文件里去。如果应用目录下有按日期分子目录的组织方式再单独写一个递归清理函数不要混在一起。-mtime ${days}的语义find的-mtime 7表示超过 7 * 24 小时之前修改过。之所以用加号而不是裸数字是因为裸数字匹配的是恰好 7 天这个区间不是我们理解的7 天以上。设成DAYS_SYSTEM7后传参7才是正确写法写成7会出现昨天改过的文件也被删掉的诡异问题。区分清空与删除clean_app_logs里对*.log用truncate对*.log.*、*.gz用-delete。原因前面提过活跃文件不能删归档文件留着没有价值。真实环境里如果应用写日志时带着日期轮转像app.log.2025-11-20那这些就是归档文件到期直接删。如果应用一直往app.log写就只清空。3.3 防误删保护机制脚本里的protect_files函数很有意思。/var/log/btmp、/var/log/wtmp是登录失败的记录和登录成功的记录lastlog记录每个用户最近登录时间。这些文件被find按名字匹配到的概率不高但一旦清空了last、who、lastlog这几条命令的排查能力就废了。chattr -a给它们加追加属性我在清理前显式解除一次是为了防止系统里之前设置过限制导致清理失败也算双保险。另外一个常见保护手段是在脚本顶部做磁盘使用率二次判断usage$(df /var/log | awk NR2 {print $5} | tr -d %) if [ $usage -lt 80 ]; then echo disk usage ${usage}% is below 80%, skip cleanup exit 0 fi这个低于阈值不干活的逻辑非常实用。日志增长没那么快的时候没必要天天清空文件人为减少对服务的干扰。我把它放在main之前配合 crontab 每天跑一次实际效果是只有磁盘到 80% 以上才清理。既保住了磁盘水位又不会每天都做无谓的 IO 操作。4. 实测和排障生产环境里我踩过的三个坑脚本写完之后不能直接上生产一定要先在测试环境空转再拿到低峰时段实操。下面这三个坑都是我在真实服务器上排过的每一段都对应一条可以直接抄的事故清单。4.1 坑一find -mtime的 24 小时误差观察我起初把清理脚本放到 crontab 每天凌晨 3 点跑预期是清掉 7 天前的日志。但第二天检查时发现昨天下午生成的日志也被清空了。排查后定位到原因-mtime 7是严格按 Unix 时间戳减去 7 * 86400 秒整除后计算天数而非自然天。比如文件修改时间是 2025-11-20 14:00脚本在 2025-11-27 03:00 跑时间差是 6 天 13 小时在find看来已经是 7 个整数天了于是被当作过期文件处理。对策按自然天判断给脚本传入DAYS_SYSTEM后不要直接在find命令里写-mtime而是用stat先算出文件的修改日期再比较或者直接用find -newermt指定绝对时间阈值。我在生产脚本里用的是更稳妥的写法find $dir -maxdepth 1 -type f -name *.log \ ! -newermt $(date -d ${days} days ago %Y-%m-%d 00:00:00) \ -exec truncate -s 0 {} \;用! -newermt表达文件修改时间早于 N 天前零点的逻辑避开了-mtime的整除陷阱。这个细节如果不踩一次很难意识到。4.2 坑二日志目录里混入了空格和通配符字符观察脚本在某个应用服务器上报错find输出的路径被 shell 拆分结果一条truncate只截断了路径的前半段剩下半段变成了多余参数。原因很朴素——应用日志目录里有带空格的子目录比如/data/logs/my app/。find -exec truncate -s 0 {} \;本身对空格是安全的因为{}会在内部展开成完整路径但如果我在循环里把find输出接到了变量上就埋下了分裂隐患。对策优先用find -exec不要用for file in $(find ...)。如果必须用 for 循环改成while IFS read -r filefind $dir -type f -name *.log -print0 | while IFS read -r -d file; do truncate -s 0 $file done-print0配合-d 是处理任意文件名的唯一正解。虽然我脚本里刻意用-maxdepth 1减少了递归但真实环境下应用目录下出现带空格文件名的概率并不低。4.3 坑三crontab 环境变量和权限不一致观察脚本在终端里手动执行一切正常放进 crontab 后却提示权限不足。排查发现 root 的 crontab 默认 PATH 只有/usr/bin:/bin而脚本里用到了/usr/bin/truncate、/usr/bin/find这些路径在精简系统上不一定在默认 PATH 里。对策在脚本头部显式导入环境不要依赖 shell 的默认 PATHexport PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin同样重要的是crontab 里最好确认脚本本身有可执行权限并且用绝对路径调用0 3 * * * /bin/bash /opt/scripts/log-cleaner.sh all /var/log/log-cleaner-cron.log 21还有个细节脚本里truncate命令在部分旧系统上不存在我后来改用: file作为备选方案。truncate的优点是你可以只清空超大文件而不影响文件的权限属主非常稳如果系统没有truncate就用: $file冒号是 shell 内建的空操作 file把文件截断为空效果一样。但这种方式要求当前用户对文件有写权限所以脚本里要确保以 root 或日志目录属主身份运行。5. 让脚本真正“自动”——定时调度、防重入和告警联动脚本写好只是第一步让它按节奏自动跑才是自动清理的本体。我这里有一套完整的落地套路。5.1 crontab 配置示例推荐早上非业务高峰跑一次凌晨 3 点到 4 点之间最合适# 日志清理示例 0 3 * * * root /bin/bash /opt/scripts/log-cleaner.sh all /var/log/log-cleaner-cron.log 21如果设置了磁盘阈值判断低于 80% 跳过这个频率不会对系统造成压力。如果日志增长速度更快可以在 14 点再加一次0 14 * * * root /bin/bash /opt/scripts/log-cleaner.sh app /var/log/log-cleaner-cron.log 21应用日志单独跑一次app模式只处理应用目录不影响系统日志。5.2 防止脚本重入的锁机制crontab 的定时任务不保证不重叠。如果上一次清理因为磁盘 IO 慢没有结束下一次任务又被触发两个脚本同时truncate文件不仅没必要还可能导致日志短暂写乱。解决方案是用flock给脚本加锁#!/usr/bin/env bash exec 9 /var/lock/log-cleaner.lock if ! flock -n 9; then echo another cleaner instance is running, skip exit 0 fi # 后续是真正的清理逻辑flock -n表示拿不到锁就立刻退出不会阻塞等待。锁文件放在/var/lock重启后自动清空不用担心残留。5.3 磁盘使用率告警联动脚本本身清完日志后最好加一段检查磁盘水位的逻辑方便排查为什么清完还是会报警。# 在脚本执行完清理动作后输出当前水位 check_disk_usage() { local partition$1 local usage usage$(df $partition | awk NR2 {print $5} | tr -d %) if [ $usage -ge 90 ]; then echo WARNING: $partition usage is ${usage}%, need manual intervention else echo OK: $partition usage is ${usage}% fi }把这段输出接到log-cleaner-cron.log再用你自己的监控系统扫这个日志文件里的 WARNING 关键字就能实现自动清理 清理失败自动告警。我在实际部署时还加了一步把每天的磁盘水位追加到本地一个csv文件后续压测或排障时能快速看到日志清理的效果线。6. 进阶Python 改写、轮转前置和彻底告别手写脚本shell 脚本简单直接但维护到一定程度后会有几个痛点find的参数组合越来越复杂、不同发行版之间行为差异大、没有结构化的输出结果。如果你需要在多台机器上统一管理日志清理可以考虑下面几种进阶方案。6.1 什么时候应该用 Python 或替代工具重写“要不要用 Python 重写”的衡量标准很简单你的清理策略里规则是否已经复杂到 shell 里全是 if/else如果只是保留天数不同、目录不同shell 完全够用。但如果你要做正则匹配日志名称、按文件日期解析、将清理结果上报到监控平台Python 的os.path.getmtime、pathlib.glob、re会让代码干净很多。一个 Python 版本的简化骨架#!/usr/bin/env python3 import os import time RETENTION_DAYS 7 LOG_DIRS [/var/log] PROTECTED {/var/log/btmp, /var/log/wtmp, /var/log/lastlog} def clean_dir(directory: str, days: int) - list[str]: cleaned [] now time.time() for root, _, files in os.walk(directory): for name in files: path os.path.join(root, name) if not name.endswith(.log) or path in PROTECTED: continue age_days (now - os.path.getmtime(path)) / 86400 if age_days days: open(path, w).close() cleaned.append(path) return cleanedopen(path, w).close()在这里等价于 shell 的: file。注意遍历用os.walk比 shell 的find更容易控制子目录深度和行为差异。写完之后用python3 log-cleaner.py直接跑输出可以做成 JSON配合监控接口非常方便。但我也要提醒一句能用 shell 解决的生产问题不要轻易引入 Python 运行时。部分精简镜像上连 Python 都没有为了一个清理脚本安装解释器性价比太低。先评估环境再选型。6.2 从“事后清理”走向“事前轮转”集中日志平台与监控水位日志清理的最终形态不是写一个更聪明的清理脚本而是尽量让日志不在本地堆积。我和团队后来推动做的一个改造就是把所有服务日志通过 filebeat / fluentbit 发送到集中的日志平台本地只保留最近 12 天。这样就算脚本哪天挂了磁盘也不会立刻爆掉。在这个方案落地前我总结出一套行之有效的本地兜底策略给应用日志统一加logrotate轮转或让应用自己按天切分不要让单一app.log无限增长。接一个简单的磁盘监控超过 80% 就提前告警不要等到 95% 再处理。清理脚本固定每天早上跑一次保留策略按7 天 / 15 天 / 30 天分类执行结果留日志。每次上线清理脚本时先在测试环境用maxdepth1跑find的-print模式预览命中列表肉眼确认没问题再真正执行。我见过太多日志目录又满了的工单大部分其实不是空间不够而是没有一套稳定的清理节奏。这套脚本和流程在几十台机器上跑下来稳定运行了很长时间磁盘水位一直维持在安全线以内。如果你在落地的过程中遇到其他发行版特有的日志路径差异或者服务进程特殊导致句柄不释放的情况直接按自己环境调整目录列表和保留天数就行。有一次我碰到一个服务不仅不释放旧句柄还会在truncate -s 0之后一直写旧偏移量导致文件空洞越来越大最后是重启服务才彻底解决。这种非典型情况虽然少见但真遇到了要记得检查lsof L1看谁还在握着已删除的文件处理手段不是死磕脚本而是先处理服务本身的日志句柄问题——脚本能解决 90% 的常规膨胀剩下的 10% 总是要具体问题具体分析的。