【実務・中級編】【WMI イベント監視】__InstanceModificationEvent を用いた特定プロセスの CPU 使用率異常検知と自動警告システム – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

ポーリングという甘えを捨てろ。WMIイベント駆動による低負荷・高精度なプロセス監視設計論

業務自動化の現場において、多くのエンジニアが陥る「初歩的かつ致命的な設計ミス」がある。それは、「ループ(ポーリング)による監視」だ。

「1秒ごとにCPU使用率をチェックして、しきい値を超えたら警告を出す」
一見、シンプルで動作も分かりやすい。しかし、その設計はプロフェッショナルとは呼べない。なぜなら、監視対象が増えるほど、監視スクリプト自身がCPUリソースを食いつぶし、システム全体のパフォーマンスを毀損する「監視のためのオーバーヘッド」を生み出すからだ。

我々アーキテクトが目指すべきは、システムが「静か」な時は一切の計算資源を消費せず、異常が発生した「瞬間」にのみ、OSから通知を受け取るイベント駆動型(Event-Driven)の設計である。

本稿では、WMI(Windows Management Instrumentation)の真髄である`__InstanceModificationEvent`を使い、特定プロセスのCPU使用率異常を検知する、極めて堅牢かつ低負荷な監視システムの構築手法を伝授する。

1. なぜ「Do While True + WScript.Sleep」は非効率なのか

まず、現場でよく見かける「非効率なコード」の構造を批判的に分析しておこう。

' --- アンチパターン:ポーリング方式 ---
Do While True
    Set objProcess = GetObject("winmgmts:").ExecQuery("SELECT * FROM Win32_PerfFormattedData_PerfProc_Process WHERE Name = 'chrome'")
    ' CPU使用率の判定ロジック...
    WScript.Sleep 1000 ' 1秒待機
Loop

このコードには、以下の3つの欠陥がある。

1. リソースの浪費: `WScript.Sleep`で休止していても、WMIクエリの発行自体が重い。特にプロセスの数が増えると、クエリ発行のコストが無視できなくなる。
2. 検知の遅延と精度のトレードオフ: 待機時間を長くすれば負荷は下がるが、異常検知のレイテンシ(遅延)が増える。逆に短くすれば、監視スクリプトがCPUを占有する。
3. 決定論的な挙動の欠如: 「いつ検知するか」がスクリプトのループタイミングに依存しており、OSのイベント発生タイミングと同期していない。

これに対し、WMIのイベントサブスクリプションを利用すれば、OSのカーネルレベルに近い場所で状態変化を監視し、変化があった時のみスクリプトに制御が戻る。これこそが、低負荷・リアルタイム監視の正解だ。

2. WMI イベントサブスクリプションのメカニズム

我々が利用するのは、WMIのシステムクラスである`__InstanceModificationEvent`だ。これは、特定のWMIインスタンスの値が変更された瞬間に発行されるイベントである。

ここで重要なのが、WQL(WMI Query Language)における`WITHIN`句の使い方だ。

SELECT * FROM __InstanceModificationEvent WITHIN 1 
WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfProc_Process' 
AND TargetInstance.Name = 'chrome'

この`WITHIN 1`は、「WMIエンジンが1秒間隔でインスタンスの状態をチェックし、変化があればイベントを投げる」という指示だ。VBScript側でループを回すのではなく、WMIサービスというバックグラウンドプロセスに監視を委託する。これにより、VBScriptはイベントが届くまで完全に「待機(Blocked)」状態となり、CPU使用率は実質ゼロになる。

3. プロダクション・クオリティの監視スクリプト

以下に、実務でそのまま利用可能な、堅牢性を担保した監視スクリプトの設計例を示す。単なる検知だけでなく、ログ出力、エラーハンドリング、およびオブジェクトのライフサイクル管理を徹底している。

' ==============================================================================
' System Monitor: CPU Anomaly Detector (Event-Driven Version)
' Author: Chief Architect
' Description: WMI Event Subscription を用いた低負荷プロセス監視
' ==============================================================================
Option Explicit

' --- 設定セクション ---
Const TARGET_PROCESS_NAME = "chrome"   ' 監視対象プロセス名 (拡張子なし)
Const CPU_THRESHOLD       = 80         ' 警告を出すCPU使用率 (%)
Const LOG_FILE             = "C:\Logs\ProcessMonitor.log" ' ログ出力先

' --- グローバル変数 ---
Dim objWMIService, colMonitoredEvents, objEvent

' --- メイン処理 ---
Main

