psutil × lm-sensors × NVMe SMARTで完全把握!NUCBOX(ミニPC)のシステム情報をInfluxDBに丸ごと記録する

InfluxDB

スマートホームの中核サーバーとして NUCBOX(Intel N100搭載ミニPC)を24時間稼働させていると、「CPU温度は大丈夫か」「NVMeの寿命はどれくらいか」「メモリやディスクは逼迫していないか」が気になってきます。
本記事では、psutillm-sensorsNVMe SMART の3つのツールを組み合わせ、NUCBOXのシステム情報をPythonで収集してInfluxDB v2に記録する方法を解説します。CPU使用率・温度・Turbo Boost状態・NVMeの寿命・ネットワークI/Oなど、サーバー運用に必要な指標を一括で5秒ごとに蓄積できます。

なぜこの機能を作ったのか?

我が家では NUCBOX を24時間365日稼働するスマートホームサーバーとして使っています。各種自動化プログラム・InfluxDB・Grafana・NGINX・Flaskなど多くのサービスが同時に動いており、ハードウェアへの負荷がそれなりにあります。
「気づいたらCPUが高温でサーマルスロットリングしていた」「NVMeの寿命がいつの間にか消耗していた」という状況を事前に検知・可視化したいと考え、システム情報を定期的にInfluxDBへ記録する仕組みを作りました。

サーバーの異常をGrafanaで見えるようにして、障害が起きる前に気づきたい!

実現したいこと

  • CPU使用率・温度・コア別情報・Turbo Boost状態を取得する
  • メモリ・スワップの使用状況を取得する
  • ディスク使用率・読み書きI/Oを取得する
  • ネットワークの送受信データ量を取得する
  • NVMeの温度・SMART情報(寿命・電源投入回数・異常シャットダウン数など)を取得する
  • CPUサーマルスロットリングの発生状況を取得する
  • 以上の情報を5秒ごとにInfluxDB v2へ自動記録する

この記事でわかること

  • psutil を使ったCPU・メモリ・ディスク・ネットワーク情報の取得方法
  • lm-sensors(sensorsコマンド) を使ったCPU・NVMe温度の取得方法
  • nvme smart-log コマンドを使ったNVMe SMART情報の取得方法
  • Intelの Turbo Boost 状態と サーマルスロットリング の監視方法
  • 収集したデータをInfluxDB v2へ一括保存する方法

必要な準備と用意するもの

ハードウェア
  • NUCBOX(Intel N100 搭載ミニPC)または同等のLinux搭載PC
    • NVMe SSD 搭載モデル推奨(NVMe SMART取得のため)
ソフトウェア/サービス
  • Python 3
    • psutil
  • lm-sensors(sensors コマンド)
  • nvme-cli(nvme コマンド)
  • InfluxDB v2(稼働済み)

完成イメージ

プログラムを起動すると5秒ごとに以下の処理が繰り返されます。

① psutil で CPU・メモリ・ディスク・ネットワーク情報を取得
sensors コマンドで CPU温度・NVMe温度を取得
nvme smart-log コマンドでNVMe SMART情報を取得
④ /sysファイルシステムからサーマルスロットリング・Turbo Boost状態を取得
⑤ 全データをInfluxDB v2 の nucbox_system_log measurementに保存
⑥ Grafanaでシステム状態をリアルタイムに可視化できる

システムの仕組み

取得する情報のカテゴリ

収集するデータは大きく6つのカテゴリに分かれます。

  • CPU情報:全体使用率・コア別使用率・周波数・温度・ロードアベレージ・Turbo Boost・サーマルスロットリング
  • メモリ情報:物理メモリ使用率・使用量・空き容量・スワップ使用量
  • ディスク情報:パーティション使用率・累積読み書きバイト数・I/O回数
  • ネットワーク情報:累積送受信データ量・パケット数
  • NVMe温度:Composite温度・センサー1(コントローラ)・センサー2(NAND)
  • NVMe SMART情報:累積読み書き量・寿命消費率・電源投入回数・異常シャットダウン数・メディアエラーなど

情報取得に使うツールの役割分担

