2026年7月9日

2026年7月9日

Pythonでサーバーのディスク容量を監視する方法|全マウントの使用率・inode・満杯までの残り時間

サーバーのディスク容量を Python で監視するなら、標準ライブラリの shutil.disk_usage() と os.statvfs() で全マウントポイントの使用率と inode を確認し、前回までの記録から「このペースだとあと何時間で満杯になるか」まで出すと、手遅れになる前に気づけます。追加のライブラリは不要です。この記事では、df と数字を一致させる計算方法、監視対象のマウントの選び方、増加ペースの推定、通知、そして警告が出たときに容量を食っているものを探す手順までを説明します。CPU やメモリも含めた汎用の監視ではなく、ディスクに絞った内容です。

ディスク監視で見るべき3つの値

ディスクが原因の障害は「使用率が上限に達した」だけではありません。監視では次の3つを分けて見ます。

  • 使用率:一般ユーザーが使える空きがどれだけ残っているか。MySQL やログの書き込みが止まる直接の原因になります。
  • inode の使用率:ファイルの数の上限です。小さなセッションファイルやキャッシュが大量にたまると、容量に余裕があっても「No space left on device」で書き込めなくなります。
  • 増加ペース:使用率 70% でも1時間に 5% ずつ増えていれば緊急、90% でも1か月変化がなければ計画的に対処すれば足ります。閾値だけでは、この違いがわかりません。

shutil.disk_usage の数字が df と合わない理由

まず標準ライブラリで値を取ってみます。

import shutil

du = shutil.disk_usage("/")
print(du.total, du.used, du.free)  # バイト単位
print(du.used / du.total * 100)    # df の Use% と少しずれる

used / total で計算すると、df -h の「Use%」より小さな値になります。ext4 などは既定でブロックの一部(多くの環境で 5%)を root 用に予約しており、free はこの予約分を除いた「一般ユーザーが使える空き」だからです。つまり used + free は total より小さくなります。

df と同じ考え方で計算するなら、分母を used + free にします。

percent = du.used / (du.used + du.free) * 100

この式なら、一般ユーザーが使える空きが 0 になった時点が「満杯」と一致します。Web サーバーや PHP は root 以外のユーザーで動くことが多いので、この基準のほうが実際の障害に近い数字です。

inode は os.statvfs() で取ります。f_files が inode の総数、f_ffree が空きの数です。btrfs のように inode を固定で持たないファイルシステムでは f_files が 0 になることがあるので、0 で割らないよう分岐しておきます。

import os

st = os.statvfs("/")
if st.f_files:
    print((st.f_files - st.f_ffree) / st.f_files * 100)

監視対象のマウントポイントを選ぶ

/ だけを見ていると、/var や /home を別パーティションにしている構成で見落とします。Linux では /proc/self/mounts に現在のマウントが並んでいるので、ここから実ディスクのファイルシステムだけを拾います。

tmpfs・overlay(Docker のコンテナ層)・squashfs(snap)などは、容量の意味が違うか、常に使用率いっぱいと表示されるため除外します。同じデバイスがバインドマウントで複数の場所に出てくることもあるので、デバイス名で重複を除きます。なお、このファイルではマウント先のパスに含まれる空白が \040 という表記に置き換わっています。

監視スクリプトの例

使用率・inode・増加ペースをまとめて確認し、問題があれば Webhook に通知するスクリプトです。履歴は JSON ファイルに保存し、直近7日分だけを残します。

#!/usr/bin/env python3
"""全マウントのディスク使用率・inode・満杯までの時間を確認して警告する"""
import json
import os
import shutil
import sys
import time
import urllib.request
from pathlib import Path

FS_TYPES = {"ext4", "ext3", "xfs", "btrfs"}
WARN_PERCENT = 85.0        # 使用率(df 基準)の閾値
WARN_INODE_PERCENT = 85.0  # inode 使用率の閾値
WARN_HOURS_LEFT = 72.0     # 満杯まで残りこの時間を切ったら警告
MIN_SPAN = 6 * 3600        # 増加ペースは6時間以上前の記録と比べて出す
KEEP = 7 * 24 * 3600       # 履歴の保存期間
HISTORY = Path("/var/lib/disk-monitor/history.json")
WEBHOOK = os.environ.get("DISK_MONITOR_WEBHOOK", "")


