运维知识
悠悠
2026年8月22日

5块硬盘同一周挂掉,逼我写了个故障预测脚本——SMART数据+开源模型实战

半年前机房里一周炸了5块盘,RAID重建还没跑完第二块又掉了,那个晚上我盯着iDRAC面板手都在抖。事后复盘发现这5块盘的SMART数据在故障前2-4周就已经有明显异常了——只是没人看。

从那之后我搞了一套磁盘预测感知的东西,用smartmontools采集SMART数据,跑一个轻量的XGBoost模型做故障预测,提前换盘。跑了半年,成功预警了12块盘,误报3次,漏报0次。这篇把整个落地过程写出来。

先搞清楚SMART数据到底是什么

很多人知道SMART但没认真看过里面的字段。简单说,SMART(Self-Monitoring, Analysis and Reporting Technology)是硬盘固件内置的一套自检机制,它会持续记录磁头读写错误率、坏扇区数量、通电时间、温度等几十个指标。

在Linux上用smartmontools就能读:

# 安装
yum install -y smartmontools   # CentOS/RHEL
apt install -y smartmontools   # Debian/Ubuntu

# 查看磁盘SMART是否开启
smartctl -i /dev/sda

# 开启SMART(部分盘默认关闭)
smartctl -s on /dev/sda

# 查看全部SMART属性
smartctl -A /dev/sda

输出大概长这样:

ID# ATTRIBUTE_NAME          FLAG     VALUE WORST THRESH TYPE      UPDATED  RAW_VALUE
  1 Raw_Read_Error_Rate     0x002f   200   200   051    Pre-fail  Always   0
  5 Reallocated_Sector_Ct   0x0033   200   200   140    Pre-fail  Always   0
  7 Seek_Error_Rate         0x002e   200   200   000    Old_age   Always   0
  9 Power_On_Hours          0x0032   044   044   000    Old_age   Always   41437
194 Temperature_Celsius     0x0022   097   091   000    Old_age   Always   50
197 Current_Pending_Sector  0x0032   200   200   000    Old_age   Always   0
198 Offline_Uncorrectable   0x0030   200   200   000    Old_age   Always   0

这里面每个字段的VALUE是归一化值(满分通常200或100),RAW_VALUE是原始计数。重点关注TYPE列——Pre-fail意味着这个属性是厂家认为的"故障前兆指标"。

哪几个指标真正能预判故障

Backblaze运营着30万块硬盘,他们每季度公开故障统计数据。根据他们多年的数据分析(加上我自己的经验),真正和故障强相关的SMART指标就这几个:

ID属性名含义危险信号
5Reallocated_Sector_Ct重映射扇区数RAW_VALUE > 0 且持续增长
187Reported_Uncorrectable无法纠正的错误数任何非零值
188Command_Timeout命令超时次数突然暴增
197Current_Pending_Sector等待重映射的扇区RAW_VALUE > 0
198Offline_Uncorrectable离线不可纠正扇区RAW_VALUE > 0
196Reallocated_Event_Count重映射事件计数持续增长

有个常见误解:ID 1 Raw_Read_Error_Rate 数字很大就代表盘要挂了。其实不是,希捷的盘这个值正常就是几百万,它的计算方式不一样。真正要盯的是上面那6个。

我的经验法则:Reallocated_Sector_Ct 的RAW值一旦开始涨,不管涨多少,这块盘就进入了"观察期"。如果一周内增长超过10个,直接安排换盘,不要犹豫。Current_Pending_Sector 出现非零值,配合Reallocated一起涨,基本可以确认这盘活不了多久了。

搭建数据采集管道

光知道看哪些指标没用,得持续采集才行。我用一个简单的bash脚本配合crontab做定时采集,数据存成CSV,后面喂给模型。

#!/bin/bash
# collect_smart.sh - 采集所有磁盘SMART数据
# 建议每6小时跑一次

TIMESTAMP=$(date +%Y-%m-%d_%H:%M:%S)
OUTPUT_DIR="/opt/smart_data"
mkdir -p ${OUTPUT_DIR}

