服务器健康巡检:从SMART趋势看硬件老化

在机房和云主机之间搬过站的人都知道,同一台物理机活了三年,硬盘坏、内存条接触不良、风扇停转、网卡降速都是早晚的事。问题是这些故障大多不是「突然坏掉」,而是「参数在慢慢变差」:SMART 里重映射扇区从 0 涨到 8,内存单条错误每小时冒出几条,网卡从 1000Mbps 协商成 100Mbps,机箱里一颗风扇转速掉到 300 转。

这些指标单看一次毫无意义,只有长期的趋势才有意义。所以服务器健康巡检的正解不是「每季度看一眼」,而是把关键指标每天采一次、存下来、看趋势。这篇讲的就是怎么用纯 shell 搭一套每天自动跑的健康巡检,不装 Zabbix、不装 Prometheus,一台机器几十行脚本就够。

一、先明确监什么:四类指标

服务器健康可以分成四层,每层关注的东西不一样:

存储层:SMART 的五个关键属性、RAID 状态、文件系统只读挂载、磁盘利用率与 inode 使用率、写入错误计数。

内存层:ECC 可纠正错误计数、可用内存趋势、swap 使用量、OOM 事件。

硬件层:风扇转速、温度、电压、电源状态、机箱开盖、网卡链路速率与错误包。

系统层:内核报错(MCE、IO error)、服务重启次数、僵尸进程、加载的 kernel module 异常。

个人站长的机器大多是 VPS 或独服,VPS 上硬件层基本看不到(被虚拟化屏蔽),能看的主要是存储层和系统层;独服上四层都能看,尤其是 SMART 和 IPMI。

二、SMART:只看这五个属性就够

smartctl -a 输出几十行,绝大多数属性从出厂到报废都是 100,看了没意义。真正需要盯的是这五个:

smartctl -H -A /dev/sda | grep -E 'SMART overall|Reallocated_Sector|Pending_Sector|Offline_Uncorrect|UDMA_CRC|Power_On_Hours'

逐条解释它们的含义和判据:

Reallocated_Sector_Ct(重映射扇区数)。硬盘自己发现坏扇区后,用备用扇区替换的数量。这个值 只有 0 和非 0 两种状态,一旦不为 0 就意味着盘上有物理坏道了。0 涨到 1 可以观察,涨到 10 以上就该准备换盘,一个月内翻倍则立刻换。

Current_Pending_Sector(待决扇区)。读取出错但还没决定要不要重映射的扇区。这个值非 0 比 Reallocated 更危险,因为它意味着「数据现在读不出来」。出现就要立刻做完整备份,别等。

Offline_Uncorrectable。离线自检时发现无法纠正的扇区。非 0 = 数据已经丢了,必须从备份恢复。

UDMA_CRC_Error_Count。这不是盘的问题,是线的问题。SATA 线接触不良、背板松动、走线过长都会让这个值上涨。很多站长看到 SMART 有错就换盘,实际换根线就解决了。判据:这个值在涨,而前面三个都是 0,那就是线缆问题。

Power_On_Hours。通电小时数。机械盘超过 4 万小时(约四年半)进入高故障期,SSD 则看 Total_LBAs_Written 判断写入量是否接近 TBW。

SSD 还要额外看两个:Media_Wearout_Indicator(或 Percentage_Used)低于 10 就该换,Program_Fail_Count 非 0 说明写入已经出问题。

注意一条:虚拟机里的 /dev/sda 通常不支持 SMART,smartctl 会报「Unable to detect device type」。这不是故障,是虚拟化屏蔽,脚本里要能识别并跳过,否则每天都会误报。

三、日常巡检脚本:一次采集,存成一行

把上面所有指标采集下来,输出成「时间 + 键值对」的一行,追加到一个文件里。这样既能人眼看,也方便后面用 awk 算趋势。

#!/bin/bash
# /usr/local/bin/health.sh  每天由 cron 跑一次
OUT=/var/log/health/$(date +%Y%m).log
mkdir -p /var/log/health
printf '%s ' "$(date '+%F %T')" >> "$OUT"

# 磁盘 SMART
for d in /dev/sd[a-z] /dev/nvme[0-9]n[0-9]; do
    [ -b "$d" ] || continue
    info=$(smartctl -H -A "$d" 2>/dev/null) || continue
    echo "$info" | grep -q 'PASSED\|OK' || printf 'smart_fail_%s=1 ' "$(basename $d)" >> "$OUT"
    for k in Reallocated_Sector_Ct Current_Pending_Sector UDMA_CRC_Error_Count; do
        v=$(echo "$info" | awk -v k="$k" '$2==k {print $10}')
        [ -n "$v" ] && printf '%s_%s=%s ' "$(basename $d)" "$k" "$v" >> "$OUT"
    done
