← 回目录
28 Week 06 · 脚本与收尾 · 产出日

写一个真正的巡检脚本

今天的产物,就是你简历上那句「写过自动化巡检脚本」的底气。自己敲,别复制——面试时你要能讲清每一行为什么这么写。

先想清楚要检查什么

这一步比写代码重要
六项,都是这三十天学过的
磁盘使用率
超阈值告警。最常见的线上问题(Day 21)。
内存使用率
接近满会触发 OOM 杀进程(Day 11)。
系统负载
跟 CPU 核数比(Day 11)。
关键服务
该跑的有没有在跑(Day 12)。
SSH 爆破
登录失败次数异常,是被攻击的信号(Day 05、20)。
系统错误日志
journalctl -p err 有没有新错误(Day 13)。

完整脚本

/opt/check.sh
第一部分:头和配置
#!/bin/bash
#===============================================
# check.sh —— 服务器日常巡检
# 用法: ./check.sh   或挂到 crontab 每天执行
#===============================================

set -u          # 用了未定义变量就报错(Day 26)
                # 注意:不加 -e,某项失败要继续检查后面的

#—— 可调参数,都集中放在开头 ——
LOG="/var/log/check.log"
DISK_MAX=80                        # 磁盘告警阈值 %
MEM_MAX=85                         # 内存告警阈值 %
SSH_FAIL_MAX=10                    # SSH 失败次数阈值
SERVICES="sshd crond nginx"        # 要检查的服务

WARN=0                             # 告警计数器
为什么把参数放开头

阈值和服务列表会经常改。集中放在最上面,下次调整不用在代码里翻找——这是个很基本但很多人不做的好习惯。

第二部分:两个辅助函数
#—— 输出函数:同时打屏幕和写日志 ——
log() {
    echo "$1" | tee -a "$LOG"
}

warn() {
    log "[告警] $1"
    WARN=$((WARN + 1))
}

ok() {
    log "[正常] $1"
}
tee -a 的作用

tee 是"三通"——把内容同时送到屏幕和文件-a 是追加。

这样你手动跑能看见输出,crontab 跑也会留下日志。比单纯 >> 好用。

第三部分:六项检查
log ""
log "===== 巡检开始 $(date '+%F %T') 主机: $(hostname) ====="

#—— 1. 磁盘 ——
while read -r part use; do
    if [ "$use" -gt "$DISK_MAX" ]; then
        warn "磁盘 $part 已用 ${use}%(阈值 ${DISK_MAX}%)"
    else
        ok "磁盘 $part 已用 ${use}%"
    fi
done < <(df -h | awk 'NR>1 && $1 ~ /^\/dev/ {gsub(/%/,"",$5); print $6, $5}')

#—— 2. 内存 ——
MEM=$(free -m | awk 'NR==2 {printf "%.0f", $3/$2*100}')
if [ "$MEM" -gt "$MEM_MAX" ]; then
    warn "内存已用 ${MEM}%(阈值 ${MEM_MAX}%)"
else
    ok "内存已用 ${MEM}%"
fi

#—— 3. 负载(跟核数比)——
CORES=$(nproc)
LOAD=$(uptime | awk -F'load average:' '{print $2}' | awk -F, '{print $1}' | tr -d ' ')
LOAD_INT=${LOAD%.*}
if [ "$LOAD_INT" -ge "$CORES" ]; then
    warn "负载 $LOAD 已达到或超过核数 $CORES"
else
    ok "负载 $LOAD / ${CORES} 核"
fi

#—— 4. 关键服务 ——
for svc in $SERVICES; do
    if systemctl is-active --quiet "$svc"; then
        ok "服务 $svc 运行中"
    else
        warn "服务 $svc 未运行"
    fi
done

#—— 5. SSH 爆破 ——
FAILS=$(grep -c "Failed password" /var/log/secure 2>/dev/null || echo 0)
if [ "$FAILS" -gt "$SSH_FAIL_MAX" ]; then
    warn "SSH 登录失败 ${FAILS} 次,可能被爆破"
    log "      来源 IP TOP3:"
    grep "Failed password" /var/log/secure 2>/dev/null \
      | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -3 \
      | while read -r n ip; do log "      $ip  ${n} 次"; done
else
    ok "SSH 登录失败 ${FAILS} 次"
fi

#—— 6. 系统错误日志 ——
ERRS=$(journalctl -p err -b --no-pager 2>/dev/null | wc -l)
if [ "$ERRS" -gt 20 ]; then
    warn "本次开机以来有 ${ERRS} 条错误日志"
