Shell脚本如何实现服务器自动巡检?CPU、内存、磁盘与端口检查教程

服务器巡检是运维里最枯燥、又最不能省的一件事。业务没出问题时,没人会想起去看 CPU 和磁盘;等磁盘写满、内存被吃光、端口莫名不通时,值班的人已经被电话叫醒了。手工敲 top、df -h、free -m 每天重复一遍,既费时间又容易漏项,机器一多基本顾不过来。

这篇文章教你用一段纯 Shell 脚本把 CPU、内存、磁盘、端口这四类最常见的检查项自动化,输出一份人能一眼看懂的巡检报告,并直接对接定时任务和告警。脚本不依赖任何第三方工具,只用 Linux 自带命令,CentOS、Rocky、Ubuntu、Debian 都能跑。

文中的阈值(比如磁盘 85%、内存 90%)都是运维圈里常用的经验值,不是硬标准,你可以按自己业务的承压情况调整。所有命令都会标注发行版差异,涉及清理和重启之类的动作会单独给出风险提示。

一、巡检脚本要检查什么

先想清楚"巡检"到底要产出什么。它不是把一堆原始数据打印出来,而是回答三个问题:哪里不正常、严重到什么程度、下一步该看什么。基于这个目标,我们把检查项收敛成四类核心指标,外加一个整体汇总。

检查项 取数命令 关注点 常见阈值(经验值)
CPU 负载 uptime、/proc/loadavg 1/5/15 分钟负载与核数比值 15 分钟负载 > 核数×1.5
内存 free -m 可用内存、swap 使用 可用内存 < 10%、swap 大量占用
磁盘 df -h、df -i 使用率、inode 使用率 使用率 > 85%,inode > 85%
端口/服务 ss -lntup、systemctl 关键端口是否监听 期望端口不在监听列表
系统基础 uptime、who 开机时长、登录用户 异常重启、异常登录

这里要强调一点:负载(load average)不能单独看。一台 4 核机器负载 4.0 就是满载,而 32 核机器负载 4.0 几乎在摸鱼。所以脚本里必须先取 CPU 核数,再用"负载 ÷ 核数"来判断,否则告警会天天误报。

另外,内存检查也有个坑:Linux 会拿空闲内存做缓存(buff/cache),所以 free 里的 free 一列通常很小,看起来"内存快没了",其实大部分是可回收的缓存。判断内存是否真的紧张,应该看 available 一列,而不是 free 一列。

二、取数命令详解

写脚本前先把每个取数命令摸清楚,后面拼装才不会乱。

CPU 核数:从 /proc/cpuinfo 数 processor 行,或者用 nproc。后者更简单,几乎所有发行版都有。

负载:cat /proc/loadavg 输出形如 0.15 0.10 0.05 1/234 5678,前三个数就是 1、5、15 分钟负载,比解析 uptime 的输出更干净。

内存:free -m 以 MB 为单位输出,available 列是"估算的可用内存",最贴近真实可用量。

磁盘:df -P -h 用 POSIX 格式输出,避免长设备名换行导致解析错位;df -Pi 看 inode。这两个都要检查——有时候空间还有,inode 却用光了,新文件一个也创建不了,这是很隐蔽的故障。

端口:ss -lntup 是现代发行版推荐的命令,netstat 在很多系统里已经默认不装。用 ss -lnt 只列 TCP 监听端口即可,检查特定端口是否在列。

服务状态:systemctl is-active nginx 会返回 active 或 inactive,很适合脚本判断,比解析 systemctl status 的输出可靠得多。

三、完整巡检脚本

下面是一份可直接运行的脚本,把它存成 /usr/local/bin/server_check.sh,赋执行权限即可。脚本会输出一份带 ✅/⚠️ 标记的报告,末尾给出整体结论。

#!/usr/bin/env bash
# 服务器自动巡检脚本
# 适用:CentOS/Rocky/Debian/Ubuntu 通用
# 用法:bash server_check.sh
set -uo pipefail

# ---- 可调阈值(经验值,按业务调整)----
LOAD_FACTOR=1.5      # 15 分钟负载 / 核数 超过此值告警
MEM_WARN=10          # 可用内存百分比低于此值告警
DISK_WARN=85         # 磁盘使用率高于此值告警
INODE_WARN=85        # inode 使用率高于此值告警
CHECK_PORTS="22 80 443"   # 需要确认在监听的端口

WARN=0
report() { printf '%s %s\n' "$1" "$2"; }

echo "================ 服务器巡检报告 ================"
echo "主机: $(hostname)  时间: $(date '+%F %T')"
echo "系统: $(grep -E '^(PRETTY_NAME)=' /etc/os-release | cut -d= -f2 | tr -d '\"')"
echo "------------------------------------------------"

