Shell04 系统监控巡检脚本学习笔记
定位:生产级巡检脚本实战 · 日期:2026-10-16
产出:/usr/local/bin/monitor.sh + crontab 每天 8:00 自动执行
前置:Shell03 函数与数组(本笔记大量复用函数分层 + 数组名单)
一、生产巡检脚本的 5 条设计原则
| 原则 |
为什么 |
怎么做 |
| 配置与代码分离 |
运维改阈值/名单,不改逻辑 |
顶部"配置区"放数组和阈值 |
| 统一记录格式 |
输出能被日志系统/人一眼看懂 |
record() 函数统一打 PASS/WARN/ERROR |
| 退出码规范 |
监控系统(Zabbix/Prometheus)靠退出码判断 |
0=正常 1=告警 2=严重错误 |
| 日志落盘 |
出问题要回溯 |
同时输出屏幕 + 写 /var/log/monitor.log |
| 只读无副作用 |
巡检不能改系统状态 |
所有检查都是读命令(df/free/ss/ps) |
架构图
┌─────────────────────────────────────┐
│ 配置区(阈值 + 检查名单数组) │ ← 运维只改这里
├─────────────────────────────────────┤
│ 底层函数:log() / record() │ ← 日志 + 统一记录
├─────────────────────────────────────┤
│ 业务函数:check_cpu / check_mem ... │ ← 每个维度一个函数
├─────────────────────────────────────┤
│ 主流程 main():依次调用 + 汇总 + 退出码 │
└─────────────────────────────────────┘
二、完整脚本(最终版,含所有修复)
#!/bin/bash
# ============================================================
# monitor.sh —— 生产级系统巡检脚本
# 用法:bash monitor.sh
# 退出码:0=全部正常 1=有告警 2=有严重错误
# ============================================================
# ==================== 配置区(运维只改这里) ====================
CPU_LOAD_WARN=2.0 # 1分钟 load 告警阈值(一般设为 CPU 核数)
MEM_FREE_WARN=10 # 内存可用率低于多少 % 告警
DISK_USE_WARN=80 # 磁盘使用率高于多少 % 告警
INODE_USE_WARN=80 # inode 使用率高于多少 % 告警
SWAP_USE_WARN=50 # swap 使用率高于多少 % 告警
CHECK_PORTS=(8080 8443 9000 9001 9002) # 必须监听的端口
CHECK_PROCS=(nginx python3) # 必须运行的进程
CHECK_DISKS=(/ /mnt/c /mnt/d) # 必须检查的挂载点
LOG_FILE="/var/log/monitor.log" # 日志落盘路径
# ==================== 全局状态 ====================
PASS_COUNT=0
WARN_COUNT=0
ERROR_COUNT=0
# ==================== 底层:日志 ====================
log() {
local level=$1 msg=$2 ts
ts=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$ts] [$level] $msg"
echo "[$ts] [$level] $msg" >> "$LOG_FILE" 2>/dev/null
}
# 统一记录检查结果(所有 check_* 都调它)
record() {
local status=$1 item=$2 detail=$3
case $status in
PASS) PASS_COUNT=$((PASS_COUNT+1)); log "INFO" "✅ $item — $detail" ;;
WARN) WARN_COUNT=$((WARN_COUNT+1)); log "WARN" "⚠️ $item — $detail" ;;
ERROR) ERROR_COUNT=$((ERROR_COUNT+1)); log "ERROR" "❌ $item — $detail" ;;
esac
}
# ==================== 检查函数 ====================
# 1. CPU 负载
check_cpu() {
local load over
load=$(uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' ')
over=$(awk -v l="$load" -v t="$CPU_LOAD_WARN" 'BEGIN{print (l>t)?1:0}')
if [ "$over" = "1" ]; then
record "WARN" "CPU负载" "1分钟load=$load > 阈值$CPU_LOAD_WARN"
else
record "PASS" "CPU负载" "1分钟load=$load(阈值$CPU_LOAD_WARN)"
fi
}
# 2. 内存(注意:awk 匹配 /^Mem:/ 大小写敏感!)
check_mem() {
local total available free_pct low
total=$(free -m | awk '/^Mem:/{print $2}')
available=$(free -m | awk '/^Mem:/{print $7}')
free_pct=$(awk -v a="$available" -v t="$total" 'BEGIN{printf "%.1f", a/t*100}')
low=$(awk -v f="$free_pct" -v t="$MEM_FREE_WARN" 'BEGIN{print (f<t)?1:0}')
if [ "$low" = "1" ]; then
record "WARN" "内存" "可用率=${free_pct}% < 阈值${MEM_FREE_WARN}%(${available}M/${total}M)"
else
record "PASS" "内存" "可用率=${free_pct}%(${available}M/${total}M)"
fi
}
# 3. 磁盘使用率
check_disk() {
for mp in "${CHECK_DISKS[@]}"; do
local use
use=$(df -P "$mp" 2>/dev/null | awk 'NR==2{print $5}' | tr -d '%')
if [ -z "$use" ]; then
record "ERROR" "磁盘$mp" "挂载点不存在或无法读取"
continue
fi
if [ "$use" -ge "$DISK_USE_WARN" ]; then
record "WARN" "磁盘$mp" "使用率=${use}% >= 阈值${DISK_USE_WARN}%"
else
record "PASS" "磁盘$mp" "使用率=${use}%"
fi
done
}
# 4. inode(含健壮性:WSL 的 DrvFs 不支持 inode,显示 "-")
check_inode() {
for mp in "${CHECK_DISKS[@]}"; do
local use
use=$(df -iP "$mp" 2>/dev/null | awk 'NR==2{print $5}' | tr -d '%')
if [[ ! "$use" =~ ^[0-9]+$ ]]; then
record "PASS" "Inode$mp" "不支持 inode 统计(跳过)"
continue
fi
if [ "$use" -ge "$INODE_USE_WARN" ]; then
record "WARN" "Inode$mp" "使用率=${use}% >= 阈值${INODE_USE_WARN}%"
else
record "PASS" "Inode$mp" "使用率=${use}%"
fi
done
}
# 5. 端口监听
check_ports() {
for port in "${CHECK_PORTS[@]}"; do
if ss -tlnp 2>/dev/null | grep -q ":$port "; then
record "PASS" "端口$port" "监听中"
else
record "ERROR" "端口$port" "未监听"
fi
done
}
# 6. 关键进程
check_procs() {
for proc in "${CHECK_PROCS[@]}"; do
if pgrep -x "$proc" >/dev/null 2>&1; then
record "PASS" "进程$proc" "运行中"
else
record "ERROR" "进程$proc" "未运行"
fi
done
}
# 7. 内核错误日志
check_dmesg() {
local err_count
err_count=$(dmesg --level=err,crit,alert,emerg 2>/dev/null | wc -l)
if [ "$err_count" -gt 0 ]; then
record "WARN" "内核日志" "存在 ${err_count} 条 error/crit 级日志"
else
record "PASS" "内核日志" "无 error 级日志"
fi
}
# 8. swap(虚拟内存)
check_swap() {
local total used used_pct over
total=$(free -m | awk '/^Swap:/{print $2}')
used=$(free -m | awk '/^Swap:/{print $3}')
[ "$total" = "0" ] || [ -z "$total" ] && { record "PASS" "虚拟内存" "未启用 swap"; return; }
used_pct=$(awk -v a="$used" -v t="$total" 'BEGIN{printf "%.1f", a/t*100}')
over=$(awk -v u="$used_pct" -v t="$SWAP_USE_WARN" 'BEGIN{print (u>t)?1:0}')
if [ "$over" = "1" ]; then
record "WARN" "虚拟内存" "使用率=${used_pct}% > 阈值${SWAP_USE_WARN}%(${used}M/${total}M)"
else
record "PASS" "虚拟内存" "使用率=${used_pct}%(${used}M/${total}M)"
fi
}
# ==================== 主流程 ====================
main() {
log "INFO" "===== 巡检开始 ====="
check_cpu
check_mem
check_disk
check_inode
check_ports
check_procs
check_dmesg
check_swap
local total=$((PASS_COUNT + WARN_COUNT + ERROR_COUNT))
echo ""
log "INFO" "===== 巡检汇总 ====="
log "INFO" "总检查项: $total | 通过: $PASS_COUNT | 告警: $WARN_COUNT | 严重: $ERROR_COUNT"
if [ "$ERROR_COUNT" -gt 0 ]; then
log "ERROR" "巡检结果: 存在严重错误"
exit 2
elif [ "$WARN_COUNT" -gt 0 ]; then
log "WARN" "巡检结果: 存在告警"
exit 1
else
log "INFO" "巡检结果: 全部正常"
exit 0
fi
}
main "$@"
三、10 个命令详解
| 命令 |
作用 |
关键参数/用法 |
uptime |
开机时间 + 1/5/15 分钟 load |
load < 核数=正常,>核数=CPU排队 |
free -m |
内存使用(MB) |
看第 7 列 available(真正可用),不是 free |
df -P |
磁盘使用率 |
-P=POSIX 格式,强制一行不换行;第 5 列=使用率 |
tr -d '%' |
删除指定字符 |
把 "80%" 变 "80"(纯数字才能比较) |
df -iP |
inode 使用率 |
-i=查索引节点;生产必查(小文件耗尽 inode 的坑) |
grep -q |
安静匹配 |
不输出内容,只返退出码;脚本里"有没有"判断一律用 -q |
pgrep -x |
精确查进程名 |
不会匹配到 grep 自己(ps|grep 的经典坑);-x=完全相等 |
dmesg --level=err |
内核错误日志 |
硬件故障/OOM/磁盘错误先出现在这里 |
awk -v a="$x" |
传 shell 变量进 awk |
awk 有独立变量空间,必须用 -v 传;BEGIN 块做纯计算 |
inode(概念) |
文件索引节点 |
存元数据,文件名存在目录项;总数固定,满了写不进文件 |
awk 提取数值(运维最常用技巧)
free -m | awk '/^Mem:/{print $2}' # /^Mem:/ 只处理以 Mem: 开头的行,取第 2 列
df -P / | awk 'NR==2{print $5}' # NR==2 只处理第 2 行(跳过表头)
uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}'
awk -v 浮点比较(bash 只能比整数)
over=$(awk -v l="$load" -v t="$CPU_LOAD_WARN" 'BEGIN{print (l>t)?1:0}')
# -v 传变量;BEGIN 不读文件直接执行;三目运算输出 1/0
[ "$over" = "1" ] && echo "超阈值"
四、踩坑记录(血泪)
| 坑 |
现象 |
原因 |
修复 |
| inode DrvFs |
integer expression expected,使用率=-% |
WSL 的 /mnt/c /mnt/d 是 DrvFs,不支持 inode,df -i 输出 - |
加 [[ ! "$use" =~ ^[0-9]+$ ]] 判断,非数字跳过 |
| awk 大小写 |
内存/swap 显示 -nan%(M/M) |
脚本里写 /^MEM:/,free 输出是 Mem:(首字母大写其余小写),awk 正则大小写敏感 |
改成 /^Mem:/ 和 /^Swap:/ |
| 除零 nan |
0/0*100 = nan |
变量为空时 awk 算 0/0 |
加空值判断(swap total=0 时跳过) |
| cron 未安装 |
crontab: command not found |
Debian 最小镜像不含 cron |
apt install -y cron |
| vim Esc 失效 |
按 Esc 显示 ^[ |
终端不识别 ANSI 转义 |
用 Ctrl+[ 代替 Esc,或 EDITOR=nano crontab -e |
| crontab 覆盖 |
写入新任务后旧任务消失 |
echo \| crontab - 是整体替换,不是追加 |
追加用 (crontab -l; echo '新任务') \| crontab - |
五、crontab 定时部署
格式
分 时 日 月 周 命令
0 8 * * * /usr/local/bin/monitor.sh >> /var/log/monitor_cron.log 2>&1
覆盖 vs 追加
# 覆盖(全部替换)
echo '任务' | crontab -
# 追加(保留旧任务)
(crontab -l 2>/dev/null; echo '新任务') | crontab -
# 查看
crontab -l
生产三坑
| 坑 |
解决 |
| cron 的 PATH 很精简(只有 /usr/bin:/bin) |
脚本开头显式设 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
| 任务重叠(上一个没跑完下一个又启动) |
用 flock:/usr/bin/flock -n /tmp/monitor.lock 脚本 |
| 日志越来越大 |
用 logrotate 轮转(daily + rotate 7 + compress) |
WSL 注意
- WSL 不是 7x24 服务器,关机/休眠时 cron 不跑(配置方法和真实服务器一致)
service cron start 确保服务运行
- 验证:把时间改成 1 分钟后,等触发看
/var/log/monitor_cron.log
六、毕业成果
- ✅ 一个生产级巡检脚本(8 个检查维度,可扩展)
- ✅ 配置与代码分离(改阈值不改逻辑)
- ✅ 退出码规范(0/1/2,可接监控系统)
- ✅ crontab 每天 8:00 自动执行 + 日志落盘
- ✅ 健壮性处理(WSL DrvFs、空值、除零)
- ✅ 10 个运维高频命令实战掌握
扩展方向:加邮件/钉钉报警(退出码非 0 时发通知)、加日志关键字检查(grep ERROR)、加服务自动恢复(检测到端口挂了自动 service start)。
🔗 相关笔记
评论