前言
学了变量、条件、三剑客,是时候写个真正有用的东西了。下面是一个我维护了两年、持续迭代的服务器巡检脚本——每台机器上一份,cron 每天跑,异常发告警。
一、需求设计
- 检查六项:负载、内存、磁盘、网络、关键服务、安全项
- 分级输出:
OK/WARN(阈值临界)/FAIL(必须处理) - 退出码语义:0 全绿 / 1 有告警(供 cron 判断是否发通知)
- 兼容 RHEL 系(OpenCloudOS/CentOS/Rocky)
二、完整脚本
#!/usr/bin/env bash
#=============================================================
# server-inspect.sh —— 服务器每日巡检
# 用法: ./server-inspect.sh [-c cpu阈值] [-m 内存阈值] [-d 磁盘阈值]
#=============================================================
set -uo pipefail # 注意不用 -e: 巡检要"逐项跑完", 不能一条失败就退出
VERSION="2.3"
LOGFILE="/var/log/inspect.log"
# ---------- 可调参数 ----------
CPU_LIMIT=80 # CPU 使用率告警线 %
MEM_LIMIT=80 # 内存使用率告警线 %
DISK_LIMIT=80 # 磁盘使用率告警线 %
LOAD_FACTOR=2 # load 告警线 = 核数 * 该系数
SERVICES=(sshd nginx crond) # 关键服务白名单
while getopts "c:m:d:" opt; do
case $opt in
c) CPU_LIMIT=$OPTARG ;;
m) MEM_LIMIT=$OPTARG ;;
d) DISK_LIMIT=$OPTARG ;;
*) echo "用法: $0 [-c n] [-m n] [-d n]"; exit 2 ;;
esac
done
# ---------- 输出工具 ----------
RED=$'\033[31m'; YELLOW=$'\033[33m'; GREEN=$'\033[32m'; NC=$'\033[0m'
FAIL_COUNT=0; WARN_COUNT=0
ok() { printf "%s[ OK ]%s %s\n" "$GREEN" "$NC" "$*"; }
warn() { printf "%s[WARN]%s %s\n" "$YELLOW" "$NC" "$*"; WARN_COUNT=$((WARN_COUNT+1)); }
fail() { printf "%s[FAIL]%s %s\n" "$RED" "$NC" "$*"; FAIL_COUNT=$((FAIL_COUNT+1)); }
info() { printf "%s[INFO]%s %s\n" "" "$NC" "$*"; }
section() { printf "\n===== %s =====\n" "$*"; }
# ---------- 1. 系统概览 ----------
section "系统概览"
info "主机名: $(hostname)"
info "系统: $(grep PRETTY_NAME /etc/os-release | cut -d'"' -f2)"
info "内核: $(uname -r)"
info "运行: $(uptime -p)"
# ---------- 2. 负载与 CPU ----------
section "负载与 CPU"
CORES=$(nproc)
LOAD1=$(awk '{print int($1)}' /proc/loadavg)
LOAD_LIMIT=$((CORES * LOAD_FACTOR))
if (( LOAD1 > LOAD_LIMIT )); then
fail "1分钟负载 ${LOAD1} 超限 (阈值=${LOAD_LIMIT}, ${CORES}核)"
else
ok "负载正常: ${LOAD1} / ${CORES}核 (阈值=${LOAD_LIMIT})"
fi
CPU_USAGE=$(top -bn1 | awk '/^%Cpu/ {print int($2+$4)}') # us+sy
if (( CPU_USAGE > CPU_LIMIT )); then
warn "CPU 使用率 ${CPU_USAGE}% 超过 ${CPU_LIMIT}%"
info "Top3 进程: $(ps aux --sort=-%cpu | awk 'NR>1&&NR<5 {printf "%s(%s%%) ", $11, $3}')"
else
ok "CPU 使用率 ${CPU_USAGE}%"
fi
# ---------- 3. 内存与 swap ----------
section "内存"
MEM_PCT=$(free | awk '/Mem:/ {printf "%d", $3/$2*100}')
if (( MEM_PCT > MEM_LIMIT )); then
warn "内存使用率 ${MEM_PCT}% 超过 ${MEM_LIMIT}%"
info "Top3 进程: $(ps aux --sort=-%mem | awk 'NR>1&&NR<5 {printf "%s(%s%%) ", $11, $4}')"
else
ok "内存使用率 ${MEM_PCT}%"
fi
SWAP_USED=$(free -m | awk '/Swap:/ {print $3}')
if (( SWAP_USED > 100 )); then
warn "Swap 已用 ${SWAP_USED}MB(可能存在内存压力)"
else
ok "Swap 使用 ${SWAP_USED}MB"
fi
# ---------- 4. 磁盘 ----------
section "磁盘空间"
df -h -x tmpfs -x devtmpfs | awk 'NR>1 {
gsub(/%/,"",$5)
if ($5+0 >= 85) printf ">>> [FAIL] %s 使用率 %s%%\n", $6, $5
else if ($5+0 >= 70) printf ">>> [WARN] %s 使用率 %s%%\n", $6, $5
}' | while read -r line; do echo "$line"; done
df -h -x tmpfs -x devtmpfs | awk 'NR>1 {gsub(/%/,"",$5); if($5+0<'$DISK_LIMIT') exit 0; else exit 1}' \
&& ok "所有分区使用率低于 ${DISK_LIMIT}%" \
|| warn "存在分区使用率超过 ${DISK_LIMIT}%(详见上方明细)"
INODE_PCT=$(df -i / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
if (( INODE_PCT > DISK_LIMIT )); then
warn "根分区 inode 使用率 ${INODE_PCT}%(小文件过多?)"
else
ok "根分区 inode 使用率 ${INODE_PCT}%"
fi
# ---------- 5. 网络 ----------
section "网络"
GATEWAY=$(ip route | awk '/default/ {print $3; exit}')
if ping -c2 -W2 "$GATEWAY" &>/dev/null; then
ok "网关 ${GATEWAY} 可达"
else
fail "网关 ${GATEWAY} 不可达"
fi
EXT_IF=$(ip -br route get 1.1.1.1 2>/dev/null | awk '{print $5; exit}')
if ip link show "$EXT_IF" | grep -q "state UP"; then
ok "出口网卡 ${EXT_IF} 状态 UP"
else
fail "出口网卡 ${EXT_IF} 异常"
fi
ERRS=$(ip -s link show "$EXT_IF" 2>/dev/null | awk '/RX:/{getline; print $3+$4}')
if (( ${ERRS:-0} > 1000 )); then
warn "${EXT_IF} 收发错误/丢包累计 ${ERRS}"
else
ok "网卡无异常计数"
fi
# ---------- 6. 关键服务 ----------
section "关键服务"
for svc in "${SERVICES[@]}"; do
if systemctl is-active --quiet "$svc"; then
ok "服务 ${svc} 运行中"
else
fail "服务 ${svc} 未运行"
fi
done
# ---------- 7. 安全项 ----------
section "安全检查"
SSH_ROOT=$(sshd -T 2>/dev/null | awk '/^permitrootlogin/ {print $2}')
[[ "$SSH_ROOT" == "no" ]] && ok "SSH 已禁止 root 登录" \
|| warn "SSH 允许 root 登录(建议关闭)"
SSH_PWD=$(sshd -T 2>/dev/null | awk '/^passwordauthentication/ {print $2}')
[[ "$SSH_PWD" == "no" ]] && ok "SSH 已禁用密码登录" \
|| warn "SSH 仍允许密码登录(建议改密钥)"
LAST_FAIL=$(grep -c "Failed password" /var/log/secure 2>/dev/null || echo 0)
if (( LAST_FAIL > 100 )); then
warn "本日 SSH 爆破尝试 ${LAST_FAIL} 次(建议 fail2ban)"
else
ok "SSH 爆破尝试 ${LAST_FAIL} 次"
fi
REBOOT_REQ=$(dnf -q needs-restarting 2>/dev/null | wc -l)
if (( REBOOT_REQ > 0 )); then
warn "有 ${REBOOT_REQ} 个更新待重启生效"
else
ok "无待重启的更新"
fi
# ---------- 汇总 ----------
section "巡检结果汇总 ($(date '+%F %T'))"
info "告警: ${WARN_COUNT} 项 / 失败: ${FAIL_COUNT} 项"
if (( FAIL_COUNT > 0 )); then
fail "巡检不通过, 请立即处理"
exit 1
elif (( WARN_COUNT > 0 )); then
warn "巡检通过, 但存在告警项"
exit 0
else
ok "全部通过, 系统健康"
exit 0
fi
三、用到的技术点回放
| 技术 | 位置 |
|---|---|
set -uo pipefail(不用 -e) |
巡检要跑完全部项目 |
| getopts | 命令行阈值参数 |
| 数组 + for | 服务白名单遍历 |
| awk 算术 | df/free/top 的百分比提取 |
| 管道 + while read | 分区逐行判断 |
条件兜底 ${ERRS:-0} |
变量可能为空的防御 |
| 退出码 | 供上游判断 |
四、接入 cron 与告警
install -m 750 server-inspect.sh /opt/scripts/
# /etc/cron.d/inspect
0 9 * * * root /opt/scripts/server-inspect.sh > /var/log/inspect-$(date +\%F).log 2>&1
# 只在异常时发告警(钉钉 webhook)
curl -s -H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"$(hostname) 巡检FAIL\"}}" \
"https://oapi.dingtalk.com/robot/send?token=xxx"
五、扩展方向
- HTML 报告(
printf拼<table>,邮件正文直接看) - 多机汇总(fetch 各机报告到中控,awk 拼大表)
- 趋势记录(结果追加 CSV,画使用率曲线)
- 重构成 Ansible role / Python 版本(机器多时)
小结
一个好的巡检脚本标准:退出码可靠、输出可分级、阈值可配置、误报少。把这个脚本扔到你的每台服务器上,明早起来看 FAIL 项——你大概率会发现自己忽略已久的问题。
本文是「Linux 运维」系列第 10 篇。