【メモリ消費自律監視】WMI Win32_Process 経由で自身のメモリ使用量を監視し閾値超えで安全停止する自己防衛機構
開発現場でVBScriptによるバッチ処理や業務自動化ツールを設計していると、避けて通れないのが「メモリリーク」と「リソース枯渇」の悪夢だ。
特に、数万件規模のCSVパース、巨大なADOレコードセットの操作、あるいは外部COMコンポーネント(ExcelやIEなど)の不完全な解放が重なると、プロセスは徐々に肥大化し、最終的にはWindowsの仮想メモリを圧迫してサーバーやクライアントPCをフリーズさせる。
「エラーが出たら止まる」だけの脆弱なスクリプトを野放しにしてはならない。プロのエンジニアが作るべきは、自らのリソース消費をメタ認知し、限界を迎える前に自発的にクリーンアップして安全に身を引く「自己防衛機構」を備えたスクリプトだ。
今回は、WMI(Windows Management Instrumentation)の `Win32_Process` を利用して自プロセスのメモリ使用量をリアルタイムに監視し、閾値を超えた瞬間に优雅(エレガント)かつ安全に停止するプロダクションコードの全貌を伝授する。
—
なぜ「野良スクリプト」はメモリリークに気づけないのか?
VBScriptのランタイム(`cscript.exe` / `wscript.exe`)は、基本的にはシングルスレッドで動作する軽量なホスト環境だが、COMオブジェクトの相互運用や文字列連結の多用によって、ガベージコレクタが回収しきれないメモリ断片化(Memory Fragmentation)を引き起こす。
素人がやりがちな間違いは以下の通りだ:
1. メモリ監視を他力本願にする:OSがスワップアウトしてくれるだろうと楽観視し、タスクマネージャーを見て青ざめる。
2. 例外処理の欠如:OutOfMemoryErrorが発生した時にはすでに手遅れで、ログすら残さずに異常終了する。
3. 無駄なWMIクエリの乱用:監視ロジック自体が重すぎて、それ自体がパフォーマンスボトルネックになる。
これらを解決するためには、「処理の要所要所で軽量に自身のメモリをポーリングし、あらかじめ定めた安全弁(Threshold)を超えたら、トランザクションやファイルを安全に閉じてから自死する」という設計思想が不可欠となる。
—
アーキテクチャ設計:安全停止メカニズムの要件
今回の自己防衛機構を実装するにあたり、以下の設計方針を厳守する。
- 自プロセスのPID(プロセスID)の動的特定:ハードコードせず、実行中の自身のPIDを動的に取得する。
- WMIフィルタリングの最適化:全プロセスを舐めるような愚かなクエリは発行せず、`Handle = ‘PID’` でピンポイントにクエリを叩く。
- 物理メモリ(WorkingSetSize)の監視:単なる仮想サイズではなく、実際に物理メモリを圧迫しているワーキングセット(実メモリ使用量)を監視対象とする。
- 安全なデストラクション:停止時には、開いているファイル、DB接続、COMオブジェクトを確実に解放(`Nothing`代入)し、イベントログやファイルに警告を残す。
—
プロダクションコード:自己防衛機能付きテンプレート
以下のコードは、そのまま実務のバッチ処理の骨格として組み込める完全版だ。定数部分を変更して利用してほしい。
‘ ==============================================================================
‘ Script Name: SelfHealingProcess.vbs
‘ Description: WMIを用いた自プロセスのメモリ使用量監視および安全停止機構
‘ Author: Chief Architect
‘ ==============================================================================
Option Explicit
‘ — 定数定義 —
Const THRESHOLD_MB = 150 ‘ メモリ上限閾値 (MB) – この値を超えたら安全停止
Const CHECK_INTERVAL = 100 ‘ 何回処理を実行するごとにメモリチェックを行うか
Const LOG_PATH = “C:\Logs\ProcessMonitor.log”
‘ メイン処理の実行
Main
Sub Main()
Dim objFSO, logFile
Set objFSO = CreateObject(“Scripting.FileSystemObject”)
‘ ログ出力の準備
Set logFile = objFSO.OpenTextFile(LOG_PATH, 8, True)
logFile.WriteLine “[” & Now & “] INFO: スクリプトを開始しました。PID: ” & GetCurrentProcessID()
Dim i
‘ 模擬的な大量データ処理ループ(実際にはここに業務ロジックが入る)
For i = 1 To 10000
‘ — 業務ロジックのシミュレーション (メモリリークをあえて発生させる例) —
‘ Dim dummyArray()
‘ ReDim Preserve dummyArray(i 1000)
‘ —————————————————————-
‘ 一定間隔でメモリ監視を実行
If i Mod CHECK_INTERVAL = 0 Then
If CheckMemoryUsage(THRESHOLD_MB) Then
logFile.WriteLine “[” & Now & “] WARN: メモリ使用量が閾値 (” & THRESHOLD_MB & “MB) を突破しました。安全停止シーケンスに入ります。”
logFile.Close
‘ 【重要】ここで安全なクリーンアップ(DBクローズ、ファイル閉鎖など)を呼ぶ
Call SafeCleanup()
‘ 正常終了コード(100など)でプロセスを明示的に抜ける
WScript.Quit 100
End If
End If
Next
logFile.WriteLine “[” & Now & “] INFO: スクリプトが正常終了しました。”
logFile.Close
Set objFSO = Nothing
End Sub
‘ ——————————————————————————
‘ 自身のプロセスID (PID) を取得する関数
‘ ——————————————————————————
Function GetCurrentProcessID()
Dim wmi, query, processes, proc
Set wmi = GetObject(“winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2”)
‘ WScript.ScriptFullName を利用して、現在実行中の cscript/wscript インスタンスを特定するのは困難なため、
‘ Win32_Process から実行ファイルのコマンドラインやSessionID等で絞り込むのが一般的だが、
‘ ここでは簡便かつ確実に自プロセスを特定するため、WScript.NameとWScript.ScriptFullNameの組み合わせ、
‘ もしくは簡易的に Win32_Process の CommandLine から自身のスクリプト名を探す手法をとる。
query = “SELECT ProcessId, CommandLine FROM Win32_Process WHERE Name = ‘” & WScript.Name & “‘”
Set processes = wmi.ExecQuery(query)
For Each proc in processes
‘ コマンドラインに自身のスクリプト名が含まれているかを判定
If InStr(proc.CommandLine, WScript.ScriptName) > 0 Then
GetCurrentProcessID = proc.ProcessId
Exit Function
End If
Next
‘ フォールバック: 万が一特定できない場合は 0 を返す
GetCurrentProcessID = 0
End Function
‘ ——————————————————————————
‘ メモリ使用量をチェックし、閾値を超えているか判定する関数
‘ ——————————————————————————
Function CheckMemoryUsage(maxMB)
Dim pid, wmi, query, proc, workingSetBytes, workingSetMB
pid = GetCurrentProcessID()
If pid = 0 Then
CheckMemoryUsage = False
Exit Function
End If
Set wmi = GetObject(“winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2”)
query = “SELECT WorkingSetSize FROM Win32_Process WHERE ProcessId = ” & pid
Set proc = wmi.ExecQuery(query)
For Each p in proc
workingSetBytes = CDbl(p.WorkingSetSize)
‘ バイトからメガバイトへ変換 (1MB = 1024 1024 bytes)
workingSetMB = workingSetBytes / (1024 1024)
‘ ログデバッグ用(必要に応じて有効化)
‘ WScript.Echo “Current Memory: ” & FormatNumber(workingSetMB, 2) & ” MB”
If workingSetMB > maxMB Then
CheckMemoryUsage = True
Exit Function
End If
Next
CheckMemoryUsage = False
End Function
‘ ——————————————————————————
‘ 安全停止時のクリーンアップ処理
‘ ——————————————————————————
Sub SafeCleanup()
On Error Resume Next
‘ 例: DB接続の明示的な切断
‘ If Not objConn Is Nothing Then
‘ objConn.Close
‘ Set objConn = Nothing
‘ End If
‘ 例: 開きっぱなしのファイルオブジェクトの解放
‘ If Not objStream Is Nothing Then
‘ objStream.Close
‘ Set objStream = Nothing
‘ End If
‘ 外部COMオブジェクトの強制解放
‘ Set objExcel = Nothing
On Error GoTo 0
End Sub
—
現場のエンジニアへ送る実装上の急所(Tips)
1. WMIクエリのコスト管理
`Win32_Process` へのクエリ発行は、実はOSにとってそれなりに重い処理だ。これを毎ループ(1件処理するごと)実行すると、監視処理自体がボトルネックになり、スクリプト全体のパフォーマンスが激減する。そのため、上記コードのように 「N回に1回(例: 100件に1回)」 実行するスロットリング(間引き)を必ず入れること。
2. `CDbl` による型キャストの重要性
WMIが返す `WorkingSetSize` は、環境によっては非常に大きな数値(VT_BSTR または 64bit整数に近い数値)として扱われるため、VBScriptの通常の長整数型(Long)に代入すると 「オーバーフロー (Overflow)」 を起こす。必ず `CDbl()` で倍精度浮動小数点数にキャストしてから演算すること。これができていないコードは、本番稼働の数時間後に必ずクラッシュする。
3. 終了コードの設計
異常終了や自己防衛による停止の際、単に `WScript.Quit` するのではなく、`100` や `99` といった固有の終了コード(Exit Code)を返すように設計せよ。これを呼び出し元のマスターバッチ(`.bat` やタスクスケジューラ)側で拾い、「メモリ過多による安全停止」と判定して自動リトライやアラートメール送信へ繋げるのが、エンタープライズな自動化アーキテクチャの鉄則である。
—
総括
「動けばいい」という次元のコードは、現場を疲弊させる。メモリリークを恐れて巨大なバッチを細切れに書き直す必要はない。スクリプト自身にメタ認知の機構(自己防衛機構)を組み込むことで、システムは圧倒的な堅牢性を獲得する。
プロのエンジニアであれば、コードの美しさだけでなく、そのライフサイクルと「散り際」の美しさまでデザインしよう。
