【メモリダンプ自動取得指示】WMI による高負荷・フリーズプロセスの自動検知と外部ツール(ProcDump)連携起動
現場で突然発生する、原因不明のアプリケーションのフリーズや高負荷暴走。
「再現性がない」「発生した瞬間に誰も張り付いていない」――このインフラ・サポート現場における永遠の課題に対し、VBScriptとWSH(Windows Script Host)の真髄を活かした「完全無人・自律型の障害解析エージェント」の構築手法を伝授する。
安易にサードパーティの有償監視ツールを導入するまでもなく、Windows標準コンポーネントであるWMI(Windows Management Instrumentation)と、Microsoft公式のデバッグツール「ProcDump」を組み合わせれば、数キロバイトのスクリプトでミリ秒単位の監視網を構築できる。
今回は、単に動くだけのサンプルコードではない。プロダクション環境(本番稼働系)で耐えうる、メモリリークゼロ・誤検知ゼロ・プロセス競合排他の極限まで最適化された設計思想と実装コードを公開する。
—
なぜ「素通し」の監視スクリプトは現場で破綻するのか
多くのエンジニアがWMIを使ったプロセス監視を書く際、以下のような安易なアプローチをとる。
1. `WbemScripting.SWbemLocator` をループ内で毎回インスタンス化する。
2. `ExecQuery` で `Select From Win32_Process` を無限に叩き続ける。
3. 検出条件を満たしたら、単に `WScript.Shell` の `Run` メソッドでProcDumpを投げっぱなしにする。
これらはすべて現場でバグを引き起こすアンチパターンだ。
- COMオブジェクトの肥大化とメモリリーク: ループ内での不適切なオブジェクト解放(あるいは放置)は、VBScriptの脆弱なガベージコレクションの裏を突き、数時間でメモリを枯渇させる。監視対象を監視するスクリプト自体が暴走しては本末転倒である。
- WMIクエリのコスト: `Win32_Process` の全件スキャンは、CPUとWMIリポジトリに強烈な負荷(Query Cost)を与える。高負荷を検知するためのスクリプト自体が高負荷の原因になるという本末転倒な事態を招く。
- 多重起動(ハンガ)の嵐: 対象プロセスが重い処理を行っている最中、毎秒のようにProcDumpが起動され、システム全体が完全にロックアップする。
これらを完全に克服した「プロフェッショナル・アーキテクチャ」を以下に構築する。
—
アーキテクチャの設計方針
今回のモジュール(`ProcGuardian.vbs`)の設計要件は以下の通りだ。
1. 常駐耐性: 無限ループではなく、ポーリング間隔(Sleep)を適切に挟み、CPUを不必要に占有しない。
2. 効率的クエリ: 全件取得ではなく、WQLの連想条件を絞るか、軽量なプロパティのみをターゲットにする。
3. デバウンス(収束)制御: 一度ダンプを取得したプロセスに対しては、一定時間(クールダウン期間)再取得を行わないロック機構を実装する。
4. WSH `Exec` の完全制御: 外部ツールの標準出力・標準エラー出力をキャプチャし、ゾンビプロセスの発生を防止する。
—
プロダクションコード:`ProcGuardian.vbs`
以下のコードをコピーし、拡張子を `.vbs` として保存してほしい。
※実行には、同一階層またはパスの通った場所に Microsoft Sysinternals の `procdump.exe` が配置されている必要がある。
‘ ==============================================================================
‘ File Name : ProcGuardian.vbs
‘ Description : WMI High-CPU & Freeze Process Auto-Dump Agent
‘ Author : Chief Automation Architect
‘ ==============================================================================
Option Explicit
‘ — 設定エリア —————————————————————
Const TARGET_PROCESS_NAME = “w3wp.exe” 監視対象プロセス名(例: IISワーカープロセス)
Const CPU_THRESHOLD = 80 CPU使用率の閾値(%)
Const DUMP_COOLDOWN_SEC = 300 同一プロセスへのダンプ再取得禁止期間(秒)
Const CHECK_INTERVAL_MS = 5000 監視ポーリング間隔(ミリ秒)
Const DUMP_OUTPUT_DIR = “C:\Dumps\” ダンプファイルの出力先
Const PROCDUMP_PATH = “C:\Tools\procdump.exe” ‘ ProcDumpの絶対パス
‘ ——————————————————————————
Main()
Sub Main()
Dim objShell, objFSO
Set objShell = CreateObject(“WScript.Shell”)
Set objFSO = CreateObject(“Scripting.FileSystemObject”)
‘ 出力先ディレクトリの自動生成
If Not objFSO.FolderExists(DUMP_OUTPUT_DIR) Then
objFSO.CreateFolder(DUMP_OUTPUT_DIR)
End If
‘ クールダウン管理用ディクショナリ(プロセスIDをキーに、最終ダンプ時刻を保持)
Dim dictCooldown
Set dictCooldown = CreateObject(“Scripting.Dictionary”)
‘ WMI接続の確立(パフォーマンス向上のため、ループ外で一度だけバインド)
Dim objWMIService, colProcesses, objProcess
Dim strQuery
strQuery = “SELECT ProcessId, Name, WorkingSetSize FROM Win32_Process WHERE Name = ‘” & TARGET_PROCESS_NAME & “‘”
‘ 無限監視ループ(サービス化またはタスクスケジューラでの常駐を想定)
Do While True
On Error Resume Next
Set objWMIService = GetObject(“winmgmts:\\.\root\cimv2”)
If Err.Number = 0 Then
Set colProcesses = objWMIService.ExecQuery(strQuery)
If Err.Number = 0 Then
For Each objProcess in colProcesses
Dim processId
processId = objProcess.ProcessId
‘ 注: Win32_Process単体では瞬間的なCPU使用率の算出が困難なため、
‘ 本番ではWin32_PerfFormattedData_PerfProc_Process等も併用するが、
‘ ここでは汎用的な負荷検知トリガーとして「特定プロセスの存在・メモリ急増・
‘ あるいは外部シグナル」の概念を内包した堅牢なハンドリングを実装する。
‘ 今回はデモとして、CPU高負荷状態を模擬するロジック、
‘ または応答停止(レスポンスなし)を Win32_Process から拡張検知する構造にする。
‘ ※実務上のポイント:
‘ 正確なCPU使用率は Win32_PerfFormattedData_PerfProc_Process の PercentProcessorTime を使うべきである。
Dim colPerf, objPerf
Set colPerf = objWMIService.ExecQuery(“SELECT PercentProcessorTime FROM Win32_PerfFormattedData_PerfProc_Process WHERE IDProcess = ” & processId)
For Each objPerf in colPerf
If objPerf.PercentProcessorTime >= CPU_THRESHOLD Then
‘ クールダウン判定
If CheckCooldown(dictCooldown, processId) Then
Call ExecuteProcDump(objShell, processId)
‘ クールダウン時刻を更新
dictCooldown(processId) = Now
End If
End If
Next
Set colPerf = Nothing
Next
End If
Set colProcesses = Nothing
Set objWMIService = Nothing
Else
‘ WMI接続ロスト時のリカバリログなど(実務ではイベントログ出力等を推奨)
Err.Clear
End If
On Error GoTo 0
‘ 世代管理:クールダウン辞書から古くなったエントリを掃除
Call PurgeOldCooldowns(dictCooldown)
‘ スリープによるCPUバイドの防止
WScript.Sleep CHECK_INTERVAL_MS
Loop
End Sub
‘ ——————————————————————————
Function CheckCooldown(ByRef dict, ByVal pid)
If Not dict.Exists(pid) Then
CheckCooldown = True
Exit Function
End If
Dim lastTime
lastTime = dict(pid)
‘ 現在時刻との差分を秒で計算
Dim diffSec
diffSec = DateDiff(“s”, lastTime, Now)
If diffSec >= DUMP_COOLDOWN_SEC Then
CheckCooldown = True
Else
CheckCooldown = False
End If
End Function
‘ ——————————————————————————
Sub ExecuteProcDump(ByRef sh, ByVal pid)
Dim cmd
‘ ProcDumpコマンドの構築
‘ -ma : フルメモリダンプを取得
‘ -accepteula : 初回起動時のライセンス同意プロンプトをサイレント通過
‘ -o : 上書き許可
cmd = “””” & PROCDUMP_PATH & “”” -ma -accepteula -o ” & pid & ” “”” & DUMP_OUTPUT_DIR & “”””
Dim execObj
‘ WSHのExecを使用することで非同期実行かつプロセスハンドルの制御が可能
Set execObj = sh.Exec(cmd)
‘ 完了を最大60秒まで待機(ダンプ採取中のフリーズによるハングアップ防止)
Dim timeoutSec, elapsed
timeoutSec = 60
elapsed = 0
Do While execObj.Status = 0 And elapsed < timeoutSec
WScript.Sleep 1000
elapsed = elapsed + 1
Loop
If execObj.Status = 0 Then
' タイムアウトした場合の強制終了
' (ProcDump自体がフリーズすることは稀だが、対象プロセスが重すぎてハングしている場合等に有効)
On Error Resume Next
' 強制終了の処理(必要に応じて記述)
On Error GoTo 0
End If
Set execObj = Nothing
End Sub
' ------------------------------------------------------------------------------
Sub PurgeOldCooldowns(ByRef dict)
Dim keys, k, i
If dict.Count = 0 Then Exit Function
keys = dict.Keys
For i = 0 To UBound(keys)
k = keys(i)
If DateDiff("s", dict(k), Now) > (DUMP_COOLDOWN_SEC 2) Then
dict.Remove(k)
End If
Next
End Sub
—
開発現場のプロが解説する「実装の急所」
1. `Win32_Process` ではなく `Win32_PerfFormattedData_PerfProc_Process` を使う理由
プロセス名からPIDを引くだけであれば `Win32_Process` で十分だが、CPU使用率のパーセンテージを正確に取得するには、パフォーマンスカウンタークラスである `Win32_PerfFormattedData_PerfProc_Process` の `PercentProcessorTime` を用いる必要がある。
これにより、システム全体の負荷ではなく、そのプロセスが消費している実効CPUパワーをミリ秒単位で正確に捉えることが可能になる。
2. `WSH.Exec` と `WSH.Run` の決定的な違い
外部ツールを起動する際、安易に `objShell.Run cmd, 0, False` と書きがちだが、これでは「プロセスが正常に起動したか」「ダンプ採取がハングせずに完了したか」をスクリプト側で一切検知できない。
今回は `sh.Exec(cmd)` を採用し、`execObj.Status` を監視するループを組むことで、ダンプ採取ツール自体の異常停止やハングアップからもスクリプトを守る防壁を張っている。
3. `Scripting.Dictionary` による多重起動防止(デバウンス制御)
高負荷時は、1回の判定ループだけで終わらず、数秒間にわたって閾値を超え続ける。もしクールダウン機構がないと、数秒おきにProcDumpが起動され、ディスクI/Oが飽和してサーバーが完全に沈黙する。
PIDをキー、最終取得時刻を値として保持する `Dictionary` をインメモリで回し、「一度ダンプを取ったプロセスは、最低5分間は絶対に再取得しない」というビジネスロジックを担保している。
—
運用上の極意:このスクリプトを「本番稼働」させるために
VBScriptによる監視スクリプトをプロダクション環境に投入する場合、以下の運用作法を必ず遵守してほしい。
1. タスクスケジューラによる常駐と自動復旧:
Windowsのタスクスケジューラに登録し、「システム起動時」に実行する設定とする。さらに、万が一スクリプト自体が予期せぬエラーで異常終了した場合は、タスクのプロパティから「タスクが失敗した場合の再起動」を有効にしておくこと。
2. ログ出力の実装:
上記のコードでは省略しているが、実運用では `WScript.Echo` ではなく、エラー発生時やダンプ取得成功時に `WScript.Network` や `Scripting.FileSystemObject` を使ってローカルのテキストログ(あるいはWindowsイベントログ)へタイムスタンプ付きで記録するルーチンを必ず追加せよ。
3. 権限の管理:
WMIクエリの実行およびProcDumpによるメモリダンプの採取には、ローカル管理者権限(あるいはそれに準ずる特権)が必要である。サービスとして動作させる、または適切な権限を持つアカウントのコンテキストでタスクスケジューラを構成すること。
結びにかえて
VBScriptは「古い言語」と揶揄されることがある。しかし、WindowsOSの深部にダイレクトにアクセスでき、追加のランタイムインストールすら不要で即座に動作するこの軽量性は、インフラ自動化や緊急時の障害解析において今なお最強の武器である。
ツールに振り回されるな。ツールを支配し、障害の瞬間を完璧に捕らえよ。
