磁盘清理脚本:日志轮转加阈值告警

服务器磁盘被打满的原因,九成是日志没人管。写一个能定时跑、超阈值会叫人的脚本,比事后救火体面得多。 需求拆解 按时间清理 N 天前的日志 磁盘使用率超过阈值时告警 找到并处理异常大的单个文件 全程记日志,方便事后追溯 脚本 #!/bin/b

服务器磁盘被打满的原因,九成是日志没人管。写一个能定时跑、超阈值会叫人的脚本,比事后救火体面得多。

需求拆解

  1. 按时间清理 N 天前的日志
  2. 磁盘使用率超过阈值时告警
  3. 找到并处理异常大的单个文件
  4. 全程记日志,方便事后追溯

脚本

#!/bin/bash
# disk_clean.sh —— 日志清理与磁盘巡检
set -uo pipefail

LOG_DIRS=("/var/log/myapp" "/data/logs")
KEEP_DAYS=14
WARN_PCT=80
CRIT_PCT=90
LOG_FILE="/var/log/disk_clean.log"

log() { echo "[$(date '+%F %T')] $*" >> "$LOG_FILE"; }

alert() {
    local level="$1" msg="$2"
    log "[$level] $msg"
    # 接入实际告警渠道,如企业微信 / 钉钉 / 邮件
    curl -s -X POST "$WEBHOOK_URL" \
         -H 'Content-Type: application/json' \
         -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[$level] $(hostname): $msg\"}}" \
         >/dev/null 2>&1 || true
}

# 1. 清理过期日志
for d in "${LOG_DIRS[@]}"; do
    [ -d "$d" ] || continue
    before=$(du -sm "$d" 2>/dev/null | cut -f1)
    find "$d" -type f -name "*.log*" -mtime +$KEEP_DAYS -delete 2>/dev/null
    after=$(du -sm "$d" 2>/dev/null | cut -f1)
    log "清理 $d: ${before}MB -> ${after}MB"
done

# 2. 揪出异常大的文件(超过 500MB 单独点名)
find "${LOG_DIRS[@]}" -type f -size +500M 2>/dev/null | while read -r f; do
    alert WARN "大文件: $f ($(du -h "$f" | cut -f1))"
done

# 3. 磁盘水位检查
df -P | awk 'NR>1 {print $5, $6}' | while read -r pct mount; do
    n=${pct%\%}
    [ "$n" -ge "$CRIT_PCT" ] && alert CRIT "磁盘 $mount 使用率 ${n}%"
    [ "$n" -ge "$WARN_PCT" ] && [ "$n" -lt "$CRIT_PCT" ] && \
        alert WARN "磁盘 $mount 使用率 ${n}%"
done

log "巡检完成"

几个容易忽略的细节

-mtime +N 是严格大于 N 天,不是”N 天前至今”。要保留 14 天,写 -mtime +14。

先压后删更省空间:如果日志有保留要求,先 gzip 再删原文:

find "$d" -name "*.log" -mtime +7 -exec gzip {} \;
find "$d" -name "*.log.gz" -mtime +30 -delete

set -e 慎用:清理类脚本里单个 find 失败不该让整个脚本退出,用 set -uo pipefail 而不加 -e。

while read 管道里的变量:df | while read 在子 shell 里执行,循环内修改的变量外面读不到。本例只调用函数不修改变量,所以没问题;要累计计数就得用进程替换 while read ...; done < <(df -P)。

挂到定时任务

# /etc/cron.d/disk_clean
30 3 * * * root /usr/local/bin/disk_clean.sh

凌晨 3 点半跑,避开业务高峰,也不和常见的整点任务抢 IO。

验证清单

  • 造几个假文件,touch -d "20 days ago" 验证能被正确清理
  • 把阈值临时改成 1,确认告警真能发出来
  • 确认 WEBHOOK_URL 未设置时脚本不会卡住(|| true 那一行就是干这个的)

最后一个最容易被漏——脚本在生产环境因为一个没配的变量卡住,比不写脚本更糟。