背景与适用场景
服务器巡检是运维工作中最基础也最容易被忽视的工作之一。巡检做得好,能在故障发生之前发现隐患;巡检做得敷衍,往往是"平时不巡检,出事干瞪眼"。但现实情况是:很多团队的巡检就是登录服务器敲几个命令,把结果截图往群里一丢就算交差了——既没有系统性的数据收集,也没有历史对比,更谈不上告警联动。
本篇的目标是用 Shell 脚本实现服务器巡检报告的自动生成,最终输出一个结构化的 Markdown 文件。这个方案的特点:
一键执行:一条命令收集一台或多台服务器的全部巡检数据
结构清晰:按系统、硬件、应用三层组织巡检内容
可存档、可对比:Markdown 格式便于存档,也可以用 Git 管理历史版本
可扩展:在统一框架下添加新的巡检项非常容易
无需额外依赖:纯 Shell + 系统自带工具,不依赖 Ansible、Salt 等重型工具
适用场景:
每日/每周例行巡检报告生成
交接时对服务器现状的摸底记录
故障排查前对系统健康状况的基线采集
迁移或升级前的现状确认
巡检报告框架设计
报告结构
一份完整的服务器巡检报告应该包含以下内容:
# 服务器巡检报告 ## 基本信息 - 主机名、操作系统版本、内核版本 - 当前时间、运行时长、负载状态 ## 系统资源 - CPU:型号、核心数、负载平均值 - 内存:总量、使用量、剩余量、使用率 - 磁盘:各分区使用率、IO 等待 ## 网络状态 - 网卡配置、IP 地址、网关 - 端口监听情况 - 网络连接数统计 ## 运行服务 - 关键服务运行状态(Nginx、MySQL、Redis 等) - 开机自启配置 ## 系统日志 - 近期的错误和警告日志(最近 24 小时内) ## 安全状态 - 用户账户(特权用户、非活跃账户) - 登录记录(近期登录来源) - SElinux / AppArmor 状态 ## 应用层 - 数据库状态(连接数、版本) - 中间件状态 - 业务进程状态 ## 磁盘 inode 使用情况 - 文件系统 inode 使用率 ## 历史性能(采集时刻) - top5 进程(按 CPU、内存排序) - 历史负载、内存趋势 ## 巡检结论 - 发现的问题和建议
脚本目录结构
~/server-inspection/ ├── inspect.sh # 主脚本(部署到每台服务器执行) ├── collect_remote.sh # 远程采集脚本(从一台机器批量采集多台) ├── report_template.md # Markdown 报告模板 └── output/ # 报告输出目录 ├── 2024-01-15-web-01.md └── 2024-01-15-db-01.md
本地巡检脚本:inspect.sh
这是部署到每台服务器上执行的巡检脚本,负责收集本机数据并生成 Markdown 格式的巡检报告。
#!/bin/bash
# ~/server-inspection/inspect.sh
# 服务器巡检脚本 - 在每台服务器上执行,生成 Markdown 格式巡检报告
# 用法: ./inspect.sh [输出目录]
set -euo pipefail
# 输出目录,默认当前目录
OUTPUT_DIR="${1:-.}"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
REPORT_DATE=$(date "+%Y-%m-%d")
SHORT_DATE=$(date "+%Y%m%d%H%M%S")
# 获取主机名和 IP
HOSTNAME=$(hostname)
PRIMARY_IP=$(hostname -I | awk '{print $1}')
OS_VERSION=$(cat /etc/redhat-release 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)
KERNEL=$(uname -r)
UPTIME_DAYS=$(uptime -p 2>/dev/null || uptime)
LOAD_AVG=$(cat /proc/loadavg | awk '{print $1, $2, $3}')
BOOT_TIME=$(who -b | awk '{print $3, $4}')
# 颜色定义(用于终端输出,非必须)
RED='�33[0;31m'
GREEN='�33[0;32m'
YELLOW='�33[1;33m'
NC='�33[0m'
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 确保输出目录存在
mkdir -p "$OUTPUT_DIR"
OUTPUT_FILE="${OUTPUT_DIR}/${REPORT_DATE}-${HOSTNAME}.md"
# ========== 开始生成报告 ==========
log_info "开始生成巡检报告: ${HOSTNAME}"
log_info "输出文件: ${OUTPUT_FILE}"
# 生成 Markdown 报告
cat > "$OUTPUT_FILE" << EOF
# 服务器巡检报告
**生成时间**: ${TIMESTAMP}
**主机名**: ${HOSTNAME}
**主 IP 地址**: ${PRIMARY_IP}
**操作系统**: ${OS_VERSION}
**内核版本**: ${KERNEL}
**运行时长**: ${UPTIME_DAYS}
**最近重启**: ${BOOT_TIME}
**负载平均值**: ${LOAD_AVG}
---
## 1. 系统基本信息
```bash
# 主机名
hostname: ${HOSTNAME}
# 操作系统版本
cat /etc/redhat-release:
$(cat /etc/redhat-release 2>/dev/null || echo "Non-RHEL system")
# 内核版本
uname -r: ${KERNEL}
# 系统位数
uname -m: $(uname -m)
# 最近重启时间
who -b: ${BOOT_TIME}
```
EOF
# ========== CPU 巡检 ==========
log_info "采集 CPU 信息..."
CPU_MODEL=$(cat /proc/cpuinfo | grep "model name" | head -1 | cut -d':' -f2 | sed 's/^ //')
CPU_CORES=$(nproc --all 2>/dev/null || echo "$(cat /proc/cpuinfo | grep processor | wc -l)")
CPU_PHYSICAL=$(cat /proc/cpuinfo | grep "physical id" | sort -u | wc -l)
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print "用户态: " $2 "%, 系统态: " $4 "%, 空闲: " $8 "%"}')
LOAD_AVG_15=$(cat /proc/loadavg | awk '{print $3}')
cat >> "$OUTPUT_FILE" << EOF
## 2. CPU 信息
**CPU 型号**: ${CPU_MODEL}
**物理核心数**: ${CPU_PHYSICAL}
**逻辑核心数**: ${CPU_CORES}
**当前使用率**: ${CPU_USAGE}
**15分钟负载**: ${LOAD_AVG_15}
```bash
# CPU 详细信息
lscpu:
$(lscpu 2>/dev/null | head -30)
# 实时 CPU 使用率
top -bn1 | head -10:
$(top -bn1 | head -10)
```
EOF
# ========== 内存巡检 ==========
log_info "采集内存信息..."
MEM_TOTAL=$(free -b | awk 'NR==2{printf "%.2f GB", $2/1024/1024/1024}')
MEM_USED=$(free -b | awk 'NR==2{printf "%.2f GB", $3/1024/1024/1024}')
MEM_FREE=$(free -b | awk 'NR==2{printf "%.2f GB", $4/1024/1024/1024}')
MEM_AVAILABLE=$(free -b | awk 'NR==2{printf "%.2f GB", $7/1024/1024/1024}')
MEM_USED_PCT=$(free | awk 'NR==2{printf "%.1f", $3/$2*100}')
SWAP_TOTAL=$(free -b | awk 'NR==3{printf "%.2f GB", $2/1024/1024/1024}')
SWAP_USED=$(free -b | awk 'NR==3{printf "%.2f GB", $3/1024/1024/1024}')
SWAP_USED_PCT=$(free | awk 'NR==3{printf "%.1f", $2==0?0:$3/$2*100}')
cat >> "$OUTPUT_FILE" << EOF
## 3. 内存信息
**物理内存总量**: ${MEM_TOTAL}
**已使用**: ${MEM_USED} (${MEM_USED_PCT}%)
**空闲**: ${MEM_FREE}
**可用(包含 cache/buffer)**: ${MEM_AVAILABLE}
**Swap 总量**: ${SWAP_TOTAL}
**Swap 已使用**: ${SWAP_USED} (${SWAP_USED_PCT}%)
> **巡检关注点**:如果 Swap 使用率持续高于 30%,说明物理内存可能不足。如果 Swap 已用为 0 但可用内存很少,说明 cache/buffer 被大量使用,这是 Linux 的正常行为,不需要恐慌。
```bash
# 内存详细使用情况
free -h:
$(free -h)
# 按进程内存使用排序(Top 10)
ps aux --sort=-%mem | head -11:
$(ps aux --sort=-%mem | head -11)
```
EOF
# ========== 磁盘巡检 ==========
log_info "采集磁盘信息..."
# 检查根分区和重要分区使用率
DISK_ALERT="正常"
df -h | awk 'NR>1 {
usage=$5;
sub(/%/, "", usage);
if (usage > 80) {
print "WARN: "$6" 使用率 " usage "%";
}
}' | while read line; do
DISK_ALERT="异常"
done
cat >> "$OUTPUT_FILE" << EOF
## 4. 磁盘信息
**整体状态**: ${DISK_ALERT}
### 4.1 文件系统使用情况
```
$(df -h | head -30)
```
### 4.2 磁盘 IO 统计
```bash
# 设备吞吐和 IOPS
iostat -x 1 2 | tail -n +7:
$(iostat -x 1 2 2>/dev/null | tail -n +7 || echo "iostat not available (install sysstat)")
# 每秒 IO 操作数
iostat -d 1 2 | tail -n +4:
$(iostat -d 1 2 2>/dev/null | tail -n +4 || echo "iostat not available")
```
### 4.3 inode 使用情况
```
$(df -i | head -20)
```
> **巡检关注点**:
> - 文件系统使用率超过 80% 需要关注,超过 90% 需要立即处理
> - inode 使用率超过 80% 同样需要关注(小文件过多时可能出现)
> - 如果 /var/log 或 /data 接近满载,优先清理历史日志
EOF
# ========== 网络巡检 ==========
log_info "采集网络信息..."
LISTENING_PORTS=$(ss -tlnp 2>/dev/null | awk 'NR>1 {print $4}' | sort -u)
ESTABLISHED_COUNT=$(ss -s 2>/dev/null | grep ESTAB | awk '{print $4}')
TIME_WAIT_COUNT=$(ss -s 2>/dev/null | grep TIME-WAIT | awk '{print $4}')
CLOSE_WAIT_COUNT=$(ss -s 2>/dev/null | grep CLOSE-WAIT | awk '{print $4}')
cat >> "$OUTPUT_FILE" << EOF
## 5. 网络信息
### 5.1 网卡配置
```bash
# IP 地址配置
ip addr show:
$(ip addr show | grep -E '^[0-9]+:|inet ' | head -30)
# 网卡速率和连接状态
cat /proc/net/dev:
$(cat /proc/net/dev | head -10)
```
### 5.2 端口监听情况
```bash
# 监听端口汇总
ss -tlnp | sort -k1,1n:
$(ss -tlnp 2>/dev/null | head -30 || ss -tln | head -30)
```
### 5.3 网络连接统计
**当前状态**:
- ESTABLISHED: ${ESTABLISHED_COUNT}
- TIME-WAIT: ${TIME_WAIT_COUNT}
- CLOSE-WAIT: ${CLOSE_WAIT_COUNT}
> **巡检关注点**:
> - TIME-WAIT 过多(>10000)可能是短连接过多导致,可能需要调整 `tcp_tw_reuse` 内核参数
> - CLOSE-WAIT 存在说明对端已关闭但本地未关闭 socket,需要检查应用层是否有连接泄漏
> - 如果 CLOSE-WAIT 持续不为 0 且数量较大,需要排查
```bash
# 按状态统计连接数
ss -s:
$(ss -s 2>/dev/null || echo "ss not available")
# 外部连接情况(谁在连接本机)
ss -tnp | grep ESTAB | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10:
$(ss -tnp 2>/dev/null | grep ESTAB | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -10 || echo "need root")
```
### 5.4 路由表
```
$(ip route show 2>/dev/null || route -n 2>/dev/null)
```
EOF
# ========== 运行服务巡检 ==========
log_info "采集服务状态..."
# 常用服务列表(根据实际情况调整)
SERVICES_TO_CHECK="nginx httpd mysqld redis docker sshd crond rsyslog firewalld ufw"
cat >> "$OUTPUT_FILE" << EOF
## 6. 运行服务状态
### 6.1 关键服务状态
| 服务名 | 状态 | 开机自启 | 内存占用 |
|--------|------|----------|----------|
EOF
for svc in $SERVICES_TO_CHECK; do
if systemctl list-unit-files | grep -q "^$svc.service"; then
STATUS=$(systemctl is-active $svc 2>/dev/null || echo "unknown")
ENABLED=$(systemctl is-enabled $svc 2>/dev/null || echo "unknown")
MEM=$(ps aux 2>/dev/null | grep -E "[/]$svc " | awk '{sum+=$6} END {printf "%.1f MB", sum/1024}')
echo "| $svc | $STATUS | $ENABLED | $MEM |" >> "$OUTPUT_FILE"
fi
done
cat >> "$OUTPUT_FILE" << EOF
### 6.2 全部运行中服务
```bash
systemctl list-units --type=service --state=running --no-pager:
$(systemctl list-units --type=service --state=running --no-pager 2>/dev/null | head -40)
```
### 6.3 CPU/内存占用 Top 10 进程
```bash
# 按 CPU 使用排序
ps aux --sort=-%cpu | head -11:
$(ps aux --sort=-%cpu | head -11)
# 按内存使用排序
ps aux --sort=-%mem | head -11:
$(ps aux --sort=-%mem | head -11)
```
EOF
# ========== 系统日志巡检 ==========
log_info "采集系统日志..."
cat >> "$OUTPUT_FILE" << EOF
## 7. 系统日志(最近 24 小时)
### 7.1 错误级别日志(Error 及以上)
```
$(journalctl --since "24 hours ago" --priority=err --no-pager 2>/dev/null | tail -30 || echo "journalctl not available")
```
### 7.2 警告级别日志
```
$(journalctl --since "24 hours ago" --priority=warning --no-pager 2>/dev/null | tail -20 || echo "journalctl not available")
```
### 7.3 SSH 登录记录
```bash
# 成功登录记录
last -20 | grep -v reboot:
$(last -20 2>/dev/null | grep -v reboot | head -20)
# 失败登录记录
lastb -20 2>/dev/null | head -20 || echo "lastb not available or no failed logins"
```
EOF
# ========== 安全巡检 ==========
log_info "采集安全信息..."
cat >> "$OUTPUT_FILE" << EOF
## 8. 安全状态
### 8.1 特权用户(UID=0)
```bash
# 检查 UID 为 0 的非 root 用户
awk -F: '$3 == 0 {print $1}' /etc/passwd:
$(awk -F: '$3 == 0 {print $1}' /etc/passwd 2>/dev/null)
```
### 8.2 非活跃账户(90 天未登录)
```bash
# 检查非活跃账户
lastlog -b 90 2>/dev/null | grep "Never|pts|tty" | head -10 || echo "lastlog -b not available"
```
### 8.3 SElinux / AppArmor 状态
```bash
# CentOS/RHEL SElinux
getenforce 2>/dev/null || echo "SELinux not available"
sestatus 2>/dev/null | head -5 || echo "sestatus not available"
# Ubuntu AppArmor
aa-status 2>/dev/null || echo "AppArmor not available"
```
### 8.4 防火墙状态
```bash
# firewalld (CentOS/RHEL)
firewall-cmd --state 2>/dev/null || echo "firewalld not running"
# ufw (Ubuntu)
ufw status 2>/dev/null || echo "ufw not available"
# iptables rules summary
iptables -L -n --line-numbers 2>/dev/null | head -30 || echo "iptables not available"
```
EOF
# ========== 应用层巡检 ==========
log_info "采集应用状态..."
cat >> "$OUTPUT_FILE" << EOF
## 9. 应用层巡检
### 9.1 Nginx 状态
```bash
# Nginx 版本
nginx -v 2>&1
# Nginx 进程状态
ps aux | grep nginx | grep -v grep:
# Nginx 连接状态
ss -tnp | grep nginx || echo "no nginx connections"
```
EOF
# MySQL 状态
cat >> "$OUTPUT_FILE" << 'MYSQL_EOF'
### 9.2 MySQL 状态
```bash
# MySQL 版本
mysql --version 2>/dev/null || echo "mysql client not available"
# MySQL 进程
ps aux | grep mysql | grep -v grep:
# MySQL 连接数
mysql -e "SHOW STATUS LIKE 'Threads_connected';" 2>/dev/null
mysql -e "SHOW STATUS LIKE 'Max_used_connections';" 2>/dev/null
mysql -e "SHOW PROCESSLIST;" 2>/dev/null | head -20
MYSQL_EOF
Redis 状态
cat >> "$OUTPUT_FILE" << 'REDIS_EOF'
9.3 Redis 状态
# Redis 进程 ps aux | grep redis | grep -v grep: # Redis 连接状态 redis-cli ping 2>/dev/null || echo "redis-cli not available or redis not running" redis-cli info replication 2>/dev/null | grep -E "role|connected_slaves" || echo "replication info not available" redis-cli info memory 2>/dev/null | grep -E "used_memory_human|maxmemory_human" || echo "memory info not available"
REDIS_EOF
Docker 状态
cat >> "$OUTPUT_FILE" << 'DOCKER_EOF'
9.4 Docker 状态
# Docker 版本
docker --version 2>/dev/null || echo "docker not available"
# Docker 服务状态
systemctl is-active docker 2>/dev/null || echo "docker service not running"
# 运行中的容器
docker ps 2>/dev/null || echo "docker not available"
# 容器资源使用
docker stats --no-stream 2>/dev/null | head -10 || echo "no containers or docker not available"
# 镜像数量和占用空间
docker images --format "table {{.Repository}} {{.Tag}} {{.Size}}" 2>/dev/null | head -20 || echo "no docker images"
DOCKER_EOF
========== 综合结论 ==========
log_info "生成巡检结论..."
动态判断告警项
ALERTS=""
检查磁盘告警
DISK_WARNINGS=$(df -h | awk 'NR>1 { usage=$5; sub(/%/, "", usage); if (usage > 80) { printf "- 严重: %s 使用率 %s%% ", $6, usage } else if (usage > 70) { printf "- 警告: %s 使用率 %s%% ", DISK_WARNINGS" ] && ALERTS="{DISK_WARNINGS} "
检查内存告警
MEM_PCT=MEM_PCT" -gt 90 ]; then ALERTS="${ALERTS} ### 内存告警 - 严重: 内存使用率 MEM_PCT" -gt 80 ]; then ALERTS="${ALERTS} ### 内存告警 - 警告: 内存使用率 ${MEM_PCT}% " fi
检查负载告警
LOAD_15=$(cat /proc/loadavg | awk 'print (nproc 2>/dev/null| echo 1) LOAD_INT=${LOAD_15%.*2>/dev/null || LOAD_INT=0 if [ "CORES" ]; then ALERTS="${ALERTS} ### 负载告警 - 警告: 15分钟负载 {CORES} " fi
检查可疑登录
FAILED_LOGIN=FAILED_LOGIN" -gt 10 ]; then ALERTS="${ALERTS} ### 安全告警 - 警告: 近 20 条失败登录尝试,请检查是否有暴力破解 " fi
如果没有告警
[ -z "$ALERTS" ] && ALERTS=" 无严重告警。 "
cat >> "$OUTPUT_FILE" << EOF
10. 巡检结论
${ALERTS}
10.1 巡检摘要
| 检查项 | 状态 | 说明 |
|---|---|---|
| CPU | LOAD_INT" -ge "$CORES" ] && echo " 异常" | |
| 内存 | MEM_PCT" -gt 80 ] && echo " 异常" | |
| 磁盘 | $(df -h | awk 'NR>1 {usage=$5; sub(/%/, "", usage); if (usage>80) status=" 异常"} END {print status ? status : " 正常"}') |
| 网络 | TIME_WAIT_COUNT" -gt 10000 ] && echo " 异常" | |
| 服务 | $(systemctl is-active nginx mysqld redis firewalld 2>/dev/null | grep -q "failed" && echo " 异常" |
| 日志 | $(journalctl --since "24 hours ago" --priority=err --no-pager 2>/dev/null | tail -1 |
10.2 下次巡检建议
EOF
添加动态建议
echo "" >> "OUTPUT_FILE" fi if [ "OUTPUT_FILE" fi if systemctl is-active firewalld 2>/dev/null | grep -q inactive; then echo "- 防火墙处于关闭状态,建议评估是否需要开启" >> "$OUTPUT_FILE" fi
cat >> "$OUTPUT_FILE" << EOF
本报告由 inspect.sh 脚本自动生成,生成时间:${TIMESTAMP}EOF
log_info "巡检报告已生成: ${OUTPUT_FILE}"
输出文件大小
ls -lh "$OUTPUT_FILE"
如果有告警,打印到终端
if [ -n "{ALERTS}" != " 无严重告警。 " ]; then log_warn "发现以下告警项:" echo "${ALERTS}" fi
设置执行权限: ```bash chmod +x ~/server-inspection/inspect.sh
远程批量采集脚本:collect_remote.sh
在控制节点上运行这个脚本,可以批量从多台服务器采集巡检报告:
#!/bin/bash
# ~/server-inspection/collect_remote.sh
# 批量从多台服务器采集巡检报告
# 用法: ./collect_remote.sh servers.txt
set -euo pipefail
if [ -z "${1:-}" ]; then
echo "用法: $0 <服务器列表文件>"
echo "服务器列表文件格式(每行一个,格式: 主机名或IP 用户 SSH端口(可选)):"
echo " 192.168.1.11 root"
echo " nginx-02 root 2222"
echo " db-01"
exit 1
fi
SERVER_LIST="$1"
OUTPUT_DIR="./remote-reports-$(date +%Y%m%d)"
SSH_USER="${SSH_USER:-root}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -i $SSH_KEY"
# 巡检脚本内容(通过 stdin 传给远程服务器)
INSPECT_SCRIPT=$(cat << 'SCRIPT_EOF'
set -euo pipefail
OUTPUT_DIR="${1:-.}"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
REPORT_DATE=$(date "+%Y-%m-%d")
HOSTNAME=$(hostname)
PRIMARY_IP=$(hostname -I | awk '{print $1}')
OS_VERSION=$(cat /etc/redhat-release 2>/dev/null || cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)
mkdir -p "$OUTPUT_DIR"
OUTPUT_FILE="${OUTPUT_DIR}/${REPORT_DATE}-${HOSTNAME}.md"
exec > "$OUTPUT_FILE"
# ... 这里是完整巡检逻辑(与 inspect.sh 相同的核心逻辑)
# 为了简洁,这里用 heredoc 引用 inspect.sh 的内容
SCRIPT_EOF
)
mkdir -p "$OUTPUT_DIR"
echo "开始批量采集,目标目录: $OUTPUT_DIR"
echo "=========================================="
TOTAL=$(grep -v "^#" "$SERVER_LIST" | grep -v "^$" | wc -l)
CURRENT=0
while IFS= read -r line; do
[ -z "$line" ] && continue
[[ "$line" =~ ^# ]] && continue
CURRENT=$((CURRENT + 1))
# 支持格式: IP PORT USER 或 IP USER 或 IP
set -- $line
SERVER=$1
USER="${2:-$SSH_USER}"
PORT="${3:-22}"
echo "[$CURRENT/$TOTAL] 正在采集: $SERVER (user=$USER, port=$PORT)"
# 把 inspect.sh 脚本内容通过 ssh 传给远程服务器执行
# 这里用 ssh 执行本地脚本: ssh user@host "bash -s" < script.sh
ssh $SSH_OPTS -p "$PORT" "${USER}@${SERVER}"
"bash -s" -- "${OUTPUT_DIR}" < <(cat ~/server-inspection/inspect.sh)
>> "${OUTPUT_DIR}/collect.log" 2>&1 &
echo " -> 已后台启动,将在后台运行"
done < "$SERVER_LIST"
echo ""
echo "=========================================="
echo "所有采集任务已后台启动"
echo "等待任务完成..."
# 等待所有后台任务完成(最多等 10 分钟)
wait_timeout=600
elapsed=0
while pgrep -f "ssh.*inspect" > /dev/null 2>&1; do
sleep 5
elapsed=$((elapsed + 5))
if [ $elapsed -ge $wait_timeout ]; then
echo "等待超时,停止等待"
break
fi
echo " 已等待 ${elapsed}s,仍有任务在运行..."
done
echo ""
echo "=========================================="
echo "采集完成!报告保存在: $OUTPUT_DIR"
echo ""
echo "生成的报告文件:"
ls -lh "$OUTPUT_DIR"/*.md 2>/dev/null || echo " 没有找到报告文件"
chmod +x ~/server-inspection/collect_remote.sh
服务器列表文件示例 servers.txt:
# 服务器巡检列表 # 格式: IP 或 主机名 用户 SSH端口(可选) # Web 服务器组 192.168.1.11 root 22 192.168.1.12 root 22 # 数据库服务器组 192.168.1.21 root 22 192.168.1.22 root 22 # Nginx 服务器 192.168.1.10 ops 2222
定时任务:自动巡检
把巡检脚本加入 crontab,实现自动定时执行:
# 编辑 crontab crontab -e # 每天早上 8 点执行巡检 0 8 * * * /bin/bash /root/server-inspection/inspect.sh /root/server-inspection/reports >> /root/server-inspection/reports/cron.log 2>&1 # 每周一早上 8 点执行巡检(详细版) 0 8 * * 1 /bin/bash /root/server-inspection/inspect.sh /root/server-inspection/reports-weekly >> /root/server-inspection/reports-weekly/cron.log 2>&1
报告示例输出
执行后的 Markdown 报告大致如下(截取关键部分):
# 服务器巡检报告 **生成时间**: 2024-01-15 0800 **主机名**: nginx-01 **主 IP 地址**: 192.168.1.11 **操作系统**: CentOS Linux 7.9.2009 **内核版本**: 3.10.0-1160.el7.x86_64 **运行时长**: up 45 days **最近重启**: 2023-12-01 06:30 **负载平均值**: 0.15 0.12 0.10 --- ## 2. CPU 信息 **CPU 型号**: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz **物理核心数**: 2 **逻辑核心数**: 4 **当前使用率**: 用户态: 2.3%, 系统态: 0.8%, 空闲: 96.9% **15分钟负载**: 0.12 --- ## 4. 磁盘信息 | 文件系统 | 大小 | 已用 | 可用 | 使用率 | 挂载点 | |---------|------|------|------|--------|--------| | /dev/sda1 | 50G | 35G | 15G | 70% | / | | /dev/sda2 | 200G | 160G | 40G | 80% | /data | > **巡检关注点**:/data 使用率 80%,建议清理历史数据或计划扩容 --- ## 10. 巡检结论 ### 磁盘告警 - **警告**: /data 使用率 80% ### 巡检摘要 | 检查项 | 状态 | 说明 | |--------|------|------| | CPU | 正常 | 负载: 0.12, 核心数: 4 | | 内存 | 正常 | 使用率: 62% | | 磁盘 | 异常 | /data 使用率 80% |
常见问题排查
问题一:脚本执行报错 "command not found"
原因:部分系统默认没有装某个命令
解决方案:在脚本里加判断,缺失时给出友好提示
for cmd in df free ss awk sed; do if ! command -v $cmd &> /dev/null; then echo "WARN: $cmd not found, some features may not work" fi done
问题二:中文显示乱码
原因:服务器 locale 设置不支持中文
解决方案:设置环境变量
export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8
或者在脚本输出时只使用 ASCII 字符。
问题三:远程执行时 heredoc 格式丢失
原因:SSH 传输时某些特殊字符被转义
解决方案:使用 base64 编码传输脚本
# 控制节点上 SCRIPT_B64=$(base64 -w0 ~/server-inspection/inspect.sh) ssh $SSH_OPTS user@host "echo '$SCRIPT_B64' | base64 -d > /tmp/inspect.sh && bash /tmp/inspect.sh"
风险提醒
脚本执行时机:巡检脚本本身对服务器压力极小,但 iostat、ps aux --sort 等命令会瞬时增加少量系统开销。建议不要在业务高峰期(比如大促前夕)执行巡检。
报告存放安全:巡检报告包含系统详细信息(IP、端口、服务版本等),本质上是一个"完整的系统画像",敏感环境切勿将报告放在公开可访问的目录。
日志泄露:报告中包含近期登录记录、错误日志等,可能包含业务敏感信息,需要注意报告的存储和分发范围。
SSH 免密安全:如果使用 SSH 公钥执行远程采集,确保控制节点的私钥权限正确(600),避免被滥用。
总结
本文实现了一套轻量级的服务器巡检自动化方案,核心就两个脚本:
inspect.sh:在每台服务器上执行,采集本机数据,输出 Markdown 报告
collect_remote.sh:在控制节点执行,批量从多台服务器采集报告
用定时任务驱动,就是一套无人值守的巡检系统。整个方案不依赖 Ansible 等重型工具,纯 Shell + 系统自带工具,实现简单、维护成本低。
这套方案的扩展方向:
接入告警:把脚本里的 ALERTS 输出对接钉钉/企业微信/邮件告警
历史对比:用 Git 管理报告目录,diff 两次报告看变化
数据入库:把关键指标(磁盘使用率、内存使用率)提取出来存入时序数据库
可视化:用 Grafana 展示巡检数据趋势
合规报告:针对等保等合规要求,添加特定的检查项
从今天起,巡检不再是"登录服务器敲几个命令截图丢群里",而是一条命令搞定、一份结构化报告存档。
全部0条评论
快来发表一下你的评论吧 !