def mountpoints():
    seen = set()
    with open("/proc/self/mounts", encoding="utf-8") as f:
        for line in f:
            dev, mnt, fstype = line.split()[:3]
            if fstype in FS_TYPES and dev not in seen:
                seen.add(dev)
                yield mnt.replace("\\040", " ")


def measure(mnt):
    du = shutil.disk_usage(mnt)
    usable = du.used + du.free
    st = os.statvfs(mnt)
    return {
        "percent": du.used / usable * 100 if usable else 0.0,
        "free": du.free,
        "used": du.used,
        "inode": (st.f_files - st.f_ffree) / st.f_files * 100 if st.f_files else None,
    }


def hours_left(samples, now, used, free):
    old = [s for s in samples if now - s[0] >= MIN_SPAN]
    if not old:
        return None
    t0, used0 = old[-1]  # 6時間以上前の記録のうち、いちばん新しいもの
    rate = (used - used0) / ((now - t0) / 3600)  # 1時間あたりの増加バイト数
    return free / rate if rate > 0 else None


def notify(text):
    if not WEBHOOK:
        return
    body = json.dumps({"text": text}).encode("utf-8")
    req = urllib.request.Request(WEBHOOK, data=body, headers={"Content-Type": "application/json"})
    urllib.request.urlopen(req, timeout=10)


def main():
    now = time.time()
    try:
        history = json.loads(HISTORY.read_text(encoding="utf-8"))
    except (FileNotFoundError, ValueError):
        history = {}

    warnings = []
    for mnt in mountpoints():
        m = measure(mnt)
        samples = [s for s in history.get(mnt, []) if now - s[0] <= KEEP]
        left = hours_left(samples, now, m["used"], m["free"])
        samples.append([now, m["used"]])
        history[mnt] = samples

        line = f"{mnt}: 使用率 {m['percent']:.1f}% / 空き {m['free'] / 1024**3:.1f} GiB"
        if m["inode"] is not None:
            line += f" / inode {m['inode']:.1f}%"
        if left is not None:
            line += f" / 満杯まで約 {left:.0f} 時間"
        print(line)

        if m["percent"] >= WARN_PERCENT:
            warnings.append(f"[容量] {line}")
        if m["inode"] is not None and m["inode"] >= WARN_INODE_PERCENT:
            warnings.append(f"[inode] {line}")
        if left is not None and left <= WARN_HOURS_LEFT:
            warnings.append(f"[増加] {line}")

    HISTORY.parent.mkdir(parents=True, exist_ok=True)
    tmp = HISTORY.with_suffix(".tmp")
    tmp.write_text(json.dumps(history), encoding="utf-8")
    os.replace(tmp, HISTORY)  # 書き込み途中で止まっても履歴が壊れないように置き換える

    if warnings:
        notify("ディスク警告 " + os.uname().nodename + "\n" + "\n".join(warnings))
        sys.exit(1)


if __name__ == "__main__":
    main()

いくつか補足します。

  • 増加ペースの出し方:6時間以上前の記録と今の値を比べ、1時間あたりの増加量で空きを割っています。直近数分の比較にすると、バックアップの一時ファイルのような一瞬の増減で数字が大きくぶれるためです。逆に、毎晩のバックアップで大きく増えて朝に消える、という使い方のサーバーでは、比較の間隔を 24 時間にしたほうが実態に近くなります。
  • 履歴の書き込み:ディスクが満杯に近いときは、履歴ファイルの書き込み自体が失敗する可能性があります。一時ファイルに書いてから os.replace() で置き換えているので、途中で失敗しても前回の履歴は残ります。
  • 通知:{"text": ...} という JSON は Slack の Incoming Webhook などで使える形です。送り先のサービスによって形式が違うので、ドキュメントを確認して合わせてください。Webhook の URL は秘密情報なので、スクリプトに書かずに環境変数で渡しています。
  • 閾値:85% は目安です。「警告を受けてから片付けるまでに必要な余裕」から逆算して決めます。容量の大きいディスクほど、同じ 15% でも残りの GiB が大きいので、割合ではなく空き容量(GiB)で判定するのも一つの方法です。

