【WMIメモリリーク対策】SWbemServices.ExecQuery 大規模環境におけるリソース圧迫回避と最適化処理
レガシーシステムの保全、あるいはドメイン全体を対象としたクライアント端末のインベントリ収集。その現場において、VBScriptとWMI(Windows Management Instrumentation)のコンビネーションがいまだに現役の主力として稼働しているケースは少なくない。
だが、数千台規模のネットワークや、動的に大量のプロセス・イベントログを抱えるサーバー群に対して、無策なWMIクエリを発射すればどうなるか。結果は明白だ。Wscript.exeが突発的なメモリ肥大化(メモリリーク)を起こし、最終的にOut of Memoryでスクリプトはクラッシュ、あるいはOS自体のリソースを圧迫して監視システム全体を巻き込む障害へと発展する。
今回は、VBScriptにおけるWMIオブジェクトのライフサイクル、そして`SWbemServices.ExecQuery`が隠し持つ致命的な罠と、それを回避する極限の最適化手法について、アーキテクトの視点から紐解いていく。
—
1. なぜ `ExecQuery` はメモリを食い潰すのか?
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
このコード、小規模な検証環境であれば何の問題もなく動作する。しかし、対象が「数万件のプロセス」や「膨大なイベントログ」だった場合、メモリ使用量が右肩上がりに上昇し、スクリプト終了後もプロセス空間にメモリが残留する現象が発生する。
原因は、デフォルトのクエリ実行挙動にある。
半構造化データのキャッシュとコレクションの罠
引数なしで `ExecQuery` を呼び出した場合、WMIは「半同期モード (Semi-synchronous)」かつ「双方向コレクション (Bidirectional Collection)」として結果セットをメモリ上に全展開する。
つまり、クエリにヒットした数万件のオブジェクト(`SWbemObject`)のインスタンスが、すべてローカルのCOMコンテキスト内にキャッシュされるのだ。VBScriptのガベージコレクション(参照カウント方式)は、巨大なコレクションがスコープ内にある間、メモリを解放しない。さらに、COMのマーシャリングとラッパーの生成コストが重なり、プロセスは一瞬で肥大化する。
—
2. 極限の最適化:`wbemFlagForwardOnly` と `wbemFlagUseAmendedQualifiers`
このリソース圧迫を根絶するためには、WMIの挙動を「前方オンリー(ストリーム処理)」へと強制的に切り替える必要がある。
WMI定数である `wbemFlagForwardOnly` (0x20) と `wbemFlagReturnImmediately` (0x10) をビット論理和(`OR`)で組み合わせ、`ExecQuery` の第3引数に渡すのだ。
Const wbemFlagForwardOnly = 32 ‘ 0x20
Const wbemFlagReturnImmediately = 16 ‘ 0x10
Const wbemFlags = wbemFlagForwardOnly + wbemFlagReturnImmediately
このフラグを指定すると、WMIは結果セットをメモリ上に一括保持せず、カーネル側からデータを1件ずつストリーミング(逐次フェッチ)するようになる。コレクションの逆方向への移動や `.Count` プロパティの事前取得はできなくなるが、メモリ消費量は「常に一定(数件分のオブジェクトサイズのみ)」に抑えられる。
—
3. 実装コード:大規模環境に耐える堅牢なWMIストリーミング処理
以下に、数万件規模のオブジェクトを安全に処理するためのプロダクション品質のVBScriptを示す。明示的なオブジェクトの解放(`Nothing`代入)とエラーハンドリングを徹底した実装だ。
Option Explicit
‘ —————————————————————–
定数定義
‘ —————————————————————–
Const wbemFlagForwardOnly = 32 ‘ 0x20: カーソルを前方にのみ移動(メモリ最適化の要)
Const wbemFlagReturnImmediately = 16 ‘ 0x10: 非同期ではなく即座に制御を返す
Const wbemFlags = 48 (32 + 16)
Sub ExecuteOptimizedWMIQuery()
Dim objLocator, objWMIService, colItems, objItem
Dim lngCount
‘ エラーハンドリングの有効化
On Error Resume Next
‘ 1. SWbemLocatorを使用した明示的な接続(認証やリモート接続への拡張性を担保)
Set objLocator = CreateObject(“WbemScripting.SWbemLocator”)
If Err.Number <> 0 Then
WScript.Echo “SWbemLocatorの生成に失敗しました: ” & Err.Description
Exit Sub
End If
‘ ローカル接続(リモートの場合はマシン名や資格情報を渡す)
Set objWMIService = objLocator.ConnectServer(“.”, “root\cimv2”, “”, “”)
objWMIService.Security_.ImpersonationLevel = 3 ‘ Impersonate
If Err.Number <> 0 Then
WScript.Echo “WMIサービスへの接続に失敗しました: ” & Err.Description
Set objLocator = Nothing
Exit Sub
End If
On Error GoTo 0 ‘ 標準のエラー処理に戻す
WScript.Echo “— WMIストリーミングクエリ開始 —”
‘ 2. フラグを指定してクエリを実行(メモリ爆発を防ぐ)
Set colItems = objWMIService.ExecQuery(“SELECT ProcessId, Name, WorkingSetSize FROM Win32_Process”, , wbemFlags)
lngCount = 0
‘ 3. ForwardOnlyコレクションのイテレーション
For Each objItem In colItems
‘ ループ内で必要な処理を実行
‘ ※注意: colItems.Count は ForwardOnly モードでは使用不可
lngCount = lngCount + 1
‘ 冗長なプロパティアクセスを避け、必要なデータのみ抽出
‘ ログ出力やDB書き込みなど
If lngCount Mod 1000 = 0 Then
WScript.Echo lngCount & ” 件処理完了…”
End If
‘ 4. 【重要】ループ内での個別オブジェクトの解放
‘ 膨大なループを回す場合、次の要素に移る前に参照を明示的に断つことが推奨される
Set objItem = Nothing
Next
WScript.Echo “— 処理終了 (総件数: ” & lngCount & ” 件) —”
‘ 5. ルートレベルのオブジェクトの明示的解放
‘ VBScriptのスコープ抜けによる暗黙の解放を待たず、即座にCOM参照を破棄する
Set colItems = Nothing
Set objWMIService = Nothing
Set objLocator = Nothing
End Sub
‘ 実行
ExecuteOptimizedWMIQuery()
WScript.Echo “スクリプト正常終了。メモリはクリーンに保たれています。”
—
4. チーフアーキテクトが教える、現場の「落とし穴」と極意
長年、数千台のクライアントを統括する基幹スクリプトの保守に携わってきた経験から、WMIを扱う上で絶対に守るべき鉄則をいくつか共有しておこう。
① `.Count` プロパティの誘惑に負けるな
通常のコレクションであれば `colItems.Count` で総数を取得したくなるが、`wbemFlagForwardOnly` を指定したコレクションに対して `.Count` を呼び出すと、エラー(または予期せぬ全件ロード)が発生する。件数が必要な場合は、ループ内でカウンター変数(`lngCount = lngCount + 1`)をインクリメントするストリーム処理型で実装すること。
② イベントドリブンや非同期クエリとの混同
「非同期なら速いはずだ」と `ExecNotificationQuery` や `ExecQueryAsync` に走る開発者がいるが、VBScript環境において非同期クエリのイベントハンドリング(WbemScripting.SWbemSink)を実装することは、コードの複雑性を飛躍的に高め、デバッグ不可能なデッドロックやCOMスレッドモデル(STA/MTA)の衝突を引き起こす悪手である。
同期処理であっても、今回紹介した `wbemFlagForwardOnly` さえ適切に使えば、パフォーマンスと安定性の両面で十分すぎる実用速度を叩き出せる。
③ スクリプトの寿命とプロセスリサイクル
VBScriptの実行エンジン(`wscript.exe` / `cscript.exe`)は、長大なループや複数回の重いWMIクエリを連続して実行すると、COMのヒープフラグメンテーションにより微小なメモリリークが蓄積する場合がある。
極限の安定性を求めるバッチシステムでは、1つのスクリプトにすべてを詰め込まず、処理単位でスクリプトプロセスを完全に終了(Exit)させ、Windowsのジョブスケジューラや上位のバッチファイルから連鎖起動するアーキテクチャ設計こそが、真のレガシーシステム防衛術となる。
—
総括
VBScriptは「古い言語」である。しかし、OSの深部(WMIやCOM)にダイレクトにアクセスできるその仕様は、適切にコントロールさえすれば、現代の重厚なフレームワーク群よりも圧倒的な軽量性と俊敏性を発揮する。
`SWbemServices.ExecQuery` のフラグチューニングと、容赦なきオブジェクトの明示的解放。この二つを徹底するだけで、あなたのWMIスクリプトは、どんなに巨大なエンタープライズ環境であっても、メモリを微動だにさせず、静かに、そして確実に完遂されるだろう。
