Article

ハードウェア故障の予兆を見逃さない監視方法

高田晃太郎
ハードウェア故障の予兆を見逃さない監視方法

大規模な公開データでは、年間のHDD故障率は概ね1〜3%程度と報告され、特定の年齢・モデルで上振れすることがあります[1][2][6]。研究や統計の文脈では、SMART(Self-Monitoring, Analysis and Reporting Technology:自己診断ログ)属性の異常や温度の偏りが事前に観測される事例が一定割合で認められ[2]、メモリについても大規模な実運用研究で、年間の一部DIMMに訂正可能エラー(Correctable Error)が観測される傾向が示されています[4]。また、現場の運用経験や検証環境の観測例として、ファン回転数の乱高下やNVMeのメディア消耗度(percentage used)の急伸が、実際の故障に先行するケースもあります。

問題は、これらのサインが常に大きな警報として現れるわけではないことです。しきい値を超えない微小な傾き、エラー件数のわずかな増分、季節要因による温度の底上げなど、日常のノイズに紛れる変化が多いのが現実です。だからこそ、単発の閾値越えではなく、傾向・速度・相関を取り込む監視設計が要になります。本稿では、CTOやエンジニアリーダーが現場に持ち帰れる形で、主要コンポーネント別の観測指標、Prometheus系スタックでの実装、ノイズ低減の運用までを通しで解説します。

全体像と設計思想:予兆は“点”ではなく“面”で捉える

予兆検知の基本は、ディスクやメモリといった単一デバイスの健康診断に留まらず、筐体温度・電源・振動・I/O待ち時間といった周辺の環境信号を併せて“面”で捉えることです。例えば2007年の大規模研究では、特定のSMART属性の閾値越え単独の予測力は限定的である一方[2]、温度やCRCエラーの増分と組み合わせることで予測性能が改善するという指摘があります[1]。実装上は、軽量エージェントでメトリクスを収集し、中央で時系列として保持、しきい値だけでなく傾き(rate)や移動平均、季節性の分離を使い分けます。通知は一段ではなく、警告と要注意を分けて人的負荷を平準化し、交換や予備機調達の意思決定につなげます。

対象環境はLinuxカーネル5系を前提に、smartmontools 7系、nvme-cli、edac-utils、lm-sensors、ipmitool、mdadmやzpool等のユーティリティを利用します。収集はNode Exporterのtextfile collectorや専用Exporter、自作の軽量エージェントで行い、Prometheus 2系で集約、Alertmanagerで抑制とルーティング、Grafanaで可視化する構成を土台とします。権限は原則root不要で動かしつつ、デバイスアクセスが必要な範囲のみcapabilityを付与します。

設計で最も大切なのは、ノイズよりも逸失検知のコストが大きい箇所に投資することです。ストレージはMTBFに直結するため傾向検知を厚く、メモリは訂正可能エラーのバーストを短時間で拾い、温度は相対変化と絶対上限を併用します。ポーリング周期はデバイス負荷とのトレードオフで、SMARTのフルダンプは1〜6時間間隔、温度やファンは15〜60秒、EDACは1分程度が実用的です。深い自己診断やRAIDスクラブは深夜帯にスケジュールし、I/Oの競合を避けます。

前提条件と検証環境

以下は一例としての検証構成です。2ソケットのx86サーバを複数台、NVMe SSDとSATA HDDの混載、ECC付きDDR4 DIMMを用いました。Prometheusは2.53系、Node Exporterは1.7系、Grafanaは10系、Alertmanagerは0.27系、Retentionは30日としました。smartctlの軽量ヘルスチェック(-H/-aj)は短い間隔で回しても、一般的なサーバではI/O待ち時間やスループットの目立つ劣化は出にくく、CPUオーバーヘッドも小さい範囲に収まるのが通常です。フルJSONダンプは数時間に一度の実行で、1台あたり数秒以内に収まる構成を目安とするとよいでしょう。いずれも環境依存であるため、本番適用前に負荷計測で確認してください。