このスクリプトは、問題が無いときも警告が続いているときも、実行のたびに同じ判定をします。毎回通知されるとうるさい場合は、前回の警告内容を履歴と一緒に保存し、変わったときだけ通知するようにします。

定期実行の設定

数分〜15分おきに実行すれば十分です。cron なら次のように書きます。履歴ファイルを /var/lib に置いているので root の crontab で動かす例ですが、専用ユーザーで動かす場合は書き込める場所に HISTORY を変えてください。

*/10 * * * * DISK_MONITOR_WEBHOOK=https://hooks.example.com/your-webhook-id /usr/bin/python3 /opt/disk-monitor/disk_monitor.py >> /var/log/disk-monitor.log 2>&1

設定したら、一度だけ WARN_PERCENT を 1.0 などに下げて手動で実行し、通知が届くことを確かめてから元に戻します。本当にディスクを埋めて試すのは避けてください。

警告が出たら、何が容量を使っているかを探す

通知を受けたら、まず増えている場所を特定します。-x を付けると、他のファイルシステムに入り込まずにそのパーティションの中だけを数えます。

sudo du -xh --max-depth=1 /var | sort -h | tail -n 15

よくある原因と確認方法は次のとおりです。

  • ログ:/var/log のアクセスログやエラーログ、journalctl --disk-usage で見える systemd のジャーナル。ローテーションの設定漏れが多い原因です。
  • WordPress まわり:wp-content/debug.log の肥大化、バックアッププラグインが作ったアーカイブ、キャッシュプラグインのキャッシュディレクトリ。
  • データベース:MySQL のバイナリログや、大きくなったテーブル。データベース側で確認します。
  • Docker:docker system df で、使われていないイメージやボリュームの量を確認できます。
  • 削除したのに空かない:ログファイルを rm しても、プロセスが開いたままだと容量は解放されません。sudo lsof +L1 で「削除済みだが開かれているファイル」を一覧できます。該当サービスを再起動すると解放されます。

inode の警告の場合は、容量ではなくファイルの数を数えます。sudo du --inodes -x --max-depth=1 /var | sort -n | tail のように、ファイル数の多いディレクトリを探してください(--inodes は GNU coreutils の比較的新しい版のオプションです)。PHP のセッションファイルやキャッシュの小さなファイルが原因になりやすいです。

何かを削除する前には、それが本当に不要かを確認し、データベースやサイトのファイルであればバックアップを取ってから作業してください。

自作スクリプトと監視ツールの使い分け

このスクリプトは、サーバーが数台で、まずディスクだけでも見張りたい場合に向いています。台数が増えたり、グラフで推移を見たり、監視スクリプト自身が止まったことに気づいたりする必要が出てきたら、Zabbix や Prometheus(node_exporter)、Netdata などの監視ツールを検討してください。どのツールも機能や設定方法は更新で変わるので、導入時は公式ドキュメントを確認します(2026年10月現在)。

自作の場合でも、「スクリプトが動いていること」の確認は別に用意しておくと安心です。実行のたびに外部の死活監視サービスへ ping を送り、届かなくなったら通知される仕組みがよく使われます。

まとめ

  • ディスクは「使用率」「inode」「増加ペース」の3つを分けて監視する
  • shutil.disk_usage() の使用率は used / (used + free) で計算すると df と一致し、root 予約分を除いた実際の空きに基づく
  • /proc/self/mounts から実ディスクのマウントだけを選び、/ 以外のパーティションも見る
  • 6時間以上前の記録と比べて増加ペースを出し、「満杯まで残り何時間」で警告すると、閾値だけより早く気づける
  • 警告が出たら du -x、journalctl --disk-usage、lsof +L1 などで原因を特定し、削除の前にバックアップを取る

お気軽にご相談ください

お見積りへ お問い合わせへ
▲