psutil だけではCPU温度やNVMe SMART情報が取れないため、複数のツールを使い分けます。

  • psutil:CPU使用率/周波数/ロードアベレージ、メモリ、ディスク、ネットワーク、コア別温度(sensors_temperatures)
  • sensorsコマンド(lm-sensors):CPUパッケージ温度、NVMe温度(Composite / Sensor1 / Sensor2)
  • nvme smart-logコマンド(nvme-cli):NVMe SMARTデータ(寿命・電源投入回数・メディアエラーなど)
  • /sysファイルシステム:サーマルスロットリング回数、Intel P-State(no_turbo)フラグ

実装のポイント

sensorsコマンドのパース

sensors コマンドの出力はテキスト形式のため、対象の行を見つけてパースします。
CPUパッケージ温度は "Package id 0" という行から、NVMe温度は "nvme-pci" セクションの Composite / Sensor 1 / Sensor 2 行から取得します。

sensorsの出力例
Package id 0: +45.0°C (high = +80.0°C, crit = +100.0°C)
nvme-pci-0100
Composite: +38.9°C
Sensor 1: +38.9°C
Sensor 2: +47.9°C

NVMe SMARTデータの取得(sudo必要)

nvme smart-log /dev/nvme0 は root権限が必要なため sudo で実行します。
systemdサービスとして動かす場合や、sudoersの設定が必要な点に注意してください。

nvme smart-logsudo権限が必要 です。一般ユーザーで実行するとエラーになります。systemdサービスで動かす場合は User=root にするか、sudoersで該当コマンドのパスワードなし実行を許可してください。

Turbo Boost状態の判定

Intel P-Stateドライバの /sys/devices/system/cpu/intel_pstate/no_turbo を読み取ることでTurbo Boostの有効/無効を判定します。
さらに「現在実際にBoostしているか」は、現在の動作周波数がベース周波数(N100の場合1.2GHz)を超えているかで判断します。

Intel N100のベース周波数は800MHz、最大Burst周波数は3.4GHzです。cpu_is_boosting フラグは1200MHz(1.2GHz)超えを基準にしていますが、CPUに合わせて調整してください。

コア別データの動的生成

コア数に応じて cpu_core0_percentcpu_core0_mhzcore0_temp のようなキーを動的に生成して辞書に格納します。
これにより、CPUコア数が異なる環境でもコードを変更せずに対応できます。

事前準備

psutilのインストール

pip install psutil

lm-sensorsのインストールと設定

sudo apt install lm-sensors
sudo sensors-detect   # センサーの自動検出(基本的にEnterを押し続ける)
sensors               # 動作確認

sensors の出力に Package id 0nvme-pci のセクションが含まれていれば準備OKです。

nvme-cliのインストール

sudo apt install nvme-cli
sudo nvme smart-log /dev/nvme0   # 動作確認

SMART情報が表示されれば準備OKです。/dev/nvme0 はNVMeデバイスのパスで、環境によっては /dev/nvme1 などになる場合があります。

実装方法

プログラム全体構成

  • get_cpu_temp():sensorsコマンドからCPUパッケージ温度を取得
  • get_nvme_temps():sensorsコマンドからNVMe温度3種を取得
  • get_nvme_smart():nvme smart-logコマンドからSMART情報を取得
  • get_thermal_throttle():/sysからサーマルスロットリング回数を取得
  • get_turbo_status():Turbo Boost有効フラグと現在のBoost状態を取得
  • record_nucbox_status():全情報を収集してInfluxDBへ保存するメイン処理
  • メインループ:5秒ごとに record_nucbox_status() を繰り返す

nucbox_system_record.py

# -*- coding: utf-8 -*-
import os
import socket
import time
import subprocess
import psutil
from influx.v2 import writer as influxwriter
import smarthomelog

def get_cpu_temp():
    """sensorsコマンドからCPUパッケージ温度を取得"""
    try:
        output = subprocess.check_output(["sensors"]).decode()
        for line in output.splitlines():
            if "Package id 0" in line:
                return float(line.split("+")[1].split("°")[0])
    except:
        return -1
    return -1

def get_nvme_temps():
    """sensorsコマンドからNVMe温度3種(Composite/Sensor1/Sensor2)を取得"""
    temps = {
        "nvme_composite": -1,
        "nvme_sensor1": -1,
        "nvme_sensor2": -1
    }
    try:
        output = subprocess.check_output(["sensors"]).decode()
        in_nvme = False
        for line in output.splitlines():
            if "nvme-pci" in line:
                in_nvme = True
            elif in_nvme:
                if "Composite" in line:
                    temps["nvme_composite"] = float(line.split("+")[1].split("°")[0])
                elif "Sensor 1" in line:
                    temps["nvme_sensor1"] = float(line.split("+")[1].split("°")[0])
                elif "Sensor 2" in line:
                    temps["nvme_sensor2"] = float(line.split("+")[1].split("°")[0])
                elif line.strip() == "":
                    break
    except:
        pass
    return temps