検知戦略の勘所

単純なしきい値をやめるのではなく、絶対値・速度・持続時間の三点で見ることが要です。例えばReallocated Sector Count(不良セクタ代替の累計)が0から1に増えるだけでは交換トリガーには早いことが多く、Current Pending Sector(再読み出し待ちの疑義セクタ)が同時に増え、かつReadエラー率が週次で上がっているなら早期交換の合理性が高まります。温度は絶対上限だけでなく、そのサーバ群のベースラインからの乖離に敏感になりましょう。ラック内のホットスポットやファンの劣化は相対差で浮かび上がります。ネットワークはFCS/CRCエラーやレシーブドロップの増分に注目し、PCIe AER(Advanced Error Reporting)のCorrectableがバーストする場合はケーブルやスロットの物理要因を疑います。

コンポーネント別の観測ポイントとしきい値設計

ストレージは予兆監視の主戦場です。HDDであればReallocated_Sector_Ct(代替セクタ累計)、Current_Pending_Sector(不安定セクタ数)、UDMA_CRC_Error_Count(ケーブル等のCRCエラー)、Multi_Zone_Error_Rate(複数ゾーンの読み書きエラー)など、SSD/NVMeであればMedia_Wearout_Indicator/Percentage Used(消耗度)、Available Spare(予備領域残)、Data Units Read/Write(総読み書き量)、Error Information Logのエントリ数やCRCエラーに着目します。絶対値のしきい値に加えて、週次の移動平均に対する乖離や日次の増分が連続することを条件にし、検知を一段安定させます。温度は40〜45℃超の時間割合が伸びると故障リスクが上がる傾向が示されているため[1][2]、単発越えではなく一定時間以上の継続を条件にするのが現実的です。RAIDはmdadmやベンダツールのログに出るリシンクやデグレの兆候をイベントとして扱い、ZFSはzpool statusのCksumエラーやScrubの不一致を時系列で持ちます。

メモリはEDAC(Error Detection and Correction:Linuxのメモリエラー収集フレームワーク)のCorrectable Errorが連続して増える状況に敏感であるべきです。大規模研究では、訂正可能エラーの観測後に同一DIMMで不訂正エラー(UE)が発生する確率がベースラインより高まることが示されており[4]、一定量の訂正可能エラーを閾値としてDIMM交換計画を前倒しする判断は未然防止に寄与します。DIMMスロット単位のホットスポットを特定するため、エラーの位置情報を必ず記録し、再現性のあるバーストか単発かを見極めます。

CPUと筐体は温度とクロックの関係に注目します。サーマルスロットリング(高温時の自動減速)の発生率が上がると、同一負荷でも処理時間がじわりと伸びます。ファンの回転数は単位時間あたりの変動幅が広がると軸受の劣化や埃詰まりの兆候であることが多く、PSUの電圧センサーに小さなノイズが重畳する場合はケーブルやコンセントの劣化も視野に入ります。IPMI(Intelligent Platform Management Interface)経由のセンサー値は個体差があるため、群集の相対比較で外れ値を見つける設計が効果的です。

ネットワークは、インターフェイスごとのCRCエラー、ドロップ、再送の増分と、同時期におけるリンクのUp/Downイベントの相関を確認します。ケーブルやトランシーバの物理劣化は負荷に比例しないタイミングで発生率が上がることがあり、温度や振動といった環境データと合わせて判断材料を増やしましょう。PCIe AERのCorrectableもベンダロギングと突き合わせると、原因切り分けの速度が大きく向上します。

実装ガイド:収集・集約・アラートの具体例

まずは既存のNode Exporterにtextfile collectorを併用し、SMARTの要点を時系列に落とし込む方法です。軽量に始め、必要に応じて専用Exporterや自作エージェントへ発展させます。以下はsmartctlのJSONをパースして、主要属性をPrometheusのテキストフォーマットに書き出すシェルスクリプトの例です。タイムアウトと終了コードの評価でエラーハンドリングを入れています。

