先想清楚要检查什么
这一步比写代码重要
六项,都是这三十天学过的
- 磁盘使用率
- 超阈值告警。最常见的线上问题(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——那一天你会学到「什么时候不该自己造轮子」。