def get_nvme_smart():
    """nvme smart-logコマンドからSMART情報を取得(sudo必要)"""
    stats = {
        "data_read_tb": -1, "data_written_tb": -1,
        "power_cycles": -1, "power_on_hours": -1, "unsafe_shutdowns": -1,
        "percentage_used": -1, "media_errors": -1, "error_log_entries": -1,
        "warning_temp_time": -1, "critical_temp_time": -1,
        "thermal_t1_count": -1, "thermal_t1_time": -1,
        "controller_busy_time": -1
    }
    try:
        output = subprocess.check_output(
            ["sudo", "nvme", "smart-log", "/dev/nvme0"]
        ).decode()
        for line in output.splitlines():
            if "Data Units Read" in line:
                val = int(line.split(":")[1].split("(")[0].strip())
                stats["data_read_tb"] = val * 512000 / (1024**4)
            elif "Data Units Written" in line:
                val = int(line.split(":")[1].split("(")[0].strip())
                stats["data_written_tb"] = val * 512000 / (1024**4)
            elif "power_cycles" in line:
                stats["power_cycles"] = int(line.split(":")[1].strip())
            elif "power_on_hours" in line:
                stats["power_on_hours"] = int(line.split(":")[1].strip())
            elif "unsafe_shutdowns" in line:
                stats["unsafe_shutdowns"] = int(line.split(":")[1].strip())
            elif "percentage_used" in line:
                stats["percentage_used"] = int(line.split(":")[1].strip().replace("%",""))
            elif "media_errors" in line:
                stats["media_errors"] = int(line.split(":")[1].strip())
            elif "num_err_log_entries" in line:
                stats["error_log_entries"] = int(line.split(":")[1].strip())
            elif "Warning Temperature Time" in line:
                stats["warning_temp_time"] = int(line.split(":")[1].strip())
            elif "Critical Composite Temperature Time" in line:
                stats["critical_temp_time"] = int(line.split(":")[1].strip())
            elif "Thermal Management T1 Trans Count" in line:
                stats["thermal_t1_count"] = int(line.split(":")[1].strip())
            elif "Thermal Management T1 Total Time" in line:
                stats["thermal_t1_time"] = int(line.split(":")[1].strip())
            elif "controller_busy_time" in line:
                stats["controller_busy_time"] = int(line.split(":")[1].strip())
    except:
        pass
    return stats

def read_int_safe(path):
    """sysファイルシステムから整数値を安全に読み取る"""
    try:
        with open(path, "r") as f:
            return int(f.read().strip())
    except:
        return -1

def get_thermal_throttle():
    """CPUサーマルスロットリング発生回数を取得"""
    return {
        "core_throttle_count":
            read_int_safe("/sys/devices/system/cpu/cpu0/thermal_throttle/core_throttle_count"),
        "package_throttle_count":
            read_int_safe("/sys/devices/system/cpu/cpu0/thermal_throttle/package_throttle_count")
    }

def get_turbo_status(cpu_freq):
    """Turbo Boost有効状態と現在のBoost動作状態を取得"""
    turbo_disabled = read_int_safe(
        "/sys/devices/system/cpu/intel_pstate/no_turbo"
    )
    turbo_enabled = 0 if turbo_disabled == 1 else 1
    cpu_freq_obj = psutil.cpu_freq()
    max_freq = cpu_freq_obj.max if cpu_freq_obj else -1
    min_freq = cpu_freq_obj.min if cpu_freq_obj else -1
    is_boosting = 1 if cpu_freq > 1200 else 0   # 1.2GHz超えでBoost中と判定
    return {
        "turbo_enabled": turbo_enabled,
        "cpu_is_boosting": is_boosting,
        "cpu_max_mhz": max_freq,
        "cpu_min_mhz": min_freq
    }

