【WMIメモリリーク対策】SWbemServices.ExecQuery 大量取得時におけるリソース圧迫回避と最適化処理
開発現場でよく見落とされるのが、WMI(Windows Management Instrumentation)を使ったシステム情報取得におけるメモリリークだ。
「数百台の端末からインベントリを収集するスクリプトを回したら、なぜか途中でメモリ消費量が跳ね上がり、最終的にスクリプトがクラッシュする」
「毎日の定時バッチで動かしているWSH(Windows Script Host)が、数ヶ月運用するうちにサーバーのメモリを食いつぶす」
こうした現場の悲劇の多くは、`SWbemServices.ExecQuery` のデフォルトの挙動、そしてCOMオブジェクトのライフサイクル管理に対する無理解に起因している。
今回は、VBScriptによるWMI操作の限界を見極め、大規模環境でも微動だにしない堅牢なクエリ実行手法をロジカルに解説する。
—
なぜ、通常のWMIクエリはメモリをリークするのか?
VBScriptで最も一般的に使われるWMIの取得コードを見てみよう。
‘ 【アンチパターン】デフォルトのクエリ実行
Set objWMIService = GetObject(“winmgmts:\\.\root\cimv2”)
Set colItems = objWMIService.ExecQuery(“Select from Win32_Process”)
For Each objItem in colItems
WScript.Echo objItem.Name
Next
一見、何の問題もない美しいコードに見える。しかし、この書き方には致命的な設計上の欠陥がある。
1. デフォルトの「スナップショット」と双方向カーソル
引数なしの `ExecQuery` は、内部的に「半同期モード」かつ「双方向カーソル(Bidirectional Cursor)」でコレクションを生成する。
これは、取得したすべてのプロセス情報をWMIプロバイダがメモリ上にキャッシュし、スクリプト側から自由に行ったり来たり(先頭に戻ったり)できるように保持し続ける動作を意味する。
管理対象のサーバーやクライアントで数千のプロセスやイベントログが存在する場合、このキャッシュだけで膨大なメモリが消費される。
2. VBScriptとCOMの参照カウンタの闇
VBScriptはガベージコレクタ(GC)を持たない。スクリプトが終了するまで、あるいは明示的に `Set obj = Nothing` するまでオブジェクトはメモリ上に居座り続ける。
さらに、`For Each` ループ内で暗黙的に生成される個々のインスタンスの参照解放タイミングは、VBScriptのエンジン任せであり、即座にはメモリからパージされない。これが蓄積することで、典型的なメモリリークを引き起こすのだ。
—
極限の最適化:3つの処方箋
大規模環境や常時稼働するバッチ処理でリソース圧迫を回避するためには、以下の3つの原則を徹底しなければならない。
処方箋1:`wbemFlagForwardOnly`(前方 اعتاد 专用 / 前方移動専用フラグ)の指定
クエリ結果のコレクションを「巻き戻す必要がない(上から順に舐めるだけ)」のであれば、双方向カーソルは不要である。
フラグ値 `32`(`wbemFlagForwardOnly`) を指定することで、WMIはデータをキャッシュせず、ストリーミング方式で順次破棄しながらデータを返すようになる。これによりメモリ消費量は劇的に削減される。
処方箋2:`wbemFlagEnsureLocatable` との組み合わせ(必要に応じて)
データの鮮度や確実性を担保するため、通常は `32` に加えて `48`(`32 + 16`:`wbemFlagForwardOnly` + `wbemFlagEnsureLocatable`) を指定するのが実務上最も安全かつ高速である。
処方箋3:スコープの最小化と明示的な `Nothing` 解放
ループ内でのオブジェクト生成を極力避け、不要になったコレクションやサービスオブジェクトは即座にメモリから解放する。
—
プロダクションコード例:堅牢なプロセス情報・リソース収集スクリプト
実務の現場でそのままデプロイできるよう、エラーハンドリングとメモリ最適化を極限まで高めたVBScriptのプロダクションコードを提示する。
Option Explicit
‘ ==============================================================================
‘ スクリプト名: Get-OptimizedWMIInventory.vbs
‘ 概要 : 大規模環境におけるメモリリークを排除したWMI情報取得サンプル
‘ 開発者 : チーフアーキテクト
‘ ==============================================================================
Main
Sub Main()
On Error Resume Next
Dim objLocator, objWMIService, colItems, objItem
Dim strComputer, strQuery
Dim intCount
strComputer = “.”
intCount = 0
‘ WMI定数の定義 (Type ライブラリが使えないVBScriptでは明示的に定義する)
Const wbemFlagForwardOnly = 32
Const wbemFlagReturnImmediately = 16
Const wbemFlagEnsureLocatable = 48 ‘ ForwardOnly(32) + ReturnImmediately(16)
WScript.Echo “=== WMI最適化クエリ実行開始 ===”
‘ 1. SWbemLocatorを使用したセキュアな接続(GetObjectよりも接続失敗時のハンドリングが優れている)
Set objLocator = CreateObject(“WbemScripting.SWbemLocator”)
Set objWMIService = objLocator.ConnectServer(strComputer, “root\cimv2”, “”, “”)
If Err.Number <> 0 Then
WScript.Echo “[ERROR] WMI接続失敗: ” & Err.Description
Exit Sub
End If
‘ 権限の委任設定(リモート接続時のトラブルシューティングに有効)
objWMIService.Security_.ImpersonationLevel = 3 ‘ Impersonate
‘ 2. クエリの構築(必要なプロパティだけに絞るのがプロの作法 “Select Name, ProcessId…”)
strQuery = “SELECT Name, ProcessId, WorkingSetSize FROM Win32_Process”
‘ 3. 【最重要】フラグ「48 (ForwardOnly + ReturnImmediately)」を指定して実行
Set colItems = objWMIService.ExecQuery(strQuery, “WQL”, wbemFlagEnsureLocatable)
If Err.Number <> 0 Then
WScript.Echo “[ERROR] クエリ実行失敗: ” & Err.Description
Set objWMIService = Nothing
Set objLocator = Nothing
Exit Sub
End If
‘ 4. ストリーミング処理によるメモリ圧迫のないループ処理
For Each objItem in colItems
‘ ここでファイル出力やDB連携処理を行う
‘ 例としてカウントのみ出力
intCount = intCount + 1
‘ 巨大な環境では、オブジェクトのプロパティ参照時にメモリを圧迫しないよう
‘ ローカル変数に一度バインドする
Dim pName, pId, pMem
pName = objItem.Name
pId = objItem.ProcessId
pMem = objItem.WorkingSetSize
‘ デバッグ出力(本番ではログファイル出力等に置き換える)
‘ WScript.Echo pId & ” : ” & pName & ” (” & pMem & ” bytes)”
‘ ループ内で生成された可能性のある個別インスタンスの参照を断つ
Set objItem = Nothing
Next
WScript.Echo “=== 処理完了 総プロセス数: ” & intCount & ” ===”
‘ 5. 【鉄則】オブジェクトの明示的かつ逆順の解放
Set colItems = Nothing
Set objWMIService = Nothing
Set objLocator = Nothing
On Error Goto 0
End Sub
—
ファイル連携・データベース連携における注意点
このスクリプトを基盤として、取得したデータをCSVファイルやRDB(SQL Server / SQLite等)に流し込む際の実務的な注意点を挙げる。
1. ファイルハンドルのリーク対策
`Scripting.FileSystemObject` を用いてログやCSVに出力する際も、ファイルストリーム(`TextStream` オブジェクト)を開きっぱなしにしないこと。レコードごとに開閉するのではなく、一連のループの前後で開閉し、最後に必ず `Set objStream = Nothing` を行うこと。
2. トランザクションの適切な分割
データベースにインサートする場合、数万件のデータを1つのトランザクションで処理すると、WMI側ではなくDB側のログ領域やメモリが圧迫される。`1000件ごとにコミットを挟む` などのバッチ処理的アプローチを必ず実装設計に組み込むこと。
—
チーフアーキテクトからの総括
「VBScriptは古い言語だからメモリリークするのは仕方ない」というエンジニアがいるならば、それは単なる技術不足の言い訳に過ぎない。
OSの根幹を叩くWMIという強力なインターフェースを使う以上、その裏側にあるCOMの仕組みと、フラグ指定によるメモリ最適化の理屈を理解していれば、リソースを完全にコントロールすることは十分に可能だ。
現場のインフラを支える自動化スクリプトこそ、美しく、そしてタフでなければならない。
今回の知見をあなたのプロダクション環境に即座に導入し、安定稼働を実現してほしい。
