项目背景
在运维工作中,服务器的健康状态直接决定了线上服务的稳定性。传统的人工巡检方式不仅耗时,而且容易遗漏关键指标。本项目设计并实现了一套基于 Shell 脚本的企业级 Linux 服务器自动化巡检系统,用于对 CentOS 7 生产环境进行每日健康检查,覆盖 CPU、内存、磁盘、系统负载、关键服务、僵尸进程和网络连通性等核心指标。
项目目标
- 实现服务器核心指标的自动化采集与告警
- 建立标准化的巡检报告输出机制
- 通过 crontab 实现每日定时自动巡检
- 替代人工巡检,提升运维效率与故障发现速度
技术栈
| 技术 | 用途 |
|---|---|
| Shell / Bash | 脚本开发语言 |
| top / free / df | 系统性能指标采集 |
| systemctl | 服务状态管理 |
| journalctl | 服务日志查询 |
| bc | 浮点数计算与比较 |
| crontab | 定时任务调度 |
| ping | 网络连通性检测 |
项目结构
server-inspection/
├── inspect_simple.sh # 最简版脚本:纯命令拼接,适合入门理解
├── inspect_full.sh # 完整版脚本:模块化设计,生产环境可用
├── crontab_guide.sh # 定时任务配置教程
├── resume_guide.sh # 简历撰写指南
├── interview_qa.sh # 面试问答话术
└── 项目详细讲解.md # 项目文档
核心实现
1. 最简版脚本 inspect_simple.sh
最简版脚本完全由命令拼接组成,没有 if 判断、循环和函数,适合 Shell 新手快速理解 Linux 运维命令的组合使用。
#!/bin/bash
# 最简版服务器巡检脚本 (CentOS 7)
TIME=$(date "+%Y-%m-%d %H:%M:%S")
echo "========== 服务器巡检报告 [$TIME] =========="
# CPU使用率:取 top 中 idle 的相反值
echo "CPU使用率: $(top -bn1 | grep "Cpu(s)" | awk '{print 100-$8"%"}')"
# 内存使用率
echo "内存使用率: $(free -m | grep Mem | awk '{printf "%.1f%%", $3/$2*100}')"
# 磁盘使用率(根分区)
echo "磁盘使用率: $(df -h / | tail -1 | awk '{print $5}')"
# 服务状态
echo "MySQL状态: $(systemctl is-active mysqld 2>/dev/null || echo '未安装')"
echo "Nginx状态: $(systemctl is-active nginx 2>/dev/null || echo '未安装')"
echo "============================================"
逐行讲解:
#!/bin/bash:Shebang 行,指定使用 Bash 解释器执行。$(date ...):命令替换,将当前时间存入变量TIME。top -bn1:top的批处理模式,刷新一次后退出。grep "Cpu(s)" | awk '{print 100-$8"%"}':提取 CPU 空闲率,用 100 减去得到使用率。free -m | grep Mem:提取内存总量行。df -h / | tail -1 | awk '{print $5}':查看根分区磁盘使用率。systemctl is-active mysqld 2>/dev/null || echo '未安装':检查服务状态,2>/dev/null丢弃错误输出,服务不存在时显示"未安装"。
2. 完整版脚本 inspect_full.sh
完整版脚本按照配置层、初始化层、工具函数层、检测函数层、主流程层、清理层进行模块化设计,是企业级生产环境可用的版本。
配置区
CPU_THRESHOLD=80 # CPU 告警阈值
MEM_THRESHOLD=80 # 内存告警阈值
DISK_THRESHOLD=80 # 磁盘告警阈值
LOAD_THRESHOLD=5 # 系统负载告警阈值
REPORT_DIR="/var/log/server_inspection"
ALERT_LOG="$REPORT_DIR/alert.log"
REPORT_FILE="$REPORT_DIR/report_$(date +%Y%m%d).log"
REPORT_KEEP_DAYS=30
将阈值、目录、保留天数等配置变量化,方便后续修改,无需改动核心逻辑。
工具函数
write_alert() {
echo "[ALERT] [$NOW] $1" | tee -a "$ALERT_LOG"
ALERT_COUNT=$((ALERT_COUNT + 1))
}
write_log() {
echo "$1" >> "$REPORT_FILE"
}
check_threshold() {
local current=$1
local threshold=$2
local name=$3
if [ "$(echo "$current > $threshold" | bc)" -eq 1 ]; then
write_alert "$name 超过阈值!当前值: ${current}%, 阈值: ${threshold}%"
return 1
else
write_log "[OK] $name 正常,当前值: ${current}%"
return 0
fi
}
write_alert使用tee -a同时输出到控制台和告警日志文件,并累加告警计数器。check_threshold使用bc进行浮点数比较,解决 Shell 本身不支持小数比较的问题。local关键字限制变量作用域在函数内部,避免污染全局命名空间。
CPU 检测
check_cpu() {
write_log ""
write_log "========== CPU 检测 =========="
local cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d'%' -f1)
local cpu_used=$(echo "100 - $cpu_idle" | bc)
write_log "CPU 使用率: ${cpu_used}%"
write_log "CPU 空闲率: ${cpu_idle}%"
check_threshold "$cpu_used" "$CPU_THRESHOLD" "CPU使用率"
}
系统负载检测
check_load() {
write_log ""
write_log "========== 系统负载检测 =========="
local cpu_cores=$(grep -c processor /proc/cpuinfo)
local load_1min=$(awk '{print $1}' /proc/loadavg)
local load_5min=$(awk '{print $2}' /proc/loadavg)
local load_15min=$(awk '{print $3}' /proc/loadavg)
write_log "CPU核心数: ${cpu_cores}"
write_log "系统负载: 1分钟=${load_1min}, 5分钟=${load_5min}, 15分钟=${load_15min}"
if [ "$(echo "$load_1min > $LOAD_THRESHOLD" | bc)" -eq 1 ]; then
write_alert "系统负载过高!1分钟负载: ${load_1min}, 阈值: ${LOAD_THRESHOLD}"
else
write_log "[OK] 系统负载正常"
fi
}
/proc/loadavg 输出三个数值,分别代表 1 分钟、5 分钟、15 分钟的平均负载。通过对比 CPU 核心数,可以判断当前负载是否处于合理区间。
内存检测
check_memory() {
write_log ""
write_log "========== 内存检测 =========="
local mem_total=$(free -m | grep Mem | awk '{print $2}')
local mem_used=$(free -m | grep Mem | awk '{print $3}')
local mem_available=$(free -m | grep Mem | awk '{print $7}')
local mem_usage=$(echo "scale=1; $mem_used / $mem_total * 100" | bc)
write_log "内存总量: ${mem_total}MB"
write_log "内存已用: ${mem_used}MB"
write_log "内存可用: ${mem_available}MB"
write_log "内存使用率: ${mem_usage}%"
check_threshold "$mem_usage" "$MEM_THRESHOLD" "内存使用率"
}
磁盘检测
check_disk() {
write_log ""
write_log "========== 磁盘检测 =========="
df -h | grep -vE 'tmpfs|devtmpfs|Filesystem|cdrom' | while read -r line; do
[ -z "$line" ] && continue
local disk_usage=$(echo "$line" | awk '{print $5}' | cut -d'%' -f1)
local disk_mount=$(echo "$line" | awk '{print $6}')
local disk_size=$(echo "$line" | awk '{print $2}')
local disk_avail=$(echo "$line" | awk '{print $4}')
write_log "挂载点: ${disk_mount}, 总大小: ${disk_size}, 可用: ${disk_avail}, 使用率: ${disk_usage}%"
if [ "$disk_usage" -gt "$DISK_THRESHOLD" ]; then
write_alert "磁盘 ${disk_mount} 使用率过高!当前值: ${disk_usage}%, 阈值: ${DISK_THRESHOLD}%"
fi
done
}
通过 grep -vE 排除临时文件系统,只检查真实磁盘分区。使用 while read -r line 逐行处理 df -h 的输出。
服务状态检测
check_service() {
local service_label=$1
local service_name=$2
write_log ""
write_log "========== ${service_label} 服务检测 =========="
if ! systemctl list-unit-files | grep -q "^${service_name}"; then
write_log "${service_label} 服务未安装"
write_alert "${service_label} 服务未安装!"
return
fi
local status=$(systemctl is-active "$service_name" 2>/dev/null)
if [ "$status" = "active" ]; then
write_log "[OK] ${service_label} 运行正常,状态: ${status}"
local enabled_status=$(systemctl is-enabled "$service_name" 2>/dev/null)
write_log "开机自启: ${enabled_status}"
if [ "$enabled_status" != "enabled" ]; then
write_alert "${service_label} 未设置开机自启!"
fi
else
write_alert "${service_label} 服务异常!状态: ${status}"
write_log "最近服务日志:"
journalctl -u "$service_name" --no-pager -n 5 >> "$REPORT_FILE" 2>/dev/null
fi
}
该函数为通用服务检测函数,接受服务显示名称和 systemctl 服务名两个参数。除了检查运行状态,还会检查是否开机自启,服务异常时自动拉取最近 5 条日志。
僵尸进程检测
check_zombie() {
write_log ""
write_log "========== 僵尸进程检测 =========="
local zombie_count=$(ps aux | awk '{if($8=="Z") print}' | wc -l)
write_log "僵尸进程数: ${zombie_count}"
if [ "$zombie_count" -gt 0 ]; then
write_alert "存在 ${zombie_count} 个僵尸进程!"
ps aux | awk '{if($8=="Z") print}' >> "$REPORT_FILE"
else
write_log "[OK] 无僵尸进程"
fi
}
网络连通性检测
check_network() {
write_log ""
write_log "========== 网络连通性检测 =========="
if ping -c 1 -W 2 114.114.114.114 > /dev/null 2>&1; then
write_log "[OK] 外网连通正常"
else
write_alert "外网不通!"
fi
}
> /dev/null 2>&1 将标准输出和标准错误都丢弃,只保留命令的退出状态。
主流程
main() {
cat > "$REPORT_FILE" << EOF
============================================================
服务器巡检报告
生成时间: ${NOW}
服务器主机名: $(hostname)
服务器IP: $(hostname -I | awk '{print $1}')
============================================================
EOF
check_cpu
check_load
check_memory
check_disk
check_service "MySQL" "mysqld"
check_service "Nginx" "nginx"
check_zombie
check_network
cat >> "$REPORT_FILE" << EOF
============================================================
巡检完成
告警数量: ${ALERT_COUNT}
报告路径: ${REPORT_FILE}
告警日志: ${ALERT_LOG}
============================================================
EOF
}
日志自动清理
cleanup_old_reports() {
find "$REPORT_DIR" -name "report_*.log" -mtime +$REPORT_KEEP_DAYS -delete 2>/dev/null
find "$REPORT_DIR" -name "alert.log" -size +10M -exec mv {} "${ALERT_LOG}.old" \; 2>/dev/null
}
自动删除超过 30 天的历史报告,并对超过 10MB 的告警日志进行轮转,避免磁盘空间被占满。
定时任务配置
通过 crontab -e 添加定时任务,实现每天凌晨 2 点自动巡检:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
# 每天凌晨2点执行巡检
0 2 * * * /opt/server-inspection/inspect_full.sh >> /var/log/server_inspection/cron.log 2>&1
在 crontab 中显式设置 SHELL 和 PATH 非常重要,因为 cron 执行时的环境变量很少,很多命令可能找不到。>> /var/log/xxx.log 2>&1 将正常输出和错误输出都追加到日志文件中,便于后续排查。
项目成果
- 7 项核心指标全自动巡检:CPU、内存、磁盘、系统负载、MySQL、Nginx、僵尸进程、网络连通性。
- 巡检耗时从人工 30 分钟降至脚本 5 秒以内。
- 故障发现方式从被动等待用户反馈变为主动告警通知。
- 巡检报告可追溯,支持历史查询和故障复盘。
- 日志自动轮转清理,避免磁盘空间被历史日志占满。
面试高频问题
Q1:脚本是怎么设计的?
采用分层设计思想:
- 配置层:阈值、路径、保留天数变量化。
- 检测层:7 个独立函数分别负责不同指标。
- 告警层:通用阈值比较函数 + 告警写入函数。
- 调度层:crontab 定时执行 + 自动清理旧报告。
Q2:脚本在 crontab 里不执行怎么排查?
systemctl status crond检查 cron 服务是否运行。crontab -l检查配置是否正确。tail -f /var/log/cron检查 cron 是否调度。- 检查脚本权限
chmod +x、Shebang 行、绝对路径。 - 通过
>> /path.log 2>&1查看具体报错。
Q3:磁盘使用率超过 90% 怎么处理?
只做告警,不做自动清理。因为自动清理磁盘有风险,可能误删重要数据。正确的做法是:
- 通过
df -h定位高使用率分区。 - 通过
du -sh *定位大文件或大目录。 - 人工确认后,再决定是归档、压缩还是删除。
写在最后
这个项目虽然代码量不大,但完整覆盖了 Shell 脚本开发、Linux 系统管理、服务监控、定时任务、日志管理等运维核心技能。通过最简版到完整版的渐进式设计,既能帮助初学者理解基础命令,也能作为生产环境的运维工具直接使用。