for disk in $(lsblk -d -n -o NAME | grep -E '^sd|^nvme'); do
    DEV="/dev/${disk}"
    
    # 跳过不支持SMART的设备
    smartctl -i ${DEV} 2>/dev/null | grep -q "SMART support is: Enabled" || continue
    
    # 提取关键SMART属性
    SMART_DATA=$(smartctl -A ${DEV} 2>/dev/null)
    
    # 解析关键字段
    ID5=$(echo "$SMART_DATA" | awk '/Reallocated_Sector_Ct/{print $NF}')
    ID187=$(echo "$SMART_DATA" | awk '/Reported_Uncorrect/{print $NF}')
    ID188=$(echo "$SMART_DATA" | awk '/Command_Timeout/{print $NF}')
    ID197=$(echo "$SMART_DATA" | awk '/Current_Pending_Sector/{print $NF}')
    ID198=$(echo "$SMART_DATA" | awk '/Offline_Uncorrectable/{print $NF}')
    ID196=$(echo "$SMART_DATA" | awk '/Reallocated_Event_Count/{print $NF}')
    ID9=$(echo "$SMART_DATA" | awk '/Power_On_Hours/{print $NF}')
    ID194=$(echo "$SMART_DATA" | awk '/Temperature_Celsius/{print $NF}')
    
    # 写入CSV
    echo "${TIMESTAMP},${disk},${ID5:-0},${ID187:-0},${ID188:-0},${ID197:-0},${ID198:-0},${ID196:-0},${ID9:-0},${ID194:-0}" \
        >> ${OUTPUT_DIR}/smart_history.csv
done

crontab加上:

# 每6小时采集一次SMART数据
0 */6 * * * /opt/scripts/collect_smart.sh

CSV的表头:

timestamp,disk,reallocated_sector,reported_uncorrect,command_timeout,pending_sector,offline_uncorrectable,reallocated_event,power_on_hours,temperature

这个脚本跑了半年,每台机器上大概积累了几十MB的数据。数据量不大,但信息密度够用。

用smartd做实时告警(兜底方案)

在模型之外,smartd本身就能做基础告警,作为兜底:

# /etc/smartd.conf 配置示例
# 监控所有磁盘,检测到错误发邮件
DEVICESCAN -H -l error -l selftest -f \
    -m ops@yourdomain.com \
    -M exec /opt/scripts/smart_alert.sh \
    -s (S/../.././02|L/../../6/03)

参数解释:

  • -H:检查SMART健康状态
  • -l error:报告错误日志变化
  • -l selftest:报告自检结果
  • -f:检查Usage属性故障
  • -m:告警邮件收件人
  • -M exec:触发自定义脚本(可以发企微/钉钉/飞书)
  • -s:自动测试计划,S/../.././02表示每天凌晨2点跑短测试,L/../../6/03表示每周六凌晨3点跑长测试

自定义告警脚本可以对接企微机器人:

#!/bin/bash
# /opt/scripts/smart_alert.sh
WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"

curl -s -X POST ${WEBHOOK} \
    -H 'Content-Type: application/json' \
    -d "{
        \"msgtype\": \"markdown\",
        \"markdown\": {
            \"content\": \"## ⚠️ 磁盘健康告警\n> **主机**: $(hostname)\n> **设备**: ${SMARTD_DEVICE}\n> **消息**: ${SMARTD_MESSAGE}\n> **时间**: $(date '+%Y-%m-%d %H:%M:%S')\"
        }
    }"

启动smartd:

systemctl enable smartd
systemctl start smartd

# 测试告警是否正常(加-M test会立即发一封测试邮件)
# 在smartd.conf的DEVICESCAN行加上 -M test,重启后发完测试邮件再去掉

训练一个故障预测模型

smartd是规则告警——属性值达到阈值才报。但很多时候盘是"慢性病",值在涨但还没过阈值。这时候需要模型来捕捉趋势。

我用Backblaze的公开数据集做训练(他们的数据从2013年积累到现在,几十万块盘的SMART记录),然后把模型部署到自己的环境做推理。

下载Backblaze数据

# Backblaze每季度发布数据:https://www.backblaze.com/cloud-storage/resources/hard-drive-test-data
# 下载2024年Q4数据(约4GB解压后)
wget https://f001.backblazeb2.com/file/Backblaze-Hard-Drive-Data/data_Q4_2024.zip
unzip data_Q4_2024.zip -d /opt/backblaze_data/

数据预处理和模型训练

#!/usr/bin/env python3
"""
disk_failure_model.py - 基于SMART数据的硬盘故障预测模型
使用Backblaze数据集训练,XGBoost分类器
"""
import pandas as pd
import numpy as np
from pathlib import Path
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score
import xgboost as xgb
import joblib

# ========== 1. 加载数据 ==========
data_dir = Path("/opt/backblaze_data/")
dfs = []
for f in sorted(data_dir.glob("*.csv")):
    df = pd.read_csv(f, low_memory=False)
    dfs.append(df)

raw = pd.concat(dfs, ignore_index=True)
print(f"总记录数: {len(raw):,}")

