如何编写服务器磁盘使用率告警脚本?Shell监控与邮件通知配置

在所有服务器告警里,磁盘空间的告警优先级应该排在最前面。原因很简单:CPU 打满通常表现为"变慢",还能撑一撑;内存紧张有 swap 兜底,也不会立刻死机;但磁盘一旦写满,日志写不进、数据库卡死、服务可能直接起不来,而且是不可逆的硬性中断。更麻烦的是,磁盘是缓慢增长的,不主动监控,你根本不知道它什么时候会见底。

这篇文章讲怎么自己写一个磁盘使用率告警脚本:怎么取数、怎么定阈值、怎么判断"增长趋势"提前预警、怎么把邮件发出去。脚本用纯 Shell 实现,兼容 CentOS/Rocky 与 Ubuntu/Debian,可以直接挂到定时任务上长期运行。

所有阈值都会标明是经验值,你可以根据磁盘容量和增长速度调整。涉及发信配置和定时任务的部分会给出验证步骤,避免脚本看起来在跑、实际邮件一直发不出去。

一、告警要怎么设计才有效

先别急着写代码,想清楚一个"好告警"的标准。很多人写的脚本只会说"磁盘超过 85% 了",但值班的人收到后还是不知道怎么办。一个好的磁盘告警至少要回答四件事:哪台机器、哪个分区、现在多少、涨得多快。最后一项尤其关键——同样是 80%,一个分区每天涨 0.1%,另一个每天涨 5%,紧急程度完全不同。

所以设计上要分两个层次:

告警层次 触发条件 目的 动作
阈值告警 使用率 ≥ 85%(经验值) 提示需尽快处理 发邮件/IM
严重告警 使用率 ≥ 95%(经验值) 随时可能写满 升级为电话/紧急通道
趋势预警 近 N 天日均增长 × 剩余天数 < 阈值 提前预判何时写满 提前规划扩容
恢复通知 从告警状态回到正常 告知问题已解决 发"恢复"邮件

只做阈值告警是大多数人的起点,但真正有价值的是趋势预警。如果一块 100GB 的盘每天涨 2GB,那 8 天后就会满,现在不预警,等到它 85% 时只剩一周反应时间,很多采购和扩容流程根本来不及。

二、取数命令与版本差异

磁盘使用率来自 df,但直接用 df -h 的百分比列做判断有坑:它是整数四舍五入,而且会混入 tmpfs、overlay 等虚拟文件系统。推荐做法是用 df -P(POSIX 格式,保证一行一个文件系统)配合 -x 排除虚拟文件系统。

# 查看所有真实文件系统的使用率(排除 tmpfs/devtmpfs/overlay)
df -Ph -x tmpfs -x devtmpfs -x overlay

# 只看某个挂载点
df -Ph /data

# 查看 inode 使用率(小文件多时同样会满)
df -Pi -x tmpfs -x devtmpfs

需要注意的发行版差异:

  • df 与 ss、free 这些基础工具在两系里参数基本一致,脚本可通用。
  • 邮件命令差异较大:RHEL/CentOS/Rocky 用 mailx 包,命令是 mail;Debian/Ubuntu 要用 mailutils 包。安装命令不同。
  • 部分精简云镜像默认不装 bc,脚本里做小数运算建议用 awk,兼容性最好。
# 安装发信工具
# RHEL/CentOS/Rocky:
sudo dnf install -y mailx
# Debian/Ubuntu:
sudo apt update && sudo apt install -y mailutils

三、完整磁盘告警脚本

下面这份脚本带阈值告警、状态去重(避免重复轰炸)、以及记录历史用于趋势判断。

#!/usr/bin/env bash
# 磁盘使用率告警脚本
# 用法:bash disk_alert.sh
set -uo pipefail

# ---- 配置区(经验值,按需调整)----
WARN_PCT=85          # 预警阈值(%)
CRIT_PCT=95          # 严重阈值(%)
MAIL_TO="ops@example.com"
HOSTNAME_SHORT=$(hostname -s)
STATE_DIR="/var/lib/disk_alert"
HIST_DIR="$STATE_DIR/hist"
mkdir -p "$HIST_DIR"

# 排除虚拟文件系统
DF_CMD="df -Ph -x tmpfs -x devtmpfs -x overlay -x squashfs"

ALERTS=""
HIGHEST=0

while read -r fs size used avail pct mnt; do
  [ "$fs" = "Filesystem" ] && continue
  num=${pct%\%}
  [ "$num" -gt "$HIGHEST" ] && HIGHEST=$num

  # 记录历史(每个挂载点一行:日期 百分比)
  safe_mnt=$(echo "$mnt" | tr '/' '_')
  echo "$(date +%F) $num" >> "$HIST_DIR/$safe_mnt.hist"

  if [ "$num" -ge "$CRIT_PCT" ]; then
    ALERTS="${ALERTS}【严重】${mnt} 已用 ${pct}(剩余 ${avail})\n"
  elif [ "$num" -ge "$WARN_PCT" ]; then
    ALERTS="${ALERTS}【预警】${mnt} 已用 ${pct}(剩余 ${avail})\n"
  fi