done

# 磁盘与 inode 使用率
printf 'root_use=%s ' "$(df -P / | awk 'NR==2{gsub("%","",$5);print $5}')" >> "$OUT"
printf 'root_ino=%s ' "$(df -Pi / | awk 'NR==2{gsub("%","",$5);print $5}')" >> "$OUT"

# 内存与 swap
printf 'mem_avail=%s ' "$(free -m | awk '/Mem:/{print $7}')" >> "$OUT"
printf 'swap_used=%s ' "$(free -m | awk '/Swap:/{print $3}')" >> "$OUT"

# 负载、僵尸进程、只读挂载
printf 'load1=%s ' "$(cut -d' ' -f1 /proc/loadavg)" >> "$OUT"
printf 'zombie=%s ' "$(ps -eo stat | grep -c '^Z')" >> "$OUT"
printf 'ro_mount=%s ' "$(awk '$4 ~ /^ro/ && $2 !~ /proc|sys/' /proc/mounts | wc -l)" >> "$OUT"

# 硬件层(独服才有)
if command -v ipmitool >/dev/null 2>&1; then
    printf 'temp=%s ' "$(ipmitool sdr type temperature 2>/dev/null | head -1 | awk -F'|' '{print $5}' | tr -dc '0-9')" >> "$OUT"
fi

# 内核硬件错误
printf 'mce=%s ' "$(dmesg 2>/dev/null | grep -ci 'machine check\|Hardware Error')" >> "$OUT"
printf 'ioerr=%s ' "$(dmesg 2>/dev/null | grep -ci 'I/O error\|medium error')" >> "$OUT"

echo >> "$OUT"

这个脚本有几个实现细节值得说明。第一,所有采集都用 2>/dev/null 吞掉错误输出,因为 smartctl 在虚拟机上会输出一堆警告到 stderr,混进日志会让后面解析变得麻烦。第二,所有字段都用 键=值 的形式,不用固定列,这样以后加指标不用担心列错位——固定列是最容易被「加了一个字段」搞崩的格式。第三,一行一条,一个月一个文件,一天一行,一年也只有 365 行,文件小到可以直接 cat 看。

写进 cron:

17 4 * * * /usr/local/bin/health.sh >/dev/null 2>&1

用 4:17 这种「非整点」的时间,是为了避开每天凌晨的备份、日志轮转、证书续期任务。一堆任务都卡在 0 点跑,机器在那几分钟的负载会对指标产生干扰。

四、看趋势而不是看阈值

这是整套方案的核心。阈值告警的问题是:等你触到阈值,故障往往已经发生了。而趋势能在「还没出问题」的时候提醒你。

比如查看最近三个月重映射扇区的变化:

grep -ho 'sda_Reallocated_Sector_Ct=[0-9]*' /var/log/health/*.log | awk -F= '{print $2}' | uniq -c

uniq -c 会把连续相同的值合并计数,「8 出现在 40 天里,然后变成 16 出现在 12 天里」这种输出一眼就能看出涨了。这比画图更省事。

再看内存可用量的月度对比:

awk '{for(i=2;i<=NF;i++){split($i,a,"="); if(a[1]=="mem_avail") printf "%s %s\n", $1, a[2]}}' /var/log/health/*.log | tail -30

如果 30 天里可用内存在稳定下降(比如从 2800M 掉到 700M),说明有内存泄漏。这时候不要急着重启,先去查是谁在涨:ps -eo pid,rss,cmd --sort=-rss | head,然后在泄漏进程的层面解决。重启只是把问题推迟一天。

温度也是同样的道理。机械盘在 45 度以下都算正常,但如果从 38 度慢慢爬到 44 度,说明机箱散热在变差(风扇积灰、风扇转速下降),这时候清一次灰,比等到夏天高温报警再处理划算得多。

五、内核日志:硬件故障的真正现场

SMART 只覆盖硬盘,内存和总线的错误全在 dmesg 里。以下几条是「看到就要动手」的:

Machine check events logged / Hardware Error —— 一般是内存或 CPU 报的机器检查异常,配合 mcelog 或 rasdaemon 能看出具体是哪根内存条。可纠正错误(Corrected)数量少可以观察,一旦出现 Uncorrected,必须停机换件。

blk_update_request: I/O error —— 块设备读写错误。如果伴随 EXT4-fs error 或者 XFS: metadata I/O error,说明文件系统已经有损坏了,应该立刻做 dmesg 存档、卸载、fsck,同时不要在这个盘上继续写。

Link is Down / NIC Link is Up 100 Mbps —— 网卡链路从千兆掉到百兆,通常是网线水晶头氧化或者交换机端口故障。这个错误很隐蔽:站点不会挂,但带宽掉一个数量级,用户会感觉「最近怎么变慢了」。千兆口掉百兆时,ethtool 能直接看到当前速率。

Out of memory: Killed process —— OOM killer 出场。这条日志要单独统计,一天出现多次说明内存真的不够了,而不是「偶发」。

把这些做成每日扫描,命中就在报告顶部标红:

dmesg -T 2>/dev/null | grep -iE 'hardware error|i/o error|oom-kill|link is down' | tail -20

注意 dmesg -T 的人类可读时间是按「当前启动时间」推算的,跨重启后时间会漂,做长期趋势要用 journalctl -k --since 加时间范围。

六、RAID 与文件系统的一致性检查

如果机器上做了软 RAID(mdadm),/proc/mdstat 是每天必看的一行:

cat /proc/mdstat | grep -E '\[.*\]|recovery|degraded|resync'

输出里 [UU] 表示两块盘都正常,[U_] 表示有一块掉了。很多人是在「重启后进不去系统」时才发现 RAID 早就降级了,因为降级状态下系统还能跑,没有任何提示。

文件系统层面,除了前面的只读挂载检查,还要定期做一次「只读一致性扫描」。ext4 用 e2fsck -n 可以在不修改的情况下检查:

e2fsck -nf /dev/md0 >/var/log/health/fsck_$(date +%m).log 2>&1; echo "exit=$?"

退出码 0 表示干净,4 表示有需要修的东西。这个扫描对大分区很慢,建议每月跑一次而不是每天。XFS 没有只读模式下的等价命令,只能靠 xfs_repair -n,同样费力,季度一次即可。

七、报告与告警的分离

做巡检最容易走偏的一步是把所有异常都做成告警。结果是每天收到七八封邮件,全是「swap 用了 200M」这种无所谓的东西,三周之后你开始不加思索地删掉它们,然后真正的那一封也被删了。

正确的划分是:确定性异常走告警,趋势性变化走周报。

告警(立即发,手机推送)只留这几条:SMART 整体状态从 PASSED 变成 FAILED、Current_Pending_Sector 从 0 变非 0、RAID 降级、文件系统变只读、OOM 一天内出现 3 次以上、磁盘使用率超过 90%。这些都是「今晚不处理明天可能就出事」的。

周报(每周一次,邮件)放:磁盘使用率趋势、内存可用趋势、SMART 各项数值的变化、温度区间、网卡速率、僵尸进程数、内核错误计数。这些东西不紧急,但累积起来能告诉你「这台机器正在老化」。

一个简单的周报生成脚本,把最近七天的行汇总一下:

#!/bin/bash
LOG=/var/log/health/$(date +%Y%m).log
{
    echo "健康周报 $(date +%F)"
    echo "--- 磁盘使用率(最近7天)"
    grep -o 'root_use=[0-9]*' "$LOG" | tail -7
    echo "--- 可用内存 MB(最近7天)"
    grep -o 'mem_avail=[0-9]*' "$LOG" | tail -7
    echo "--- SMART 重映射(历史不同值)"
    grep -ho 'sd[a-z]_Reallocated_Sector_Ct=[0-9]*' /var/log/health/*.log | sort | uniq -c
    echo "--- 内核硬件错误"
    grep -o 'mce=[0-9]*\|ioerr=[0-9]*' "$LOG" | tail -7
} | mail -s "server health weekly" ops@example.com

八、把巡检变成一个习惯

脚本搭起来只是开始,真正决定这套东西有没有用的,是「有没有人看」。个人站点的现实是:没人会每天打开终端看日志。所以必须让数据主动找人——告警推到手机,周报发到邮箱,哪怕只是扫一眼。

我的做法是把这个周报和备份验证报告合并成一封邮件,每周一早上发。既然备份报告是必须看的(它能告诉你备份到底有没有成功),把它和健康报告绑在一起,就保证了健康数据一定会被看到。

另外建议在换硬件、加内存、扩磁盘这些「机器状态发生变化」的时刻,手动在日志里插一行注释。三个月后回看趋势时,那一行注释能省掉大量「为什么这个指标那天跳了」的困惑。echo "# 2026-10-28 换了 sda 数据线" >> /var/log/health/$(date +%Y%m).log,一行就够。

九、小结

服务器健康巡检的核心不是「装一个监控系统」,而是「把关键指标留下的痕迹保存下来」。SMART 里五个属性、dmesg 里四类错误、RAID 与文件系统的状态、磁盘和内存的趋势,这些加起来一天一行文本,一年的数据不到 100KB,却能提前几个月发现换盘需求。

脚本本身很简单,难的是判断哪些指标重要、哪些阈值该告警、哪些只看趋势。希望这篇能帮你把「凭感觉判断机器还行不行」换成「打开日志看一眼就知道」。

Last modification:October 28th, 2026 at 09:26 pm

Leave a Comment