def record_nucbox_status():
    hostname = socket.gethostname()

    # CPU
    cpu_percent = psutil.cpu_percent(interval=1)
    load1, load5, load15 = os.getloadavg()
    cpu_freq = psutil.cpu_freq().current
    cpu_temp = get_cpu_temp()

    # コア別使用率・周波数・温度
    per_cpu_percent = psutil.cpu_percent(percpu=True)
    per_cpu_freq = psutil.cpu_freq(percpu=True)
    core_temps = {}
    try:
        temps = psutil.sensors_temperatures()
        if "coretemp" in temps:
            for entry in temps["coretemp"]:
                if entry.label.startswith("Core"):
                    core_name = entry.label.lower().replace(" ", "")
                    core_temps[f"{core_name}_temp"] = entry.current
    except:
        pass
    core_data = {}
    for i, usage in enumerate(per_cpu_percent):
        core_data[f"cpu_core{i}_percent"] = usage
        if per_cpu_freq and i < len(per_cpu_freq):
            core_data[f"cpu_core{i}_mhz"] = per_cpu_freq[i].current

    # メモリ
    mem = psutil.virtual_memory()
    swap = psutil.swap_memory()

    # ディスク
    disk = psutil.disk_usage("/")
    io = psutil.disk_io_counters()

    # ネットワーク
    net = psutil.net_io_counters()

    # NVMe
    nvme_temps = get_nvme_temps()
    nvme_smart = get_nvme_smart()

    # サーマルスロットリング・Turbo Boost
    throttle = get_thermal_throttle()
    turbo = get_turbo_status(cpu_freq)

    data = {
        # CPU
        "cpu_percent": cpu_percent,
        "cpu_temp": cpu_temp,
        "cpu_freq_mhz": cpu_freq,
        "cpu_load1": load1,
        "cpu_load5": load5,
        "cpu_load15": load15,
        # メモリ
        "mem_percent": mem.percent,
        "mem_used_mb": mem.used / 1024 / 1024,
        "mem_available_mb": mem.available / 1024 / 1024,
        "mem_swap_used_mb": swap.used / 1024 / 1024,
        # ディスク
        "disk_percent": disk.percent,
        "disk_read_mb": io.read_bytes / 1024 / 1024,
        "disk_write_mb": io.write_bytes / 1024 / 1024,
        "disk_read_count": io.read_count,
        "disk_write_count": io.write_count,
        # ネットワーク
        "net_bytes_sent_mb": net.bytes_sent / 1024 / 1024,
        "net_bytes_recv_mb": net.bytes_recv / 1024 / 1024,
        "net_packets_sent": net.packets_sent,
        "net_packets_recv": net.packets_recv,
        # NVMe温度・SMART・サーマル・Turbo・コア別
        **nvme_temps,
        **nvme_smart,
        **throttle,
        **turbo,
        **core_data,
        **core_temps,
    }

    influx.insert_nucbox_system_log(hostname, data)

# ===== メインループ =====
influx = influxwriter.Writer()
success_log = smarthomelog.Log(__file__, smarthomelog.LogType.SUCCESS)

while True:
    record_nucbox_status()
    success_log.write('NUCBOX処理正常終了', __file__)
    time.sleep(5)

InfluxDB書き込みメソッド(insert_nucbox_system_log)

ホスト名をタグ、収集した全データをフィールドとしてInfluxDB v2に保存します。

def insert_nucbox_system_log(self, hostname: str, fields):
    fields['timestamp'] = datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
    measurement = 'nucbox_system_log'
    tags = {
        'hostname': hostname
    }
    try:
        self.conn.write_dict(measurement, tags, fields)
    except Exception as e:
        print(e)

コードの解説

sensorsコマンドのパース処理

output = subprocess.check_output(["sensors"]).decode()
for line in output.splitlines():
    if "Package id 0" in line:
        return float(line.split("+")[1].split("°")[0])

subprocess.check_output(["sensors"]) でコマンド出力を取得し、各行をスキャンします。
"Package id 0" 行の形式は Package id 0: +45.0°C ... なので、split("+")+ の後ろ、split("°")° の前の数字を取り出しています。

NVMe SMARTのData Units → TB変換

if "Data Units Read" in line:
    val = int(line.split(":")[1].split("(")[0].strip())
    stats["data_read_tb"] = val * 512000 / (1024**4)

nvme smart-logData Units は512バイト単位の1000倍(= 512,000バイト = 500KB)が1ユニットです。
これをTB(テラバイト)に変換するには × 512000 ÷ 1024⁴ で計算します。

コア別データの動的生成