#!/usr/bin/env bash
set -euo pipefail
OUT_DIR="/var/lib/node_exporter/textfile_collector"
DEV_LIST=(/dev/sd[a-z] /dev/nvme[0-9]n1)
TS=$(date +%s)
mkdir -p "${OUT_DIR}"
TMP=$(mktemp)
cleanup(){ rm -f "$TMP"; }
trap cleanup EXIT
for DEV in "${DEV_LIST[@]}"; do
  if [ ! -e "$DEV" ]; then continue; fi
  if [[ $DEV == /dev/nvme* ]]; then
    CMD=(timeout 5s smartctl -aj "$DEV")
  else
    CMD=(timeout 5s smartctl -a -j "$DEV")
  fi
  if ! JSON=$("${CMD[@]}" 2>/dev/null); then
    echo "hardware_smart_last_success{device=\"$DEV\"} 0 $TS" >> "$TMP"
    continue;
  fi
  echo "hardware_smart_last_success{device=\"$DEV\"} 1 $TS" >> "$TMP"
  if [[ $DEV == /dev/nvme* ]]; then
    AVAIL=$(jq '.nvme_smart_health_information_log.available_spare' <<< "$JSON")
    USED=$(jq '.nvme_smart_health_information_log.percentage_used' <<< "$JSON")
    TEMP=$(jq '.temperature.current' <<< "$JSON")
    echo "hardware_nvme_available_spare{device=\"$DEV\"} $AVAIL" >> "$TMP"
    echo "hardware_nvme_percentage_used{device=\"$DEV\"} $USED" >> "$TMP"
    echo "hardware_drive_temperature_celsius{device=\"$DEV\"} $TEMP" >> "$TMP"
  else
    REALLOC=$(jq '.ata_smart_attributes.table[] | select(.name=="Reallocated_Sector_Ct").raw.value // 0' <<< "$JSON")
    PENDING=$(jq '.ata_smart_attributes.table[] | select(.name=="Current_Pending_Sector").raw.value // 0' <<< "$JSON")
    CRC=$(jq '.ata_smart_attributes.table[] | select(.name=="UDMA_CRC_Error_Count").raw.value // 0' <<< "$JSON")
    TEMP=$(jq '.temperature.current // 0' <<< "$JSON")
    echo "hardware_smart_reallocated_sectors{device=\"$DEV\"} $REALLOC" >> "$TMP"
    echo "hardware_smart_pending_sectors{device=\"$DEV\"} $PENDING" >> "$TMP"
    echo "hardware_smart_crc_errors{device=\"$DEV\"} $CRC" >> "$TMP"
    echo "hardware_drive_temperature_celsius{device=\"$DEV\"} $TEMP" >> "$TMP"
  fi
done
mv "$TMP" "${OUT_DIR}/smart.prom"

次に、Pythonでnvme-cliとPushgatewayを併用する方法です。短いインターバルで回すため、タイムアウトと例外処理を明示し、失敗時はリトライを遅延させます。

#!/usr/bin/env python3
import json
import subprocess
import time
import socket
import sys
from urllib import request, error

PUSHGATEWAY = "http://pushgateway:9091/metrics/job/hw_nvme/instance/{}".format(socket.gethostname())
NVME_DEVICES = ["/dev/nvme0n1", "/dev/nvme1n1"]

def nvme_smart(dev):
    try:
        out = subprocess.check_output(["nvme", "smart-log", dev, "-o", "json"], timeout=5)
        return json.loads(out)
    except (subprocess.CalledProcessError, subprocess.TimeoutExpired, json.JSONDecodeError) as e:
        return {"error": str(e)}