# 1) CPU 负载
CORES=$(nproc)
LOAD15=$(awk '{print $3}' /proc/loadavg)
RATIO=$(awk -v l="$LOAD15" -v c="$CORES" 'BEGIN{printf "%.2f", l/c}')
LOAD_STATUS="✅ 正常"
awk -v r="$RATIO" -v f="$LOAD_FACTOR" 'BEGIN{exit !(r>f)}' && { LOAD_STATUS="⚠️ 偏高"; WARN=1; }
report "CPU : 核数=$CORES  15分钟负载=$LOAD15  负载比=$RATIO  $LOAD_STATUS"

# 2) 内存
MEM_TOTAL=$(free -m | awk '/^Mem:/{print $2}')
MEM_AVAIL=$(free -m | awk '/^Mem:/{print $7}')
MEM_PCT=$(awk -v a="$MEM_AVAIL" -v t="$MEM_TOTAL" 'BEGIN{printf "%.1f", a*100/t}')
SWAP_USED=$(free -m | awk '/^Swap:/{print $3}')
MEM_STATUS="✅ 正常"
awk -v p="$MEM_PCT" -v w="$MEM_WARN" 'BEGIN{exit !(p

脚本里用了 set -uo pipefail 而不是 set -e——巡检脚本要避免某个检查项失败就整体中断,所以把 -e 去掉,只保留未定义变量报错和管道错误检测。这一点和普通业务脚本的习惯相反,值得注意。

四、接入定时任务与告警

巡检要"自动",就得挂到 cron 或 systemd timer 上定期跑。最常见的是每 5 分钟或每小时一次。

# 方式一:crontab(每个用户独立)
crontab -e
# 每 5 分钟巡检一次,报告追加到日志
*/5 * * * * /usr/local/bin/server_check.sh >> /var/log/server_check.log 2>&1
# 方式二:系统级目录 /etc/cron.d/(推荐给运维脚本)
# 文件里需带用户名字段
sudo tee /etc/cron.d/server_check >/dev/null <<'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/5 * * * * root /usr/local/bin/server_check.sh >> /var/log/server_check.log 2>&1
EOF
sudo chmod 644 /etc/cron.d/server_check
# 方式三:systemd timer(适合需要日志集成、依赖管理的场景,RHEL8+/Ubuntu18+ 均支持)
# /etc/systemd/system/server_check.service
[Unit]
Description=Server Inspection
[Service]
Type=oneshot
ExecStart=/usr/local/bin/server_check.sh
StandardOutput=append:/var/log/server_check.log
# /etc/systemd/system/server_check.timer
[Unit]
Description=Run server inspection every 5 min
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target

然后 sudo systemctl daemon-reload && sudo systemctl enable --now server_check.timer 即可。三种方式没有绝对优劣:临时用 crontab 最省事,多台机器统一管理推荐 /etc/cron.d/,需要精确的日志和开机后补跑就用 systemd timer。

五、把告警发出去

光写日志没人看,得把"有告警"这件事推给人。最轻量的方式是脚本内判断有告警时发邮件。下面这段可追加到脚本结尾。

# 依赖:mailx(CentOS/Rocky 用 mailx,Debian/Ubuntu 用 mailutils)
# RHEL/CentOS/Rocky:
#   sudo dnf install -y mailx
# Debian/Ubuntu:
#   sudo apt install -y mailutils
# 追加到脚本末尾(在结论输出之后)
if [ "$WARN" -ne 0 ]; then
  SUBJECT="[巡检告警] $(hostname) 存在异常 $(date '+%F %T')"
  mail -s "$SUBJECT" ops@example.com < /var/log/server_check.log
fi

邮件在不少云主机上默认发不出去(25 端口被限制),常见做法是配一台内网 SMTP 中继,或者改用企业微信/钉钉/飞书的机器人 Webhook。Webhook 方式只需一条 curl:

# 以某 IM 机器人为例(把 URL 换成你自己的机器人地址)
curl -s -X POST "$BOT_WEBHOOK" \
  -H 'Content-Type: application/json' \
  -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"$(hostname) 巡检异常,请查看 /var/log/server_check.log\"}}"

六、风险提示与回滚

巡检脚本本身只读不写,风险很低,但有几个地方要注意。

写日志的磁盘占用:如果每 5 分钟追加一次,日志会持续增长。建议配合 logrotate,或者只在有告警时写日志。一个中等规模机器上,单次报告约 1-2 KB,每天 288 次大约几百 KB,一年累计可能到几十 MB——量不大但不能不管。

阈值调太激进的后果:告警阈值设得过紧,会每天收到大量"狼来了",最后没人看告警,真正出问题时反而被淹没。这是比技术问题更常见的运维事故。建议先跑一周只记录不告警,观察数据分布,再逐步收紧。