for i, usage in enumerate(per_cpu_percent):
    core_data[f"cpu_core{i}_percent"] = usage
    if per_cpu_freq and i < len(per_cpu_freq):
        core_data[f"cpu_core{i}_mhz"] = per_cpu_freq[i].current

psutil.cpu_percent(percpu=True) はコア数分のリストを返します。enumerate でインデックスを取りながらループし、cpu_core0_percentcpu_core1_percent… というキーを動的に生成します。コア別周波数も同様の処理で取得します。

取得データ一覧(InfluxDB保存フィールド)

InfluxDB の nucbox_system_log measurement に以下の構造で保存されます。

  • タグ:hostname(NUCBOXのホスト名)
  • CPU系フィールド:cpu_percent、cpu_temp、cpu_freq_mhz、cpu_load1/5/15、cpu_core{i}_percent、cpu_core{i}_mhz、core{i}_temp
  • メモリ系フィールド:mem_percent、mem_used_mb、mem_available_mb、mem_swap_used_mb
  • ディスク系フィールド:disk_percent、disk_read_mb、disk_write_mb、disk_read_count、disk_write_count
  • ネットワーク系フィールド:net_bytes_sent_mb、net_bytes_recv_mb、net_packets_sent、net_packets_recv
  • NVMe温度フィールド:nvme_composite、nvme_sensor1、nvme_sensor2
  • NVMe SMARTフィールド:data_read_tb、data_written_tb、percentage_used、power_cycles、power_on_hours、unsafe_shutdowns、media_errors、error_log_entries、warning_temp_time、critical_temp_time、thermal_t1_count、thermal_t1_time、controller_busy_time
  • サーマル・Turboフィールド:core_throttle_count、package_throttle_count、turbo_enabled、cpu_is_boosting、cpu_max_mhz、cpu_min_mhz

systemdによる自動起動設定

[Unit]
Description=NUCBOX System Record to InfluxDB
After=network.target influxd.service

[Service]
ExecStart=/usr/bin/python3 /home/<username>/projects/smarthome/core/record/raspi/nucbox_system_record.py
Restart=always
RestartSec=10
User=root

[Install]
WantedBy=multi-user.target

nvme smart-log はroot権限が必要なため、User=root にしています。セキュリティ上の懸念がある場合は、sudoersで nvme コマンドのみパスワードなし実行を許可して User=<username> に変更することも可能です。

sudo cp nucbox_system_record.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable nucbox_system_record
sudo systemctl start nucbox_system_record

動作確認

直接実行で確認

sudo python3 nucbox_system_record.py

InfluxDB Data Explorerで確認

InfluxDB 管理画面(デフォルト:http://localhost:8086)のData Explorerで以下のFluxクエリを実行します。

from(bucket: "your-bucket")
  |> range(start: -1m)
  |> filter(fn: (r) => r._measurement == "nucbox_system_log")
  |> filter(fn: (r) => r._field == "cpu_percent" or r._field == "cpu_temp")

CPU使用率・CPU温度のデータが5秒ごとに記録されていれば正常動作しています。

NVMe SMART情報の確認

from(bucket: "your-bucket")
  |> range(start: -1m)
  |> filter(fn: (r) => r._measurement == "nucbox_system_log")
  |> filter(fn: (r) => r._field == "percentage_used" or r._field == "power_on_hours")

percentage_used(SSD寿命消費率)が -1 でなく正常な値が入っていれば、NVMe SMARTの取得も成功しています。

まとめ

本記事では、psutil・lm-sensors・NVMe SMARTの3つを組み合わせてNUCBOXのシステム情報を網羅的に収集し、InfluxDB v2へ5秒ごとに記録する方法を解説しました。

  • psutil だけでは取れないCPU温度・NVMe温度は lm-sensors(sensorsコマンド) で補完できる
  • NVMeの寿命・異常シャットダウン数・メディアエラーは nvme smart-log で定期チェックすることで寿命の予兆を早期発見できる
  • サーマルスロットリングとTurbo Boostの状態を /sysファイルシステム から監視することで、CPUが本当に能力を発揮できているかを確認できる
  • コア別データは 動的なキー生成 で対応することで、CPUコア数が変わっても修正不要

InfluxDBに蓄積したデータをGrafanaで可視化すれば、CPU温度の推移・NVMe寿命の傾向・スロットリング発生タイミングなどを一目で確認できるようになります。24時間稼働のサーバー運用に、ぜひ活用してみてください。

タイトルとURLをコピーしました