def format_metrics(dev, data):
    lines = []
    if "error" in data:
        lines.append(f"hardware_nvme_last_success{{device=\"{dev}\"}} 0")
        return "\n".join(lines).encode()
    lines.append(f"hardware_nvme_last_success{{device=\"{dev}\"}} 1")
    lines.append(f"hardware_nvme_percentage_used{{device=\"{dev}\"}} {data.get('percentage_used', 0)}")
    lines.append(f"hardware_nvme_data_units_written{{device=\"{dev}\"}} {data.get('data_units_written', 0)}")
    lines.append(f"hardware_drive_temperature_celsius{{device=\"{dev}\"}} {data.get('temperature', 0)}")
    return "\n".join(lines).encode()

def push(body: bytes):
    req = request.Request(PUSHGATEWAY, data=body, method="PUT")
    try:
        with request.urlopen(req, timeout=3) as resp:
            if resp.status >= 300:
                raise RuntimeError(f"bad status: {resp.status}")
    except (error.URLError, TimeoutError, RuntimeError) as e:
        print(f"push error: {e}", file=sys.stderr)
        time.sleep(2)

if __name__ == "__main__":
    for dev in NVME_DEVICES:
        data = nvme_smart(dev)
        body = format_metrics(dev, data)
        push(body)

集約側は通常のPrometheus設定で取得します。Pushgatewayはジョブ粒度の寿命管理を怠るとスタックしがちなので、なるべくPull型のtextfileやExporterを優先しつつ、どうしてもPullしづらい計測のみ限定的に使います。以下は最小のスクレープ設定と、アラートルールの断片です。ラベル設計を意識し、サイレンスの容易さを確保します。

global:
  scrape_interval: 15s
scrape_configs:
  - job_name: node
    static_configs:
      - targets: ["node1:9100", "node2:9100"]
  - job_name: pushgateway
    honor_labels: true
    static_configs:
      - targets: ["pushgateway:9091"]

rule_files:
  - /etc/prometheus/alerts.yml
groups:
  - name: hardware-predictive
    rules:
      - alert: DiskSmartPendingSectorsIncrease
        expr: increase(hardware_smart_pending_sectors[24h]) > 0
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "Pending sectors increased"
          description: "{{ $labels.device }} on {{ $labels.instance }} shows pending sectors rising in 24h"
      - alert: NvmeWearOutFastRise
        expr: rate(hardware_nvme_percentage_used[7d]) > 0.5
        for: 6h
        labels:
          severity: critical
        annotations:
          summary: "NVMe wear-out accelerating"
          description: "{{ $labels.device }} wear percentage used rising too fast"
      - alert: DriveHighTempSustained
        expr: avg_over_time(hardware_drive_temperature_celsius[30m]) > 50
        for: 30m
        labels:
          severity: warning
        annotations:
          summary: "Drive temperature sustained high"
          description: "{{ $labels.device }} average > 50C for 30m"

定期ジョブはsystemdに載せると可観測性が上がります。タイムアウト、リトライ、ジャーナルの保存期間を明示し、Prometheusのnode_systemd_unit_stateで失敗を拾えるようにしておくと運用が軽くなります。

# /etc/systemd/system/smart-collector.service
[Unit]
Description=SMART collector to textfile
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/smart_to_prom.sh
TimeoutStartSec=20
SuccessExitStatus=0

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/smart-collector.timer
[Unit]
Description=Run SMART collector periodically

[Timer]
OnBootSec=2m
OnUnitActiveSec=1h
AccuracySec=1m
Persistent=true

[Install]
WantedBy=timers.target

筐体や電源のセンサーはIPMI経由で取り出すのが手堅いですが、環境によってはExporterの導入が難しいことがあります。その場合でも、短いGoプログラムでipmitoolをラップしHTTPでメトリクスを公開すれば、導入の自由度が上がります。異常終了時にHTTP 500を返して監視側で検知できるようにします。

package main

import (
    "log"
    "net/http"
    "os/exec"
    "strconv"
    "strings"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    tempGauge = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{Name: "hardware_chassis_temperature_celsius", Help: "IPMI temp"},
        []string{"sensor"},
    )
)