done < <($DF_CMD | grep -vE '^Filesystem')

# 状态去重:只有状态变化时才发信,避免每 5 分钟一封
STATE_FILE="$STATE_DIR/last_state"
LAST_STATE=$(cat "$STATE_FILE" 2>/dev/null || echo "normal")
if [ -z "$ALERTS" ]; then
  CUR_STATE="normal"
else
  CUR_STATE="alert"
fi

if [ "$CUR_STATE" = "alert" ] && [ "$LAST_STATE" != "alert" ]; then
  SUBJECT="[磁盘告警] ${HOSTNAME_SHORT} 最高使用率 ${HIGHEST}% $(date '+%F %T')"
  printf "主机: %s\n时间: %s\n\n%b\n请尽快登录处理。\n查看大目录: du -sh /var/* 2>/dev/null | sort -rh | head\n" \
    "$HOSTNAME_SHORT" "$(date '+%F %T')" "$ALERTS" | mail -s "$SUBJECT" "$MAIL_TO"
  echo "alert" > "$STATE_FILE"
elif [ "$CUR_STATE" = "normal" ] && [ "$LAST_STATE" = "alert" ]; then
  # 恢复通知
  printf "主机: %s\n时间: %s\n磁盘使用率已恢复正常,最高 %s%%。\n" \
    "$HOSTNAME_SHORT" "$(date '+%F %T')" "$HIGHEST" | mail -s "[磁盘恢复] ${HOSTNAME_SHORT}" "$MAIL_TO"
  echo "normal" > "$STATE_FILE"
fi

这段脚本最关键的设计是状态去重:它把"当前是告警还是正常"记录在 last_state 文件里,只有状态发生变化时才发信。否则每 5 分钟一封邮件,一天下来几百封,很快就没人看了。这个思路可以套用到所有类似的监控脚本上。

四、加上趋势预警

阈值告警是"事后",趋势预警是"事前"。思路是:从历史记录里算出日均增长,再除以剩余空间,估算还能撑多少天。

#!/usr/bin/env bash
# 趋势预警:估算磁盘还能用多少天
set -uo pipefail
HIST_DIR="/var/lib/disk_alert/hist"
DAYS_WINDOW=7          # 用最近 7 天的数据算增长
min_days_warn=15       # 剩余可用天数低于此值预警(经验值)

df -Ph -x tmpfs -x devtmpfs -x overlay -x squashfs | grep -vE '^Filesystem' \
| while read -r fs size used avail pct mnt; do
  safe_mnt=$(echo "$mnt" | tr '/' '_')
  hist="$HIST_DIR/$safe_mnt.hist"
  [ -f "$hist" ] || continue

  # 取最近 N 天,计算首末两天的百分比增长
  tail -n "$DAYS_WINDOW" "$hist" > /tmp/hist.$$
  first=$(head -1 /tmp/hist.$$ | awk '{print $2}')
  last=$(tail -1 /tmp/hist.$$ | awk '{print $2}')
  n=$(wc -l < /tmp/hist.$$)
  rm -f /tmp/hist.$$

  [ "$n" -lt 2 ] && continue
  per_day=$(awk -v a="$first" -v b="$last" -v d="$((n-1))" 'BEGIN{printf "%.3f",(b-a)/d}')

  # 剩余百分比 / 日均增长 = 可用天数
  remain_pct=$((100 - ${pct%\%}))
  days=$(awk -v r="$remain_pct" -v g="$per_day" 'BEGIN{ if(g<=0){print 9999} else {printf "%d", r/g} }')

  if [ "$days" -lt "$min_days_warn" ]; then
    echo "[趋势预警] $(hostname -s) ${mnt} 日均增长 ${per_day}%,预计 ${days} 天后写满"
    # 此处可同样调 mail 发送
  fi
done

这份趋势脚本输出的日均增长和"还剩几天"是很直观的数字。经验上,当预计写满时间小于 15 天时就该介入,要么清理,要么申请扩容。这个值不必抠得太准,它是运维沟通的语言,不是精确预报。

五、配置定时任务与邮件发信

告警脚本一般每 5-30 分钟跑一次。频率太高意义不大(磁盘不会几秒内暴涨),也增加噪声;太低则响应太慢。经验上:关键数据库盘每 5 分钟,一般业务盘每 15-30 分钟比较合适。

# 系统级定时任务,每 10 分钟检查一次
sudo tee /etc/cron.d/disk_alert >/dev/null <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/10 * * * * root /usr/local/bin/disk_alert.sh >/dev/null 2>&1
# 每天 08:00 做一次趋势预警
0 8 * * * root /usr/local/bin/disk_trend.sh 2>&1 | mail -s "[磁盘趋势] $(hostname -s)" ops@example.com
EOF
sudo chmod 644 /etc/cron.d/disk_alert

邮件发不出去是最常见的坑。云主机默认的 25 端口大多被限制,直接 mail 会在队列里堆积、延迟半小时后失败。排查和解决方式如下:

# 1) 验证本机能否发信
echo "test mail" | mail -s "test" your@mail.com
# 查邮件队列
mailq        # 或 postqueue -p