# ========== 2. 特征选择 ==========
# 只保留和故障强相关的SMART属性
feature_cols = [
    'smart_5_raw',    # Reallocated_Sector_Ct
    'smart_187_raw',  # Reported_Uncorrectable_Errors
    'smart_188_raw',  # Command_Timeout
    'smart_197_raw',  # Current_Pending_Sector
    'smart_198_raw',  # Offline_Uncorrectable
    'smart_196_raw',  # Reallocated_Event_Count
    'smart_9_raw',    # Power_On_Hours
    'smart_194_raw',  # Temperature_Celsius
    'smart_1_raw',    # Raw_Read_Error_Rate
    'smart_7_raw',    # Seek_Error_Rate
]

# 过滤出含这些字段的记录
available_cols = [c for c in feature_cols if c in raw.columns]
df = raw[['serial_number', 'date', 'failure'] + available_cols].copy()
df[available_cols] = df[available_cols].fillna(0)

print(f"故障样本: {df['failure'].sum()}, 正常样本: {(df['failure']==0).sum()}")
print(f"故障比例: {df['failure'].mean()*100:.4f}%")

# ========== 3. 特征工程 ==========
# 添加变化率特征(模拟:用同一块盘前后记录的差值)
df = df.sort_values(['serial_number', 'date'])
for col in available_cols:
    df[f'{col}_diff'] = df.groupby('serial_number')[col].diff().fillna(0)

# 最终特征
all_features = available_cols + [f'{c}_diff' for c in available_cols]

# ========== 4. 处理类别不平衡 ==========
# 硬盘故障是极度不平衡的(故障率约0.01-0.1%)
# 使用scale_pos_weight参数处理
pos_count = df['failure'].sum()
neg_count = (df['failure'] == 0).sum()
scale_ratio = neg_count / pos_count if pos_count > 0 else 1

# ========== 5. 训练模型 ==========
X = df[all_features].values
y = df['failure'].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

model = xgb.XGBClassifier(
    n_estimators=200,
    max_depth=6,
    learning_rate=0.1,
    scale_pos_weight=scale_ratio,
    eval_metric='aucpr',
    use_label_encoder=False,
    random_state=42
)

model.fit(
    X_train, y_train,
    eval_set=[(X_test, y_test)],
    verbose=50
)

# ========== 6. 评估 ==========
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]

print("\n" + "="*50)
print("模型评估结果:")
print("="*50)
print(classification_report(y_test, y_pred, target_names=['正常', '故障']))
print(f"AUC-ROC: {roc_auc_score(y_test, y_prob):.4f}")

# ========== 7. 特征重要性 ==========
importance = model.feature_importances_
feat_imp = sorted(zip(all_features, importance), key=lambda x: x[1], reverse=True)
print("\nTop 10 重要特征:")
for feat, imp in feat_imp[:10]:
    print(f"  {feat}: {imp:.4f}")

# ========== 8. 保存模型 ==========
joblib.dump(model, '/opt/smart_model/disk_failure_xgb.pkl')
joblib.dump(all_features, '/opt/smart_model/feature_names.pkl')
print("\n模型已保存到 /opt/smart_model/")

这段代码直接能跑。有几个注意点:

  1. 类别不平衡问题:硬盘故障率通常不到0.1%,直接训练模型会偏向预测"不坏"。scale_pos_weight参数让模型对故障样本给更高权重。
  2. 变化率特征_diff特征非常重要。一块盘Reallocated_Sector从0涨到5,和另一块从50涨到55,含义完全不同。模型需要看到"变化"而不仅仅是"当前值"。
  3. Backblaze数据的局限:他们主要是消费级和企业级SATA盘,如果你用的是SAS盘或NVMe,SMART属性ID会不一样,需要做适配。

部署推理服务

模型训练好了,需要定时跑推理。我用一个Python脚本配合systemd timer:

#!/usr/bin/env python3
"""
predict_disk_failure.py - 读取本机SMART数据,用模型预测故障概率
"""
import subprocess
import json
import joblib
import numpy as np
import requests
from datetime import datetime

# 加载模型
model = joblib.load('/opt/smart_model/disk_failure_xgb.pkl')
feature_names = joblib.load('/opt/smart_model/feature_names.pkl')

# 企微webhook(换成你自己的)
WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
THRESHOLD = 0.6  # 故障概率超过60%就告警