func scrape() error {
    cmd := exec.Command("ipmitool", "sdr", "type", "Temperature")
    cmd.Timeout = 5 * time.Second
    out, err := cmd.Output()
    if err != nil {
        return err
    }
    lines := strings.Split(string(out), "\n")
    for _, l := range lines {
        // Format example: "CPU Temp       | 42 degrees C      | ok"
        parts := strings.Split(l, "|")
        if len(parts) < 2 { continue }
        name := strings.TrimSpace(parts[0])
        valFields := strings.Fields(parts[1])
        if len(valFields) < 1 { continue }
        v, err := strconv.ParseFloat(valFields[0], 64)
        if err != nil { continue }
        tempGauge.WithLabelValues(name).Set(v)
    }
    return nil
}

func main() {
    prometheus.MustRegister(tempGauge)

    http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
        if err := scrape(); err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            _, _ = w.Write([]byte("scrape error"))
            return
        }
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok"))
    })

    http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
        if err := scrape(); err != nil {
            log.Printf("scrape error: %v", err)
        }
        promhttp.Handler().ServeHTTP(w, r)
    })

    log.Fatal(http.ListenAndServe(":9105", nil))
}

メモリのEDACは/procや/sysをパースすれば十分です。以下はシンプルなBashでDIMM単位の訂正可能エラーを合算して出す例です。ゼロ埋めとラベルの正規化を入れ、突然のテキストフォーマット崩れを避けています。

#!/usr/bin/env bash
set -euo pipefail
OUT="/var/lib/node_exporter/textfile_collector/edac.prom"
TMP=$(mktemp)
trap 'rm -f "$TMP"' EXIT
for ce in /sys/devices/system/edac/mc/*/csrow*/*ce_count; do
  [ -e "$ce" ] || continue
  v=$(cat "$ce" 2>/dev/null || echo 0)
  label=$(echo "$ce" | sed -E 's#.*/(mc[0-9]+)/csrow([0-9]+)/.*#\1_row\2#')
  echo "hardware_edac_ce_count{dimm=\"$label\"} $v" >> "$TMP"
done
mv "$TMP" "$OUT"

以上の収集を前提に、PromQL側では絶対値と傾きを併用します。以下はトレンド検知のためのクエリ例です。局所的な増分に敏感すぎないよう、ウィンドウ幅を対象メディアの寿命や業務パターンに合わせてチューニングしてください。

-- NVMe消耗の7日傾き
rate(hardware_nvme_percentage_used[7d])

-- CRCエラーの24時間増分
increase(hardware_smart_crc_errors[24h])

-- 温度の30分平均と群集平均との差(deviceラベルを除外して全体平均を計算)
avg_over_time(hardware_drive_temperature_celsius[30m]) - avg without(device) (hardware_drive_temperature_celsius)

より安定させるには、あらかじめ対象グループ(例:同一モデルやラック)でrecording ruleによりベースライン平均を作成し、その系列との差分を取ると運用しやすくなります。

運用最適化とROI:ノイズを抑え、意思決定へつなぐ

予兆検知は当てることが目的化しがちですが、現場で価値を生むのは交換計画や業務影響の最小化に繋げたときです。実務上は、SMARTの単発警告で直ちに交換するのではなく、傾向が出ている個体を優先順位付きのリストに載せ、メンテナンスウィンドウに合わせて計画交換するのが費用対効果に優れます。ディスクの在庫を少数のホットスワップ用に絞り、サプライチェーンのリードタイムを踏まえて発注トリガーをアラートと連動させると、余剰在庫の圧縮とダウンタイムの同時削減が期待できます。

ノイズ対策は三段で行います。まずはアラートのデデュープとルーティングで、同一ホスト・同一デバイスに関する短時間の重複通知を抑えます。次に、季節性のある温度や夜間バッチのI/O負荷など、説明可能な反復パターンに対してはサプレッションウィンドウを構成します。最後に、意思決定に必要な情報量を警報に含めます。たとえばNVMeの使用割合が急伸している警報には、その個体の総書き込み量、稼働時間、モデル、同型番群の平均との差分を含めることで、交換の合理性を迅速に判断できます。