邮件/Webhook 泄露:机器人 Webhook URL 相当于密码,不要写死在多人可见的脚本里,放到只有 root 可读的配置文件(如 /etc/server_check.conf,权限 600)中,脚本用 source 引入。

# 安全的做法:配置与脚本分离
sudo install -m 600 /dev/null /etc/server_check.conf
# 内容示例:BOT_WEBHOOK="https://..."
# 脚本中: [ -r /etc/server_check.conf ] && . /etc/server_check.conf

回滚:如果脚本出问题需要停用,删掉 cron 条目(crontab -e 或 sudo rm /etc/cron.d/server_check)或 sudo systemctl disable --now server_check.timer 即可,不影响任何系统状态。脚本本身可保留在 /usr/local/bin/ 下随时手工运行。

七、从巡检结果到排障思路

巡检报告本身不是终点,"告警之后怎么办"才是运维真正的价值所在。把四类指标串起来看,能快速缩小排查范围。

如果是磁盘使用率高,先用 du -sh /var/* 2>/dev/null | sort -rh | head 找出大目录,再逐层下钻。最常见的元凶是日志(/var/log)、应用数据(上传文件、缓存)、以及被删除但进程仍持有的文件——后者用 lsof +L1 能查到,这类文件 du 看不见但空间确实被占着,重启对应服务才能释放。如果是 inode 高,用 find /var -xdev -type f | wc -l 定位小文件集中的目录。

如果是内存告警,用 ps aux --sort=-%mem | head 看谁在吃内存。注意区分"常驻占用"和"缓存临时占用";如果是某个服务内存缓慢上涨(内存泄漏的典型特征),重启只是缓解,根治要反馈给开发排查。

如果是端口不通,按"监听 → 防火墙 → 网络"三步走:先 ss -lnt 确认进程真的在监听,再 iptables -L -n 或 firewall-cmd --list-all 确认防火墙放行,最后从外部 telnet 或 nc -zv 测试连通性。这三步能覆盖绝大多数"端口不通"的问题,顺序不要颠倒,否则容易在防火墙里白折腾半天。

如果是负载高而 CPU 空闲,重点看 IO 和 D 状态进程:iostat -x 1 看 %util 和 await,ps -eo pid,stat,cmd | awk '$2 ~ /D/' 找不可中断睡眠的进程。这类问题往往源自慢盘、NFS 挂载点失联或数据库慢查询,处理思路和"CPU 打满"完全不同。

常见问题

Q1:为什么用 /proc/loadavg 而不是解析 uptime 输出?
uptime 的输出格式随版本和 locale 略有变化,解析容易出岔子。/proc/loadavg 格式固定,前三列就是 1/5/15 分钟负载,机器友好得多。同理,判断内存用 free 的数值列,判断服务用 systemctl is-active,都比给人看的输出更稳。

Q2:磁盘空间还够,为什么业务报"磁盘满"?
大概率是 inode 用光了。大量小文件(比如缓存目录、邮件队列、会话文件)会快速消耗 inode,df -h 看不出来,必须用 df -i 检查。排查哪个目录小文件多可以用 find /path -xdev -type f | wc -l,或用 du 逐层统计。

Q3:负载很高但 CPU 使用率很低,是什么原因?
负载统计的是"处于运行队列和不可中断睡眠(D 状态)的进程"。如果磁盘 IO 慢、NFS 卡住,进程会长时间处于 D 状态,负载飙升但 CPU 空闲。这时应该用 iostat -x 1、iotop 去看磁盘,而不是盯着 CPU。

Q4:脚本在 CentOS 上跑得好好的,到 Ubuntu 报错?
常见原因是命令路径或默认工具不同,比如 ss 在某些精简镜像里没有、mail 命令来自不同包、awk 版本差异等。脚本开头统一设置 PATH,并尽量使用 POSIX 兼容写法,能减少这类问题。

Q5:需要巡检的机器很多,怎么统一管理?
少则用 Ansible 批量推送脚本并采集结果,多则接入专业监控(如 Prometheus + Node Exporter,或主流云厂商自带的云监控)。Shell 脚本适合做"补充性的、自定义的"检查项,通用指标交给专业监控更省心。

总结

  • 巡检脚本的核心是回答"哪里不正常、多严重、下一步看什么",不是堆砌原始数据。
  • 负载必须结合核数判断,内存要认 available 而不是 free,磁盘要同时看空间和 inode。
  • 用 /proc/loadavg、free、df -P、ss -lnt、systemctl is-active 这些机器友好输出取数,脚本更稳。
  • 定时任务推荐放在 /etc/cron.d/ 或改用 systemd timer,告警走邮件或 IM 机器人 Webhook。
  • 阈值先宽后紧、先观察再告警,避免告警泛滥;Webhook 等敏感信息要单独存放并限制权限。