def get_smart_data(device):
    """用smartctl获取SMART数据,返回JSON格式"""
    result = subprocess.run(
        ['smartctl', '-A', '-j', device],
        capture_output=True, text=True
    )
    try:
        data = json.loads(result.stdout)
        attrs = {}
        for item in data.get('ata_smart_attributes', {}).get('table', []):
            attr_id = item['id']
            raw_value = item['raw']['value']
            attrs[f'smart_{attr_id}_raw'] = raw_value
        return attrs
    except (json.JSONDecodeError, KeyError):
        return None

def predict(smart_attrs, prev_attrs=None):
    """构建特征向量并预测"""
    features = []
    base_ids = ['smart_5_raw', 'smart_187_raw', 'smart_188_raw',
                'smart_197_raw', 'smart_198_raw', 'smart_196_raw',
                'smart_9_raw', 'smart_194_raw', 'smart_1_raw', 'smart_7_raw']
    
    # 当前值
    for col in base_ids:
        features.append(smart_attrs.get(col, 0))
    
    # 变化率(和上次采集比)
    for col in base_ids:
        if prev_attrs:
            diff = smart_attrs.get(col, 0) - prev_attrs.get(col, 0)
        else:
            diff = 0
        features.append(diff)
    
    X = np.array(features).reshape(1, -1)
    prob = model.predict_proba(X)[0][1]
    return prob

def send_alert(device, prob, smart_attrs):
    """发送企微告警"""
    msg = (
        f"## 🔴 磁盘故障预警\n"
        f"> **主机**: {subprocess.getoutput('hostname')}\n"
        f"> **设备**: {device}\n"
        f"> **故障概率**: {prob*100:.1f}%\n"
        f"> **Reallocated_Sector**: {smart_attrs.get('smart_5_raw', 0)}\n"
        f"> **Pending_Sector**: {smart_attrs.get('smart_197_raw', 0)}\n"
        f"> **Power_On_Hours**: {smart_attrs.get('smart_9_raw', 0)}\n"
        f"> **时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
        f"**建议**:尽快安排换盘,优先备份该盘数据"
    )
    requests.post(WEBHOOK, json={
        "msgtype": "markdown",
        "markdown": {"content": msg}
    })

def main():
    import os
    import json as json_lib
    
    # 历史数据文件(存上次的SMART值,用于算diff)
    history_file = '/opt/smart_model/last_smart.json'
    prev_all = {}
    if os.path.exists(history_file):
        with open(history_file) as f:
            prev_all = json_lib.load(f)
    
    current_all = {}
    
    # 遍历所有磁盘
    result = subprocess.run(['lsblk', '-d', '-n', '-o', 'NAME'],
                          capture_output=True, text=True)
    disks = [d.strip() for d in result.stdout.strip().split('\n')
             if d.strip().startswith(('sd', 'nvme'))]
    
    for disk in disks:
        device = f"/dev/{disk}"
        smart_attrs = get_smart_data(device)
        if not smart_attrs:
            continue
        
        current_all[disk] = smart_attrs
        prev_attrs = prev_all.get(disk)
        
        prob = predict(smart_attrs, prev_attrs)
        print(f"[{datetime.now()}] {device}: 故障概率 {prob*100:.2f}%")
        
        if prob >= THRESHOLD:
            send_alert(device, prob, smart_attrs)
            print(f"  ⚠️  已发送告警!")
    
    # 保存当前数据作为下次的历史
    with open(history_file, 'w') as f:
        json_lib.dump(current_all, f)

if __name__ == '__main__':
    main()

systemd timer配置:

# /etc/systemd/system/disk-predict.service
[Unit]
Description=Disk Failure Prediction

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/scripts/predict_disk_failure.py
# /etc/systemd/system/disk-predict.timer
[Unit]
Description=Run disk prediction every 6 hours

[Timer]
OnCalendar=*-*-* 00/6:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now disk-predict.timer

踩过的坑

坑1:NVMe盘的SMART属性完全不一样

NVMe盘不走ATA SMART标准,它有自己的一套健康信息。smartctl输出格式不同:

# NVMe盘用这个命令
smartctl -a /dev/nvme0n1

# 关键指标不同:
# - Percentage Used: 磨损百分比(SSD寿命)
# - Media and Data Integrity Errors: 数据完整性错误
# - Critical Warning: 临界警告位

我的解决办法是在采集脚本里判断盘类型,分两套逻辑:

if [[ ${disk} == nvme* ]]; then
    # NVMe走nvme-cli或smartctl的NVMe模式
    PUSED=$(smartctl -a /dev/${disk} | grep "Percentage Used" | awk '{print $NF}' | tr -d '%')
    MEDIA_ERR=$(smartctl -a /dev/${disk} | grep "Media and Data Integrity" | awk '{print $NF}')