ビジネス価値の観点では、ダウンタイムの削減とMTTRの短縮に直接効きます。例えば500台規模の環境で、ストレージの年間故障率を2%と仮定し、予兆検知で60%を事前交換に回せたとすると、無計画ダウンは80件から32件に減ります。1件あたり平均2時間の業務停止が回避できるとすれば、64時間分の影響低減です。交換の先行コストや作業時間を織り込んでも、SLA違反による損失と比べて十分なROIが見込めます。加えて、深夜呼び出しが減ることでオンコールのバーンアウト防止や採用・定着の面でもポジティブな効果が期待できます。

最後に、監視は一度作って終わりではありません。新しいモデルやファームウェアでSMART属性の意味が微妙に変わることがあり、EDACのカーネル実装も改善が続きます。レコーディングルールやダッシュボードは四半期ごとに見直し、誤検知と見逃しを振り返ってしきい値とウィンドウ幅を更新しましょう。運用の学びをナレッジ化し、ラベル設計や命名規約、アラートの説明文テンプレートに反映すると、チーム全体の保守性が長期で高まります。より体系的なSLO設計と結びつけたい場合は、SREの文脈でハードウェアのSLIを定義し、予兆検知による改善を数字で可視化することをおすすめします。関連する導入記事として、PrometheusとAlertmanagerの実務構成解説やSLO入門、ストレージ階層のパフォーマンスチューニングも参照すると、全体最適の視点が得られます。

まとめ:壊れる前に動くチームを設計する

ハードウェアは黙って壊れるわけではなく、微かな変化を必ず残します。SMARTやEDAC、IPMIといった一次信号を軽量に収集し、Prometheusで傾向と速度を併用して判断する設計に切り替えれば、故障の後追いから脱却できます。本文の実装例をベースにまず1台から始め、傾向検知のクエリとアラート文面をチームの運用に合わせて育てていきましょう。

ノイズを恐れて見逃すより、低コストな観測を広く敷いて早めに動く方が、結果として安全で安い——これは多くの現場で繰り返し観測される傾向です。今週は重要サーバのNVMeとHDDのSMART収集をスケジュールに載せ、来週はEDACとIPMIの導入、そして翌週にアラートのチューニングという三段で試してみませんか。最初の1台がうまく回れば、残りはパターン化して横展開できます。

参考文献

  1. Backblaze. Backblaze Drive Stats for Q1 2023. https://www.backblaze.com/blog/backblaze-drive-stats-for-q1-2023/
  2. Eduardo Pinheiro, Wolf-Dietrich Weber, Luiz André Barroso. Failure Trends in a Large Disk Drive Population. USENIX FAST 2007. https://www.usenix.org/events/fast07/tech/full_papers/pinheiro/pinheiro_html/index_bak.html
  3. 日経xTECH. 「“はどれほど正確か”への調査結果は重要である。」GoogleのHDD大規模調査の解説(2007年)。https://xtech.nikkei.com/it/article/COLUMN/20070529/272852/
  4. Bianca Schroeder, Eduardo Pinheiro, Wolf-Dietrich Weber. DRAM Errors in the Wild: A Large-Scale Field Study. SIGMETRICS 2009(スタディリブ転載版)。https://studylib.net/doc/10432210/dram-errors-in-the-wild—a-large-scale-field-study-bianca
  5. Hard disk drive failure rates in datacenters(ResearchGate抄録)。https://www.researchgate.net/publication/351548704_Hard_disk_drive_failure_rates_in_datacenters
  6. Bianca Schroeder, Garth A. Gibson. Disk Failures in the Real World(CMU/FAST関連研究の要約・転載)。https://studylib.net/doc/10432207/disk-failures-in-the-real-world—bianca-schroeder-garth-a