else
    ok "错误日志 ${ERRS} 条"
fi
第四部分:收尾
log "===== 巡检结束,共 ${WARN} 项告警 ====="

# 用退出码表示结果,方便被监控系统调用(Day 26 的 $?)
if [ "$WARN" -gt 0 ]; then
    exit 1
else
    exit 0
fi

几处值得单独说

面试可能会追问的细节
为什么不用 set -e
巡检的逻辑是"查完所有项"。加了 -e,第一个服务异常脚本就退出了,后面五项全不查——那就失去巡检的意义了。
< <(命令) 是什么
进程替换。如果写成 命令 | while read,循环会在子 shell 里跑,里面改的 WARN 计数出来就没了。这个写法能避开这个坑。
2>/dev/null || echo 0
/var/log/secure 可能不存在或没权限。把错误吞掉并给个默认值 0,否则变量为空会让后面的比较报错。
exit 1 / exit 0
退出码表示有没有告警。这样 Zabbix、Prometheus 这类监控系统可以直接调用这个脚本,靠返回值判断是否需要报警

装上并跑起来

部署
$ sudo vi /opt/check.sh          # 把脚本写进去
$ sudo chmod +x /opt/check.sh    # Day 06:要有 x 权限
$ sudo bash -n /opt/check.sh     # Day 26:先查语法
$ sudo /opt/check.sh             # 手动跑一次
预期输出
===== 巡检开始 2026-09-07 15:40:02 主机: rocky =====
[正常] 磁盘 / 已用 21%
[正常] 磁盘 /boot 已用 25%
[正常] 内存已用 34%
[正常] 负载 0.15 / 4 核
[正常] 服务 sshd 运行中
[正常] 服务 crond 运行中
[正常] 服务 nginx 运行中
[正常] SSH 登录失败 0 次
[正常] 错误日志 3 条
===== 巡检结束,共 0 项告警 =====

挂到 crontab

Day 25 的两个坑都在这
每天早 8 点自动跑
$ sudo crontab -e

# 加这一行(绝对路径 + 输出重定向,两个坑都避开了)
0 8 * * * /opt/check.sh >> /var/log/check-cron.log 2>&1
先验证能在 cron 环境下跑
$ sudo env -i /bin/bash --noprofile --norc /opt/check.sh
     ↑ Day 25 学的,模拟 cron 的精简环境
       如果这样能跑通,放进 crontab 基本没问题
如果报 command not found

说明有命令不在 cron 的 PATH 里。解法:在脚本开头加一行

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

怎么把它讲成一个项目

今天真正的产出
「说一个你做过的自动化的事」

背景:「之前每天早上要登录几台服务器,一台台看磁盘、内存、服务状态,重复且容易漏。」

做法:「我写了个巡检脚本,检查六项——磁盘、内存、负载、关键服务、SSH 爆破次数、系统错误日志。超过阈值就记告警,阈值和服务列表都放在脚本开头方便调整。挂到 crontab 每天早 8 点跑,结果写日志。」

效果:「现在早上直接看一份报告就行,而且脚本用退出码表示有没有告警,后面可以直接接进监控系统。

踩过的坑:「一开始手动跑好好的,放进 crontab 就不执行——是 cron 的 PATH 太精简找不到命令,改成绝对路径就好了。另外输出一定要重定向,不然出错都不知道。

为什么这样讲有力量

背景 → 做法 → 效果 → 踩过的坑,四段齐全。

尤其最后那段坑——能说出具体踩过什么坑、怎么解决的,是"真做过"和"背过"最明显的分界线。面试官几乎一定会追问细节,而你答得出来。

今天的任务

约 30 分钟,今天时间长一点
  • 手敲整个脚本到 /opt/check.sh(别复制粘贴,敲的过程就是理解的过程)
  • bash -n 查语法,chmod +x,手动跑一次看输出
  • 制造一次真告警:把 DISK_MAX 改成 10,重跑,看告警分支和计数器工作没
  • 再制造一次sudo systemctl stop nginx,重跑看服务告警,然后起回来
  • echo $? 看退出码是不是 1(有告警时)
  • env -i 模拟 cron 环境跑一次,确认没有 command not found
  • 挂到 crontab(改成 */5 * * * * 每 5 分钟,方便验证),等 5 分钟看 /var/log/check.log
  • 最后:把上面那段"背景→做法→效果→坑"用自己的话说一遍出声
明天预告

Day 29 写备份脚本,并认识 logrotate——那一天你会学到「什么时候不该自己造轮子」。