else
    # SATA/SAS走传统SMART
    ...
fi

坑2:希捷盘的Raw_Read_Error_Rate

前面提过,希捷盘的ID 1会显示几百万甚至上亿的RAW值。如果你不知道这个,第一次看到Raw_Read_Error_Rate = 83886080会吓一跳。

实际上希捷把多个计数器打包在一个48位字段里,低16位才是真正的错误数,高位是总操作数。别把这个喂给模型当"巨大错误数"。处理方法:

# 希捷盘Raw_Read_Error_Rate需要特殊处理
def parse_seagate_raw_read_error(raw_value):
    """希捷盘的raw value是复合值,低16位是错误数"""
    return raw_value & 0xFFFF

坑3:模型在新盘上误报

新盘通电时间短,某些属性的初始化值可能触发模型误报。我加了一个简单规则:Power_On_Hours < 720(一个月以内)的盘跳过预测,只做基础规则告警。

坑4:虚拟化环境拿不到SMART

KVM/ESXi虚拟机里是看不到宿主机磁盘SMART数据的。这个脚本得部署在物理机或者通过BMC/iDRAC的API去抓。如果是公有云ECS,就别想了,云厂商自己做了这层。

效果和改进方向

跑了半年的数据:

  • 监控磁盘总数:约200块(HDD + SSD混合)
  • 成功预警:12块盘在故障前1-3周被换掉
  • 误报:3次(新盘初始化 + 希捷盘RAW值解析问题,修复后降为0)
  • 漏报:0次
时间事件预警提前量结果
2月sda Reallocated快速增长提前18天换盘后原盘离线测试确认坏道
3月sdb Pending_Sector出现提前9天RAID降级但无数据丢失
5月nvme0n1 Percentage_Used 98%提前21天SSD寿命到期,换新
6月sdc Command_Timeout暴增提前3天接口问题,换线缆解决

后续我准备做的改进:

  1. 接入Prometheus + Grafana:用smartctl_exporter把SMART数据暴露成Prometheus指标,做趋势可视化
  2. 模型定期refit:每季度用最新数据重新训练,适应盘型变化
  3. Ceph集成:Ceph自带的设备健康预测模块(ceph device predict-life-expectancy)用的也是类似原理,可以参考它的实现给自己的系统加强

Prometheus + Grafana可视化(附配置)

smartctl_exporter可以直接把SMART数据暴露成Prometheus指标:

# 安装smartctl_exporter
# 从GitHub Release下载: https://github.com/prometheus-community/smartctl_exporter
wget https://github.com/prometheus-community/smartctl_exporter/releases/download/v0.12.0/smartctl_exporter-0.12.0.linux-amd64.tar.gz
tar xzf smartctl_exporter-0.12.0.linux-amd64.tar.gz
cp smartctl_exporter-0.12.0.linux-amd64/smartctl_exporter /usr/local/bin/

# systemd service
cat > /etc/systemd/system/smartctl-exporter.service << 'EOF'
[Unit]
Description=Smartctl Exporter
After=network.target

[Service]
ExecStart=/usr/local/bin/smartctl_exporter --smartctl.path=/usr/sbin/smartctl --web.listen-address=:9633
Restart=always

[Install]
WantedBy=multi-user.target
EOF

systemctl enable --now smartctl-exporter

Prometheus配置加一行:

# prometheus.yml
scrape_configs:
  - job_name: 'smartctl'
    static_configs:
      - targets: ['node1:9633', 'node2:9633', 'node3:9633']
    scrape_interval: 5m  # SMART数据变化慢,5分钟够了

Grafana可以用Dashboard ID 20204(smartctl_exporter官方仪表板),直接import就有完整的磁盘健康视图。

总结

阶段工具作用
数据采集smartmontools + cron/systemd定时读取SMART属性
实时告警smartd + 企微webhook阈值触发立即通知
预测分析XGBoost + Backblaze数据提前1-3周预判故障
可视化smartctl_exporter + Prometheus + Grafana趋势监控和仪表板

磁盘故障预测不是什么高深的东西,核心就是两步:持续采集数据 + 发现异常趋势。即使你不想搞模型,单纯把smartd配好,加上每周看一眼Reallocated_Sector有没有涨,就已经能避掉80%的"突然死亡"了。

别等到RAID红灯亮了才去看日志——那时候你能做的就只剩祈祷重建跑得比第二块盘挂得快了。


我是「运维躬行录」,专注分享云计算和运维的生产实践经验。如果这篇文章对你有帮助,帮忙点个赞、转发一下。
关注公众号:耕云躬行录
个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码