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

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

ポーリングからの脱却:WMI Event Subscriptionによる超低負荷・リアルタイム・プロセス監視アーキテクチャ

シニアエンジニア、あるいは長年レガシーなWindows環境の保守に携わってきた諸君なら、一度は「監視スクリプトのジレンマ」に直面したことがあるはずだ。

「CPU使用率を監視したい。しかし、`Do While True` ループの中で `Sleep` を挟むような、稚拙なポーリング処理を実装したくない。」

ポーリングは、リソースの搾取だ。監視対象のプロセスが静止している間も、スクリプトは無意味なウェイトとコンテキストスイッチを繰り返し、システムに微小な、しかし確実に蓄積されるノイズを撒き散らす。真のアーキテクトが求めるのは、「変化が起きた時だけ、システムがスクリプトを叩き起こす」というイベント駆動型(Event-Driven)の設計である。

本稿では、WMI(Windows Management Instrumentation)の深淵、`__InstanceModificationEvent` を用いた、極限まで低負荷なプロセス監視の実装手法を伝授する。

1. ポーリングの罪と、WMIイベントの真髄

一般的な監視スクリプトは、一定間隔でスナップショットを取得し、前回の値と比較する。これを「ポーリング」と呼ぶ。しかし、この手法には二つの致命的な欠陥がある。

1. サンプリング間隔のトレードオフ: 間隔を短くすればリアルタイム性が増すが、CPU負荷が高まる。間隔を長くすれば負荷は下がるが、スパイク(突発的な負荷)を見逃す。
2. リソースの浪費: 状態に変化がない場合でも、スクリプトは「確認」という名の無駄な計算資源を消費し続ける。

これに対し、WMIの Event Subscription 機構は、OSのカーネルおよびWMIリポジトリが状態の変化を検知した瞬間に、クエリに合致するイベントを通知する。スクリプト側は、`ExecNotificationQuery` を通じて「イベントが届くまで待機(Block)」状態に入る。この待機状態は、OSレベルで効率的に管理されており、スクリプトがCPUサイクルを消費することはない。

2. クラス選定の勘所:`Win32_PerfFormattedData` を選ぶ理由

WMIでプロセスを監視する際、多くの者が `Win32_Process` を選択する。しかし、それは初歩的なミスだ。`Win32_Process` はプロセスの生存確認には適しているが、CPU使用率という動的なパフォーマンス統計を保持していない。

我々が狙うべきは、`Win32_PerfFormattedData_PerfProc_Process` クラスである。

ここで重要なのは “Formatted” という単語だ。WMIには `Win32_PerfRawData_…` という生データを提供するクラスも存在するが、これを用いると、エンジニアは「前回の値との差分」を自前で計算し、時間軸で割るという、数学的かつ実装コストの高い処理を強行しなければならない。`Formatted` クラスは、OS側ですでに計算済みの「%」を返してくれる。我々は、その恩恵を享受するだけでいい。

3. 極限の監視スクリプト:実装例

以下に、特定のプロセス(例:`chrome`)のCPU使用率が閾値を超えた瞬間に警告を発する、プロフェッショナル仕様のVBScriptを示す。

' =============================================================================
' Title: WMI Event-Driven CPU Monitor (Professional Edition)
' Description: __InstanceModificationEvent を利用した低負荷なプロセス監視
' Author: Legendary Chief Architect
' =============================================================================
Option Explicit

' --- 設定領域 ---
Const TARGET_PROCESS_NAME = "chrome"  ' 監視対象プロセス名 (拡張子なし)
Const CPU_THRESHOLD       = 80.0      ' 警告を出すCPU使用率(%)
Const WMI_INTERVAL        = 1         ' WMIエンジンによるサンプリング間隔(秒)
' ----------------

Dim objWMIService, objEvents, objEvent, objTargetInstance
Dim strQuery, strProcessName

' WMIクエリの構築
' __InstanceModificationEvent WITHIN n は、WMIがn秒ごとにインスタンスの変化をチェックすることを意味する。
' これはスクリプトのループではなく、WMIサービス内部のスケジューリングであるため、極めて効率的である。
strQuery = "SELECT * FROM __InstanceModificationEvent WITHIN " & WMI_INTERVAL & _
           " WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfProc_Process' " & _
           " AND TargetInstance.Name = '" & TARGET_PROCESS_NAME & "'"

