很多人以为"装了监控就等于有告警",其实两回事:监控解决"看得见",告警解决"第一时间知道"。而对中小团队来说,最经济、最可靠的告警通道依然是邮件——它不需要申请任何第三方账号、不受即时通讯工具接口变更影响、天然留存可回溯,而且能在手机邮箱 App 上实时推送。本文讲的是一条不依赖重型监控平台的轻量路线:用 mailx 或 msmtp 打通服务器发信能力,再用 Shell 脚本 + crontab 做故障检测,配合 systemd 的 OnFailure 捕获服务崩溃。整套方案几十行配置就能跑起来,几台到几十台机器的规模完全够用。
一、先拆清楚:告警系统只有三件事
不管用什么工具,告警都只有三个环节,出问题的时候按这三段排查,定位会非常快。
- 检测(Detection):拿到数据。可以是命令输出(
df -h)、可以是接口返回值(curl -sf)、也可以是监控系统的指标。 - 判定(Evaluation):判断是不是异常。关键是要加"持续时间"或"连续次数",避免瞬时抖动误报。比如"连续 3 次检测失败"才告警。
- 通知(Notification):把消息送出去。邮件、企业微信机器人、钉钉、短信网关都是通道。
新手最容易忽略的是第二环节。直接 if [ $used -gt 90 ]; then 发邮件; fi 的脚本,在磁盘 89.6% 与 90.2% 之间反复横跳时会疯狂发信。正确做法是引入状态文件或连续计数,只有持续满足条件才触发,并且恢复时发一次"已恢复"通知。
二、打通邮件通知:mailx 与 msmtp 配置
Linux 服务器本身不能发邮件,需要通过一个 MTA(邮件传输代理)把它交给外部 SMTP 服务器。生产环境不建议装完整的 Postfix/Sendmail(配置复杂、容易被滥用为垃圾邮件中继),推荐轻量方案:mailx(提供 mail 命令)+ msmtp(只负责发信的 SMTP 客户端)。
# CentOS / RHEL yum install -y mailx msmtp # Debian / Ubuntu apt-get install -y bsd-mailx msmtp msmtp-mta # /etc/msmtprc (权限必须是 600,否则 msmtp 拒绝运行) defaults auth on tls on tls_trust_file /etc/ssl/certs/ca-bundle.crt logfile /var/log/msmtp.log account ops host smtp.exmail.qq.com # 腾讯企业邮 port 465 # SSL 端口;587 为 STARTTLS from alert@yourdomain.com user alert@yourdomain.com password 你的授权码或密码 tls_starttls off # 465 用 off;587 改成 on account default : ops chmod 600 /etc/msmtprc # /etc/mail.rc 末尾追加,指定 mail 命令使用 msmtp 发信 set sendmail=/usr/bin/msmtp set from=alert@yourdomain.com set smtp=smtp.exmail.qq.com # 测试发信 echo "这是一封来自 $(hostname) 的测试邮件" | mail -s "告警通道测试" ops@yourdomain.com tail -f /var/log/msmtp.log
| 邮件服务商 | SMTP 主机 | SSL 端口 | STARTTLS 端口 | 备注 |
|---|---|---|---|---|
| 腾讯企业邮 | smtp.exmail.qq.com | 465 | 587 | 密码用客户端专用密码 |
| QQ 邮箱 | smtp.qq.com | 465 | 587 | 需在设置里开启 SMTP 并获取授权码 |
| 阿里云邮 | smtp.mxhichina.com | 465 | 80 | 端口较特殊 |
| Gmail | smtp.gmail.com | 465 | 587 | 需应用专用密码 |
测试不通过时按这个顺序查:一是 /etc/msmtprc 权限不是 600;二是云服务商默认封禁 25 端口(所以必须用 465/587);三是密码填成了登录密码而非"客户端授权码/应用专用密码";四是服务器 IP 在邮件服务商的风控名单里。先手动 echo test | mail -s test 你的邮箱 验证通道,再往下写检测脚本——通道不通就写脚本,排查起来会很痛苦。
三、故障检测脚本:把常见异常都覆盖上
下面是一个整合脚本,覆盖磁盘、内存、负载、服务存活、端口监听五类检测,内置了"连续 N 次才告警"的判定逻辑与状态去重,可以直接用。
#!/bin/bash
# /opt/scripts/server_alert.sh —— 综合异常检测与邮件告警
MAILTO="ops@yourdomain.com"
HOST=$(hostname)
IP=$(hostname -I | awk '{print $1}')
STATE_DIR=/var/tmp/alert_state # 状态目录,用于判定持续性与去重
mkdir -p "$STATE_DIR"
send() { # 用法: send "级别" "主题" "正文"
printf "主机: %s (%s)\n时间: %s\n级别: %s\n\n%s\n" \
"$HOST" "$IP" "$(date '+%F %T')" "$1" "$3" \
| mail -s "[$1] $HOST - $2" "$MAILTO"
}
# 连续 N 次异常才告警:check_persist "标识" "次数阈值" "当前是否异常(0异常/1正常)"
check_persist() {
key=$1; limit=$2; ok=$3
cnt_file="$STATE_DIR/$key"
if [ "$ok" != "0" ]; then rm -f "$cnt_file"; return 1; fi
cnt=$(cat "$cnt_file" 2>/dev/null || echo 0)
cnt=$((cnt+1)); echo "$cnt" > "$cnt_file"
[ "$cnt" -ge "$limit" ] && return 0 || return 1
}
# 1) 磁盘:>85% 预警,>90% 严重
while read used mount dev; do
case "$dev" in tmpfs|devtmpfs|overlay*) continue;; esac
if [ "$used" -ge 90 ]; then check_persist "disk_$mount" 2 0 && \
send "CRITICAL" "磁盘 ${used}%" "分区 $dev 挂载于 $mount,使用率 ${used}%"$'\n'"$(du -h --max-depth=2 $mount 2>/dev/null | sort -hr | head -6)"
elif [ "$used" -ge 85 ]; then check_persist "diskw_$mount" 3 0 && \
send "WARNING" "磁盘 ${used}%" "分区 $dev 挂载于 $mount,使用率 ${used}%"
else check_persist "disk_$mount" 1 1; check_persist "diskw_$mount" 1 1; fi
done < <(df -P | awk 'NR>1 {gsub("%","",$5); print $5, $6, $1}')
# 2) 内存:可用比例低于 10%
avail_pct=$(free | awk '/^Mem:/ {printf "%d", $7/$2*100}')
[ "$avail_pct" -lt 10 ] && check_persist "mem" 3 0 && \
send "CRITICAL" "可用内存不足" "当前可用内存占比 ${avail_pct}%"$'\n'"$(free -h)"
[ "$avail_pct" -ge 10 ] && check_persist "mem" 1 1
# 3) 负载:load5 / CPU核数 > 1.5
CORES=$(nproc)
LOAD5=$(awk '{print $2}' /proc/loadavg)
if awk -v l="$LOAD5" -v c="$CORES" 'BEGIN{exit !(l/c > 1.5)}'; then
check_persist "load" 3 0 && \
send "WARNING" "系统负载过高" "load5=$LOAD5,CPU核数=$CORES,比值=$(awk -v l=$LOAD5 -v c=$CORES 'BEGIN{printf "%.2f", l/c}')"$'\n'"$(ps aux --sort=-%cpu | head -8)"
else
check_persist "load" 1 1
fi
# 4) 关键服务存活检测
for svc in nginx mysqld redis docker; do
if systemctl list-unit-files | grep -q "^$svc.service"; then
if ! systemctl is-active --quiet "$svc"; then
check_persist "svc_$svc" 2 0 && \
send "CRITICAL" "服务已停止" "服务 $svc 未运行"$'\n'"$(systemctl status $svc | tail -8)"
else
check_persist "svc_$svc" 1 1
fi
fi
done
# 5) 端口监听检测
for port in 80 443 3306; do
ss -lnt | awk '{print $4}' | grep -q ":$port$" || \
check_persist "port_$port" 2 0 && send "CRITICAL" "端口未监听" "端口 $port 未在监听状态"
ss -lnt | awk '{print $4}' | grep -q ":$port$" && check_persist "port_$port" 1 1
done
脚本设计上有三个细节值得借鉴:check_persist 函数实现"连续 N 次才告警",正常时清零计数;磁盘告警正文里附带占用最大的目录,省去登录排查;告警标题带 [CRITICAL]/[WARNING] 前缀,方便在邮箱里用规则自动分类。
# 加入定时任务:每 5 分钟执行一次 chmod +x /opt/scripts/server_alert.sh crontab -e */5 * * * * /bin/bash /opt/scripts/server_alert.sh >/dev/null 2>&1 # 确认 cron 服务在运行 systemctl status cron # Debian/Ubuntu systemctl status crond # CentOS/RHEL
四、让 systemd 主动告诉你"服务挂了"
crontab 轮询有 5 分钟延迟,而且服务崩溃后如果被 systemd 自动重启,轮询可能刚好错过。更精准的做法是用 systemd 的 OnFailure:当服务进入 failed 状态时,systemd 会立刻触发指定动作。
# /etc/systemd/system/alert-mail@.service —— 通用告警单元,%i 是失败的服务名 [Unit] Description=Failure alert for %i [Service] Type=oneshot ExecStart=/bin/bash -c 'printf "服务: %%i\n主机: $(hostname)\n时间: $(date)\n\n$(systemctl status %%i | tail -10)\n" | \ /usr/bin/mail -s "[CRITICAL] $(hostname) 服务 %%i 异常退出" ops@yourdomain.com' # 使用方式一:在需要监控的服务里加 OnFailure # 编辑 /etc/systemd/system/myapp.service [Unit] Description=My Application OnFailure=alert-mail@%n.service [Service] ExecStart=/usr/local/bin/myapp Restart=always RestartSec=5s StartLimitBurst=5 StartLimitIntervalSec=300 [Install] WantedBy=multi-user.target # 使用方式二:不改原服务文件,用 drop-in 追加 mkdir -p /etc/systemd/system/nginx.service.d cat > /etc/systemd/system/nginx.service.d/alert.conf <<'EOF' [Unit] OnFailure=alert-mail@%n.service EOF systemctl daemon-reload systemctl restart nginx
顺带说明几个 systemd 自愈参数的含义:Restart=always 表示无论何种退出原因都重启(也可用 on-failure,仅在非 0 退出时重启);RestartSec=5s 是重启前等待 5 秒,避免立即重启打满日志;StartLimitBurst=5 配合 StartLimitIntervalSec=300 表示 300 秒内最多重启 5 次,超过后 systemd 放弃重启并把服务置为 failed——这个上限非常重要,否则一个启动即崩溃的服务会无限重启,刷爆日志并触发告警风暴。达到上限后进入 failed,恰好会触发 OnFailure,形成"自愈尝试 + 最终告警"的完整闭环。
五、告警分级与降噪
| 级别 | 触发条件示例 | 通知方式 | 响应要求 |
|---|---|---|---|
| INFO | 服务正常重启、备份完成 | 邮件汇总,不单独推送 | 无需处理 |
| WARNING | 磁盘 >85%、负载比值 >1.5 | 邮件 + 工作群 | 当天处理 |
| CRITICAL | 磁盘 >90%、服务停止、内存 <10% | 邮件 + 短信/电话 | 立即响应 |
降噪的三个有效手段:一是持续时间(如磁盘 90% 持续 10 分钟);二是同类合并(同一台机器 30 分钟内的告警合并成一封邮件);三是恢复通知(异常消失时发一封"已恢复",让人不必反复确认)。另外建议给邮件加过滤规则:WARNING 进文件夹,CRITICAL 设特殊提醒音,避免所有告警混在一起导致麻木。
六、落地实施步骤
- 先配通邮件通道:安装 mailx + msmtp,写
/etc/msmtprc(权限 600),用echo | mail -s test确认收到测试邮件。 - 确认 cron 服务在运行,准备好
/opt/scripts与状态目录/var/tmp/alert_state。 - 部署综合检测脚本,先用
bash -x手动执行一次,观察变量取值是否正确(尤其是avail_pct与LOAD5)。 - 人为制造一次异常(如临时把测试机磁盘写满到 91%),确认邮件按预期到达且内容包含定位信息。
- 为关键服务配置 systemd 的
Restart=always、RestartSec、StartLimitBurst,并挂上OnFailure。 - 加入 crontab,先设 30 分钟一次观察一周误报情况,再收紧到 5 分钟一次。
- 在邮件端配置过滤规则,把 WARNING 与 CRITICAL 分开。
- 每季度做一次告警演练:随机停掉一个非核心服务,确认从异常发生到邮件到达的时间在预期内。
小结
Linux 服务器异常自动告警并不需要一上来就上 Prometheus 全家桶。对几十台规模的中小环境,"msmtp 打通邮件 + Shell 脚本检测 + crontab 定时 + systemd OnFailure 兜底"这套轻量组合就能覆盖 90% 的场景,且维护成本极低。关键设计有三点:告警判定必须引入持续时间或连续次数,避免抖动误报;告警正文要附带定位信息(占用最大的目录、Top 进程、服务状态尾部日志),让人收到就能判断;服务自愈要配 StartLimitBurst 上限,防止无限重启。避坑清单:一是 /etc/msmtprc 权限不是 600 导致 msmtp 静默失败;二是使用被封禁的 25 端口;三是把登录密码当 SMTP 密码用;四是脚本里直接 mail 而 cron 环境变量 PATH 不完整导致命令找不到(脚本里用绝对路径可规避);五是只告警不验证,某天邮箱失效了半年都没人发现——定期演练是唯一可靠的验证方式。
关键词:Linux自动告警、邮件通知、msmtp配置、mailx发信、故障检测脚本、systemd OnFailure
企业QQ咨询




