【高負荷プロセス検知】WMI パフォーマンスデータ(Win32_PerfFormattedData)を活用したリソース監視
レガシーシステムの呪縛から逃れられない現場において、Windowsのバックグラウンドで密かにリソースを食い潰す暴走プロセスの検知は、インフラエンジニアにとって永遠の課題だ。サードパーティ製の監視エージェントを導入できない閉域網や、追加コストが認められない環境において、WSH (Windows Script Host) と WMI (Windows Management Instrumentation) は、今なお最強かつ最後の砦として機能する。
今回は、VBScriptの限界とWMIの挙動特性を熟知した者だけが実装できる、高精度かつ省メモリな高負荷プロセス検知・自動特定ロジックの極限の知見を公開する。
—
1. なぜ `Win32_PerfFormattedData` なのか?
プロセス監視において、よく見かけるのが `Win32_Process` クラスのポーリングだ。しかし、これには致命的な欠陥がある。`Win32_Process` はCPU使用率の「累計時間」しか保持しておらず、CPU使用率を算出するためには、自前で一定時間間隔での差分計算(デルタ値の算出)を行わなければならない。スクリプト側で複雑な計算状態を維持するのは、メモリリークやパフォーマンス低下の温床となる。
一方、今回採用する `Win32_PerfFormattedData_PerfProc_Process`(あるいは `Win32_PerfFormattedData_RMCPerf_RDPSession` など)は、OSのパフォーマンスカウンタからすでに計算済みの整形されたデータ(Formatted Data)を直接取得できる。これにより、スクリプト側の演算負荷を極限まで削ぎ落とし、単一のクエリで「今、何パーセントCPUを消費しているか」を直撃で捉えることが可能になる。
—
2. 実装上の致命的罠:オブジェクトのライフサイクルとCOMの解放
VBScriptにおけるWMI操作の最大の罠は、COMコンポーネント(特に `WbemScripting.SWbemLocator` やクエリ結果のコレクション)のメモリ管理と接続リークである。
ガベージコレクション(JScriptの世代別GCとは異なり、VBScriptの背後にあるCOM参照カウント方式)は非常に気まぐれだ。特にWMIの動的コレクションをループ処理する際、明示的にオブジェクトを破棄 (`Set obj = Nothing`) しないと、WMIプロバイダ側のメモリ(WMI Provider Host: `WmiPrvSE.exe`)が肥大化し、監視スクリプト自体がサーバーのリソースを圧迫するという本末転倒な事態を引き起こす。
以下の実用コードでは、このライフサイクル管理を完全に制御し、長時間稼働(24時間365日のタスクスケジューラ実行など)に耐えうる堅牢な設計としている。
—
3. 高負荷プロセス検知・特定スクリプト(完全版)
以下のコードを `ProcessMonitor.vbs` として保存し、実行せよ。エラーハンドリング、ログ出力、そして厳格なメモリ解放のイディオムが網羅されている。
‘ ==============================================================================
‘ @file ProcessMonitor.vbs
‘ @brief WMI PerfFormattedDataを用いた高負荷プロセス検知・警告ロジック
‘ @author チーフアーキテクト
‘ ==============================================================================
Option Explicit
‘ — 定数定義 —
Const THRESHOLD_CPU = 80 ‘ CPU使用率の閾値(%)
Const THRESHOLD_WORKINGSET = 524288000 ‘ メモリ使用量の閾値(バイト: 例として約500MB)
Const LOG_PATH = “C:\Logs\ProcessMonitor.log”
‘ メイン処理の実行
Call Main()
Sub Main()
Dim objLocator, objService, colProcesses, objProcess
Dim wshShell, fso, logFile
Dim cpuUsage, workingSet, procName, procId
Dim alertFlag
alertFlag = False
On Error Resume Next
‘ 1. WMI接続の確立(ローカルマシン前提、リモート接続の場合は第一引数を指定)
Set objLocator = CreateObject(“WbemScripting.SWbemLocator”)
Set objService = objLocator.ConnectServer(“.”, “root\cimv2”)
If Err.Number <> 0 Then
Call WriteLog(“CRITICAL: WMIへの接続に失敗しましたエラーコード: ” & Hex(Err.Number))
Exit Sub
End If
‘ セキュリティとネームスペースの最適化(半権限・同期モード)
objService.Security_.ImpersonationLevel = 3 ‘ Impersonate
‘ 2. パフォーマンスカウンタからプロセス情報を一括取得
‘ ※ “_Total” や “Idle” プロセスを除外してクエリを発行するのが実務の定石
Set colProcesses = objService.ExecQuery( _
“SELECT Name, IDProcess, PercentProcessorTime, WorkingSet ” & _
“FROM Win32_PerfFormattedData_PerfProc_Process ” & _
“WHERE Name <> ‘_Total’ AND Name <> ‘Idle'”, , 48)
If Err.Number <> 0 Then
Call WriteLog(“ERROR: プロセス情報の取得クエリに失敗しましたエラーコード: ” & Hex(Err.Number))
CleanUp objService, objLocator, Nothing
Exit Sub
End If
On Error GoTo 0
‘ 3. コレクションの走査と閾値判定
For Each objProcess in colProcesses
‘ 型安全のためのキャストと安全な値の取得
cpuUsage = CDbl(objProcess.PercentProcessorTime)
workingSet = CDldSafe(objProcess.WorkingSet)
procName = CStr(objProcess.Name)
procId = CInt(objProcess.IDProcess)
‘ CPUまたはメモリの閾値を超過しているか判定
If cpuUsage >= THRESHOLD_CPU Or workingSet >= THRESHOLD_WORKINGSET Then
Dim alertMsg
alertMsg = “【警告】高負荷プロセスを検知 -> ” & _
“プロセス名: ” & procName & ” ” & _
“(PID: ” & procId & “) | ” & _
“CPU使用率: ” & cpuUsage & “% | ” & _
“メモリ(WorkingSet): ” & FormatBytes(workingSet)
Call WriteLog(alertMsg)
alertFlag = True
‘ 【拡張領域】ここでプロセス強制終了(Terminate)や
‘ イベントログへの書き込み(Wscript.Shell使用)をフック可能
‘ Call ForceTerminateProcess(procId)
End If
Next
‘ 4. 厳格なオブジェクト解放(メモリリークの完全防止)
‘ コレクション -> サービス -> ロケータの順に逆順でNothingを代入
Set colProcesses = Nothing
Set objService = Nothing
Set objLocator = Nothing
If alertFlag Then
‘ 必要に応じて管理者へのメール通知やイベントログ出力連携
End If
End Sub
‘ ==============================================================================
‘ 補助関数群
‘ ==============================================================================
‘ ログ出力関数
Sub WriteLog(ByVal message)
Dim fso, ts
On Error Resume Next
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ ログフォルダの存在確認・作成
Dim logDir
logDir = fso.GetParentFolderName(LOG_PATH)
If Not fso.FolderExists(logDir) Then
fso.CreateFolder(logDir)
End If
‘ 追記モードでオープン (ForAppending = 8, Create = True)
Set ts = fso.OpenTextFile(LOG_PATH, 8, True)
ts.WriteLine “[” & Now & “] ” & message
ts.Close
Set ts = Nothing
Set fso = Nothing
End Sub
‘ バイト数を見やすい単位に変換するフォーマッタ
Function FormatBytes(ByVal bytes)
Dim kb, mb, gb
kb = 1024
mb = kb 1024
gb = mb 1024
If bytes >= gb Then
FormatBytes = FormatNumber(bytes / gb, 2) & ” GB”
ElseIf bytes >= mb Then
FormatBytes = FormatNumber(bytes / mb, 2) & ” MB”
ElseIf bytes >= kb Then
FormatBytes = FormatNumber(bytes / kb, 2) & ” KB”
Else
FormatBytes = bytes & ” Bytes”
End If
End Function
‘ 64bit整数などのオーバーフローを防ぐための安全なキャスト
Function CDldSafe(ByVal val)
On Error Resume Next
CDldSafe = CDbl(val)
If Err.Number <> 0 Then
CDldSafe = 0
End If
On Error GoTo 0
End Function
‘ クリーンアップ用ヘルパー
Sub CleanUp(ByRef svc, ByRef loc, ByRef col)
On Error Resume Next
If Not col Is Nothing Then Set col = Nothing
If Not svc Is Nothing Then Set svc = Nothing
If Not loc Is Nothing Then Set loc = Nothing
On Error GoTo 0
End Sub
—
4. チーフアーキテクトが教える現場の勘所とチューニング
1. WMIクエリのフラグ指定 (`48`)
`ExecQuery` の第3引数に渡している `48` (`wbemFlagForwardOnly + wbemFlagReturnImmediately`) は極めて重要だ。これにより、結果セットをメモリ上にキャッシュせず、フォワードオンリー(前進のみ)のストリームとして処理するため、大量のプロセスが稼働している環境でもメモリ消費量を最小限に抑えられる。
2. 多重起動の防止(Single Instance Pattern)
監視スクリプトが何らかの原因(無限ループや重いI/O待ち)でタスクスケジューラの次回実行時まで終了しなかった場合、スクリプトが多重起動してリソースを食い潰す。実運用では、WMIの `Win32_Process` を使って自スクリプト(`cscript.exe ProcessMonitor.vbs`)の実行プロセス数をカウントし、すでに走っていれば即座に終了するガードロジックを冒頭に組み込むべきだ。
3. レガシー環境における文字コードの罠
`FileSystemObject` でテキストファイルを出力する際、デフォルトではANSI(Shift_JIS)になる。現代の統合監視基盤やUTF-8前提のログ解析ツールに流し込む場合は、ADODB.Stream等を利用してUTF-8(BOM付き/無し)で書き込むようにラッパー関数を拡張することを推奨する。
—
総括
VBScriptは「古い言語」として片付けられがちだが、OSの深部(WMIやCOM)に直接かつ軽量にアクセスできるという点において、現代の高水準言語(C#やPowerShell)の初期化コストや実行オーバーヘッドを凌駕するアドバンテージを持っている。
メモリのライフサイクルを完全に掌握し、WMIの挙動特性を理解したこのスクリプトは、あなたのインフラストラクチャを静かに、そして確実に守る強靭な盾となるだろう。コードの海を支配するのは、いつだって細部へのこだわりを持つ者だけだ。