On Error Resume Next

' 1. WMIサービスへの接続
' GetObject よりも SWbemLocator を用いる方が、明示的な接続管理の観点から望ましい
Dim objLocator
Set objLocator = CreateObject("WbemScripting.SWbemLocator")
Set objWMIService = objLocator.ConnectServer(".", "root\cimv2")

If Err.Number <> 0 Then
    WScript.Echo "CRITICAL ERROR: WMI接続に失敗しました。: " & Err.Description
    WScript.Quit
End If
On Error GoTo 0

WScript.Echo "Monitoring started for process: " & TARGET_PROCESS_NAME & " (Threshold: " & CPU_THRESHOLD & "%)"
WScript.Echo "Press Ctrl+C to terminate."

' 2. 通知クエリの実行
' ExecNotificationQuery は、イベントが届くまでスクリプトの実行をブロックする。
' この間、CPU使用率はほぼゼロである。
Set objEvents = objWMIService.ExecNotificationQuery(strQuery)

' 3. イベントループ
' ここでのループは「イベントが届いた時」にのみ回転する。
Do
    ' 次のイベントが来るまでここで待機 (Blocking Call)
    Set objEvent = objEvents.NextAll

    ' イベントからターゲットインスタンス(プロセスの実体)を抽出
    Set objTargetInstance = objEvent.TargetInstance

    ' CPU使用率の検証
    ' Win32_PerfFormattedData_PerfProc_Process の PercentProcessorTime を参照
    If objTargetInstance.PercentProcessorTime >= CPU_THRESHOLD Then
        Call TriggerAlert(objTargetInstance.Name, objTargetInstance.PercentProcessorTime)
    End If

    ' --- メモリ管理の極意 ---
    ' 長時間稼働するスクリプトにおいて、オブジェクトの解放を怠ることはメモリリークを意味する。
    ' 参照を明示的に切り離し、ガベージコレクションの対象とする。
    Set objTargetInstance = Nothing
    Set objEvent = Nothing
Loop

' =============================================================================
' サブプロシージャ: 警告処理
' =============================================================================
Sub TriggerAlert(procName, cpuVal)
    Dim strMsg
    strMsg = "[ALERT] " & Now & " | Process: " & procName & " | CPU Usage: " & cpuVal & "%"
    
    ' 標準出力への書き出し
    WScript.Echo strMsg
    
    ' ここにシステム連携(メール送信、イベントログ書き込み、外部API呼び出し等)を実装する
    ' 例: Call SendEmailAlert(strMsg)
End Sub

4. アーキテクトの視点:運用における極意

このスクリプトを現場に投入する際、シニアエンジニアとして以下の3点に留意せよ。

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

VBScriptにおける `Set obj = Nothing` は、単なる作法ではない。WSH(Windows Script Host)は、プロセスが終了するまでメモリを確保し続ける性質がある。特に `ExecNotificationQuery` で生成されるイベントオブジェクトは、ループ内で適切に解放しなければ、監視を続けているうちにメモリを食いつぶし、システム全体のパフォーマンスを低下させる「監視スクリプトによるDoS攻撃」を自ら引き起こすことになりかねない。

II. `WITHIN` 句の重み

クエリ内の `WITHIN 1` という値は、WMIがプロバイダーをポーリングする間隔だ。これを `0` に近づけすぎると、WMIサービス自体の負荷が跳ね上がる。監視の目的(リアルタイム性)と、システムの安定性のバランスを、エンジニアの直感ではなく、データの特性に基づいて決定せよ。

III. 権限とセキュリティコンテキスト

WMIへのアクセスには、適切な権限が必要だ。サービスとして実行する場合、`LocalSystem` アカウントでの実行を検討することになるが、その場合は、スクリプトが引き起こすアクション(ファイル操作やネットワーク通信)が、システム全体に壊滅的な影響を与えないよう、厳格なバリデーションを実装しなければならない。

結論

WMIイベント監視は、Windows管理における「静」と「動」を繋ぐ高度な技術だ。ポーリングという安易な道を選ばず、イベント駆動のアーキテクチャを構築すること。それこそが、レガシー環境においても、モダンなシステムにおいても、信頼されるエンジニアとしての証である。

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