Sub Main()
    On Error Resume Next
    
    ' 1. WMIサービスへの接続
    Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
    If Err.Number <> 0 Then
        LogError "WMI接続失敗: " & Err.Description
        Exit Sub
    End If
    On Error GoTo 0

    LogInfo "--- 監視プロセス開始: " & TARGET_PROCESS_NAME & " (閾値: " & CPU_THRESHOLD & "%) ---"

    ' 2. WQLによるイベントクエリの構築
    ' __InstanceModificationEvent を使用し、Win32_PerfFormattedData_PerfProc_Process の変化を捕捉
    ' WITHIN 1 はWMIエンジン側でのチェック間隔。スクリプトの負荷を最小化する。
    Dim strQuery
    strQuery = "SELECT * FROM __InstanceModificationEvent WITHIN 1 " & _
               "WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfProc_Process' " & _
               "AND TargetInstance.Name = '" & TARGET_PROCESS_NAME & "'"

    ' 3. イベント通知の実行 (ここがイベント駆動の核)
    ' ExecNotificationQuery はイベントが発生するまでスクリプトの実行をブロックする
    Set colMonitoredEvents = objWMIService.ExecNotificationQuery("WQL", strQuery)

    ' 4. イベントループ
    ' このループは「イベントが届くまで待機」するため、CPU負荷は極めて低い
    Do
        Set objEvent = colMonitoredEvents.NextAllOC() ' 次のイベントを待機
        
        If Not objEvent Is Nothing Then
            CheckThreshold objEvent.TargetInstance
        End If
        
        ' オブジェクトの解放(メモリリーク防止)
        Set objEvent = Nothing
    Loop

    ' 終了処理 (通常はCtrl+C等で中断される)
    Cleanup
End Sub

' --- 閾値判定ロジック ---
Sub CheckThreshold(objTarget)
    Dim currentCPU
    currentCPU = objTarget.PercentProcessorTime

    ' CPU使用率が閾値を超えた場合のみ警告
    If currentCPU >= CPU_THRESHOLD Then
        LogWarning "【異常検知】プロセス: " & TARGET_PROCESS_NAME & " | CPU使用率: " & currentCPU & "%"
        ' ここにメール送信やプロセス強制終了などのアクションを実装可能
    End If
End Sub

' --- ロギング・ユーティリティ ---
Sub LogInfo(msg)
    WriteLog "INFO", msg
End Sub

Sub LogWarning(msg)
    WriteLog "WARN", msg
End Sub

Sub LogError(msg)
    WriteLog "ERROR", msg
End Sub

Sub WriteLog(level, msg)
    Dim fso, objFile, strTimestamp
    strTimestamp = Now()
    
    On Error Resume Next
    Set fso = CreateObject("Scripting.FileSystemObject")
    ' フォルダが存在しない場合は作成する設計にすべきだが、ここでは簡略化
    Set objFile = fso.OpenTextFile(LOG_FILE, 8, True) ' 8 = ForAppending
    
    If Err.Number = 0 Then
        objFile.WriteLine "[" & strTimestamp & "] [" & level & "] " & msg
        objFile.Close
    Else
        ' ログ出力自体が失敗した場合のフォールバック
        WScript.Echo "Critical Error: Cannot write to log file. " & msg
    End If
    On Error GoTo 0
End Sub

Sub Cleanup()
    Set colMonitoredEvents = Nothing
    Set objWMIService = Nothing
    LogInfo "--- 監視プロセス終了 ---"
End Sub

4. 実務における設計の勘所(Architect’s Notes)

このスクリプトをプロダクション環境に投入する際、以下の3点に留意せよ。

① オブジェクトのライフサイクル管理

VBScriptにおけるメモリ管理は、エンジニアの責任だ。`colMonitoredEvents.NextAllOC()` で取得したイベントオブジェクトは、ループの各ステップで必ず `Set objEvent = Nothing` により明示的に解放せよ。これを怠れば、長時間稼働させる監視ツールは、いつの間にかメモリを食いつぶす「ゾンビ・プロセス」へと成り下がる。

② 権限(Privilege)の壁

WMIのパフォーマンス・カウンター(`Win32_PerfFormattedData_*`)へのアクセスには、管理者権限が必要な場合が多い。実行ユーザーが適切な権限(Local Admin もしくは WMI Namespace のアクセス許可)を持っているか、設計段階で検証しておくこと。

③ ログの永続性と外部連携

実務では、ログをローカルファイルに書くだけでは不十分だ。

  • ファイルサイズ制限: ログファイルが肥大化してディスクを圧迫しないよう、ローテーション処理を組み込むか、ログ転送エージェント(SplunkやCloudWatch Agent等)に渡す設計にせよ。
  • データベース連携: 大規模な監視を行う場合は、ログをテキストではなく、SQLiteやSQL Server等のDBへ直接インサートする設計に切り替え、分析の容易性を確保せよ。

まとめ

「ループで回せば動く」というのは、実装の第一歩に過ぎない。
真にプロフェッショナルなエンジニアは、「いかにシステムに負荷をかけずに、必要な情報を、必要な瞬間に、確実に手に入れるか」という観点でアーキテクチャを設計する。

WMIのイベント駆動モデルをマスターすることは、Windows環境における自動化の領域を、単なる「スクリプト」から「堅牢なシステム」へと昇華させる鍵となるだろう。

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