# 2) 配置外部 SMTP 中继(以 postfix 为例,RHEL 系统自带)
# 安装:dnf install postfix(RHEL系) / apt install postfix(Debian系)
# 编辑 /etc/postfix/main.cf,指定上游中继:
#   relayhost = [smtp.example.com]:587
#   smtp_sasl_auth_enable = yes
#   smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
#   smtp_use_tls = yes
# 然后在 /etc/postfix/sasl_passwd 写入 用户名:密码,执行:
sudo postmap /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
sudo systemctl restart postfix

如果不想维护 SMTP,最省事的是改用 IM 机器人 Webhook,一条 curl 搞定,且送达比邮件及时得多。

# IM 机器人(以企业微信/钉钉/飞书为例,URL 换成自己的)
send_im() {
  curl -s -X POST "$BOT_WEBHOOK" -H 'Content-Type: application/json' \
    -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"$1\"}}"
}
send_im "[磁盘告警] $(hostname -s) /data 已用 92%,请处理"

六、风险提示与排障

第一,别把阈值设得太低。 90% 就狂告警、结果每次清理完又降回来,反而让人麻木。经验做法是:预警 85%,严重 95%,且配合状态去重。真正的做法是把告警当"要处理的任务",而不是"要忽略的噪声"。

第二,谨慎清理。 收到告警后清理时,千万不要为了省事直接 rm -rf /var/log/*。这会删掉正在写入的日志、甚至破坏目录结构。正确顺序是:du 找大目录 → 确认文件性质 → 用 logrotate 或按时间清理 → 必要时先归档。

第三,注意 inode。 df -h 显示空间充足但业务报"磁盘满",多半是 inode 耗尽。告警脚本应该同时监控 df -i,两者任一超阈值都要告警。

回滚方式:告警脚本只是读取和发信,停止它只需删除 /etc/cron.d/disk_alert 或注释掉 crontab 行;状态文件 /var/lib/disk_alert/ 可整体删除重置。若改动了 postfix 中继配置导致发信异常,恢复 main.cf 备份并 systemctl restart postfix 即可。

七、磁盘真的满了怎么应急

收到 95% 严重的告警时,先别慌着删文件,按"先止血、再定位、后治理"三步走。止血就是立刻腾出一点空间保证服务能写,最快的办法是清空一个已知安全的日志(注意是清空内容而不是删除文件,避免影响正在写入的进程):

# 安全清空(保留文件句柄,服务无需重启)
: > /var/log/nginx/access.log
# 或 truncate 同样效果
truncate -s 0 /var/log/nginx/access.log
# 注意:不要用 rm,否则空间不会立即释放

定位阶段用 du 找元凶,这一步不要跳。治理阶段则是把根因解决掉——可能是日志没做轮转、可能是临时文件堆积、也可能是某个功能写太多数据,只有解决了根因,告警才会真正消失,否则第二天又会满。

常见问题

Q1:为什么告警有时候不发了?
最常见是状态去重生效——磁盘一直处于告警状态,脚本判定"状态没变"就不再发。这是有意设计。如果希望持续提醒,可以改成每天固定时间重复一次,或告警升级时再发。

Q2:df 和 du 统计的空间对不上,正常吗?
正常。df 看的是文件系统层面已分配的块,du 是逐文件累加。差异来源包括:被删除但仍被进程占用的文件(du 看不到但 df 算)、稀疏文件、预留空间(ext4 默认预留 5% 给 root)。差异在几个百分点内属正常。

Q3:邮件能发出但对方收不到,怎么办?
先看是否进了垃圾邮件(检查 SPF/DKIM 配置,用外部邮箱自测)。云主机的 IP 常被列入黑名单,自建发信到达率本就不稳定。生产环境更推荐用企业邮箱的 SMTP 中继,或直接用 IM 机器人。

Q4:怎么判断是空间问题还是 inode 问题?
df -h 看空间,df -i 看 inode。如果空间还剩很多但 df -i 的 IUse% 接近 100%,就是 inode 耗尽,通常是某目录堆积海量小文件。用 find /path -xdev -type f | wc -l 定位。

Q5:脚本能在所有 Linux 上都跑吗?
核心的 df、awk、sort、mail 是标准工具,脚本本身通用。唯一要换的是安装包名(mailx vs mailutils),以及部分命令的默认路径。建议脚本开头统一设置 PATH。

总结

  • 磁盘告警应回答"哪台机、哪个分区、多少、涨多快",只有结合趋势才有提前量。
  • 用 df -Ph -x tmpfs -x devtmpfs 取数,排除虚拟文件系统;同时用 df -Pi 监控 inode。
  • 阈值告警配状态去重,只在状态变化时发信,避免告警泛滥;恢复时发一条"恢复"通知。
  • 趋势预警按"剩余百分比 ÷ 日均增长"估算写满天数,经验上当预计不足 15 天时就应介入。
  • 云主机 25 端口常被限制,邮件发不出优先配 SMTP 中继或用 IM 机器人 Webhook;处理告警时先找大目录,切勿 rm -rf 整个日志目录。