IOPSの暴走を制する:VBScriptによる「スロットリング制御型」大容量ファイル転送エンジンの構築
プロフェッショナルな現場において、`FileSystemObject (FSO)` の `CopyFile` メソッドを無造作に叩くことは、時として致命的な過ちとなる。
共有ストレージ(SAN/NAS)や、高負荷なDBサーバーが稼働する環境において、制御不能なディスクI/Oは、システム全体のレイテンシを増大させ、他のミッションクリティカルなプロセスを窒息させる。我々エンジニアに求められるのは、単に「ファイルをコピーすること」ではない。「システムの健全性を維持しながら、計算された帯域内でデータを移動させること」である。
本稿では、FSOの限界を突破し、I/O負荷を平滑化(Smoothing)するための「スロットリング制御付き転送エンジン」の設計思想とその実装について詳述する。
1. なぜ `FSO.CopyFile` ではいけないのか
標準的な `FSO.CopyFile` は、OSのファイルシステムレベルでの最適化を最大限に利用しようとする。これは単一のタスクとしては高速だが、以下の制御不能な挙動を招く。
1. I/O Burst (スパイク) の発生: 転送開始直後、OSは利用可能なディスク帯域とIOPSを限界まで使い切ろうとする。これがストレージコントローラーのキューを埋め尽くし、他のアプリケーションに対する応答速度を劇的に低下させる。
2. 進捗・負荷制御の欠如: コピーが完了するまで、スクリプト側からは「今、どれだけの負荷をかけているか」を制御する術がない。
3. メモリ管理のブラックボックス化: 大容量ファイルを扱う際、OS側のバッファリングに依存するため、メモリ使用量の推移を予測できない。
プロフェッショナルが構築すべきは、ファイルを「塊(Chunk)」として扱い、「読み込み $\rightarrow$ 書き込み $\rightarrow$ 待機(Sleep)」 のサイクルを自律的に回すエンジンである。
2. アーキテクチャ設計:スロットリング・アルゴリズム
本エンジンでは、`ADODB.Stream` を利用してバイナリデータをチャンク単位で制御する。`FSO` はファイル存在確認やディレクトリ操作に留め、実データ転送のコアには、より低レイヤーに近い挙動が可能な `ADODB.Stream` を採用するのが定石である。
アルゴリズムのフロー
1. Chunk Sizeの定義: 一回に読み込むバイト数(例: 1MB, 4MB)。
2. Target Throughputの定義: 許容する転送速度(例: 10MB/s)。
3. Time Deltaの計測: チャンクの読み書きにかかった時間を計測。
4. Dynamic Sleepの算出:
$$\text{Sleep Time} = \left( \frac{\text{Chunk Size}}{\text{Target Throughput}} \right) – \text{Actual Elapsed Time}$$
もし `Actual Elapsed Time` が目標時間を超えていれば、Sleepはスキップする。
3. 実装:Throttled File Transfer Engine
以下に、VBScriptによる極限の実装例を示す。このコードは、単なるコピー機能ではなく、システムの負荷を計算しながら動作する「制御エンジン」としての構造を持っている。
' ==============================================================================
' Throttled File Transfer Engine (Professional Edition)
'
' 概要: IOPS負荷を抑制するため、指定したスループットに基づき
' 動的なSleep制御を行うバイナリ転送エンジン。
'
' 注意: VBScriptには直接の Windows API Declare ができないため、
' WScript.Sleep を使用して制御を行う。
' ==============================================================================
Option Explicit
' --- 設定パラメータ ---
Const CHUNK_SIZE = 1048576 ' 1MB per chunk
Const TARGET_MBPS = 20 ' 目標転送速度 (MB/s)
Const DEBUG_MODE = True
Dim fso
Set fso = CreateObject("Scripting.FileSystemObject")
' 実行例
Dim sourceFile, destFile
sourceFile = "C:\Source\LargeData.dat"
destFile = "D:\Backup\LargeData.dat"
On Error Resume Next
Call ThrottledCopy(sourceFile, destFile)
If Err.Number <> 0 Then
WScript.Echo "CRITICAL ERROR: " & Err.Description
Else
WScript.Echo "Transfer Completed Successfully."
End If
On Error GoTo 0
' オブジェクトの明示的解放
Set fso = Nothing
' ------------------------------------------------------------------------------
' メイン関数: スロットリング制御付きコピー
' ------------------------------------------------------------------------------
Sub ThrottledCopy(strSrc, strDest)
Dim objSrcStream, objDestStream
Dim lngBytesRead, lngTargetTimeMS, lngStartTime, lngEndTime, lngElapsedMS
Dim dblChunkSizeMB, dblTargetTimeSec, dblActualTimeSec
If Not fso.FileExists(strSrc) Then
Err.Raise 1, "ThrottledCopy", "Source file not found."
End If
' 目標チャンクあたりの所要時間(秒)を計算
dblChunkSizeMB = CHUNK_SIZE / 1048576
dblTargetTimeSec = dblChunkSizeMB / TARGET_MBPS
lngTargetTimeMS = dblTargetTimeSec * 1000
' Streamオブジェクトの生成 (ADODB.Streamを使用)
' Binaryモードを使用することで、FSOのテキストモードによる破損を防ぐ
Set objSrcStream = CreateObject("ADODB.Stream")
Set objDestStream = CreateObject("ADODB.Stream")
objSrcStream.Open
objSrcStream.Type = 1 ' adTypeBinary
objSrcStream.LoadFromFile strSrc
objDestStream.Open
objDestStream.Type = 1 ' adTypeBinary
' 書き込み先が新規作成の場合はそのまま、既存の場合は追記(adWrite)
objDestStream.Write ""
If DEBUG_MODE Then WScript.Echo "Starting throttled transfer... Target: " & TARGET_MBPS & " MB/s"
' 転送ループ
Do Until objSrcStream.EOS
lngStartTime = Timer ' ミリ秒精度の計測が必要な場合はAPI検討だが、VBSではTimerが限界
' Timerは秒単位だが、小数を保持するため実質的にミリ秒に近い精度を持つ
' 1. チャンク読み込み
objSrcStream.Read(CHUNK_SIZE)
' 2. チャンク書き込み
objDestStream.Write objSrcStream.GetStream // objSrcStream ' ※実際にはReadしたデータを保持する必要がある
' 上記は概念的な記述。ADODB.Streamの仕様に基づき、正しい実装は以下:
' (以下、修正された実動作ロジック)
Loop
' --- 修正版ループロジック (実用的な実装) ---
' 再定義
Set objSrcStream = CreateObject("ADODB.Stream")
objSrcStream.Open
objSrcStream.Type = 1
objSrcStream.LoadFromFile strSrc
Set objDestStream = CreateObject("ADODB.Stream")
objDestStream.Open
objDestStream.Type = 1
Do Until objSrcStream.EOS
lngStartTime = Timer
' チャンク取得
Dim binChunk
binChunk = objSrcStream.Read(CHUNK_SIZE)
' 書き込み
objDestStream.Write binChunk
' 時間計測
lngEndTime = Timer
lngElapsedMS = (lngEndTime - lngStartTime) * 1000
' スロットリング計算
If lngElapsedMS < lngTargetTimeMS Then
Dim sleepTime
sleepTime = CLng(lngTargetTimeMS - lngElapsedMS)
If sleepTime > 0 Then
WScript.Sleep sleepTime
End If
End If
If DEBUG_MODE Then
WScript.Echo "Progress: " & Int(objSrcStream.Position / 1048576) & " MB processed..."
End If
Loop
' 保存とクローズ
objDestStream.SaveToFile strDest, 2 ' adSaveCreateOverWrite
objSrcStream.Close
objDestStream.Close
' メモリ解放 (極めて重要)
Set objSrcStream = Nothing
Set objDestStream = Nothing
End Sub
4. プロフェッショナルのための最適化・保守知見
メモリ管理の極意
VBScriptにおける `Set obj = Nothing` は、単なるマナーではない。特に `ADODB.Stream` のようなCOMオブジェクトを使用する場合、参照カウントが残っていると、プロセスが終了するまでメモリ(およびファイルハンドル)が解放されない。大量のファイルをループで処理する場合、明示的な解放を怠ると、容易に `Out of Memory` または `File in use` エラーを招く。
チャンクサイズの選定
`CHUNK_SIZE` は、小さすぎると `WScript.Sleep` のオーバーヘッドとコンテキストスイッチのコストが支配的になり、転送効率が落ちる。逆に大きすぎると、スロットリングの解像度が低くなり、I/Oスパイクを抑えきれない。
- LAN環境: 4MB ~ 8MB
- 低速なWAN/ストレージ: 64KB ~ 256KB
といった使い分けが、熟練のエンジニアの判断基準である。
レガシー環境への配慮
古いWindows Server環境では、`ADODB.Stream` の挙動がOSのキャッシュ管理に強く依存する場合がある。本スクリプトのように、アプリケーション層で明示的に `Read` と `Write` を分けることで、OSのプリフェッチ機能による予期せぬI/O増大を抑制できる。
結論
VBScriptはレガシーな技術として片付けられがちだが、その真髄は「OSの挙動をいかに理解し、制約の中でいかに振る舞うか」という設計思想にある。
`FSO.CopyFile` という「魔法」に頼るのではなく、データの流れを、時間と容量の観点から完全に支配する。それこそが、インフラの安定性を守るシニアエンジニアの責務である。
