【極限の知見】FSOの「CopyFile」を捨てろ。IOPS負荷を制御するスロットリング型転送エンジンの設計
業務自動化の現場で、多くのエンジニアが陥る「致命的な罠」がある。それは、「FSO(FileSystemObject)の `CopyFile` メソッドを、大規模なデータ移行やバックアップ処理にそのまま使ってしまうこと」だ。
小規模な設定ファイルのコピーなら、それで十分だろう。しかし、数GB、数十GBのログファイルやデータベースのダンプファイルを、共有ストレージ(NAS)や高負荷なサーバー環境でコピーした瞬間、そのコードは「自動化ツール」から「システム障害の引き金」へと変貌する。
なぜか? 答えは単純だ。`FSO.CopyFile` は「ブラックボックスなアトミック操作」だからだ。
OSレベルで全力のI/Oリソースを使い切り、ディスクIOPS(Input/Output Operations Per Second)をスパイクさせる。その結果、同じストレージを参照している基幹データベースのレスポンスが低下し、システム全体がハングアップする。これが、プロフェッショナルが最も忌み嫌う「リソース競合(Resource Contention)」である。
本稿では、単なるファイルコピーを超えた、「システムの平穏を保ちながら、確実にデータを運ぶ」ためのスロットリング(流量制御)付き転送エンジンの設計思想と実装を伝授する。
1. なぜ「CopyFile」ではいけないのか:設計思想の転換
プロフェッショナルな設計において、重要なのは「目的(ファイルをコピーすること)」だけではない。「副作用(I/O負荷)をどう制御するか」が設計の成否を分ける。
標準的なFSOの限界
- 制御不能なスループット: OSのスケジューラに任せきりであり、スクリプト側から「秒間10MBに抑える」といった制御が一切できない。
- 進捗管理の欠如: コピーが始まってから終わるまで、スクリプトは「待ち」の状態になり、進捗率(%)を算出できない。
- エラーリカバリの困難さ: 巨大なファイルのコピー中にネットワークが瞬断した場合、最初からやり直しとなる。
本稿が提示する「ストリーム・スロットリング」戦略
我々が構築すべきは、ファイルを巨大な塊として扱うのではなく、「小さなチャンク(塊)に分割し、各チャンクの書き込み後に意図的な待機(Sleep)を挟む」という、ストリームベースの制御モデルである。
2. アーキテクチャ・ブループリント
堅牢な転送エンジンを構築するために、以下の3層構造で設計する。
1. I/Oストリーム層: `ADODB.Stream` を活用し、バイナリデータをチャンク単位で読み書きする。
2. スロットリング層: 読み書きしたデータ量に基づき、`WScript.Sleep` を動的に制御する。
3. レジリエンス層: エラーハンドリングとログ出力を分離し、異常時に「どこまで完了したか」を特定可能にする。
3. プロダクション・コード例:ThrottledFileTransferEngine
以下に、実務でそのまま利用可能なレベルまで磨き上げたVBScriptコードを示す。
注意点として、VBScriptのFSO単体ではバイナリの細かなチャンク操作が困難なため、Windows環境で最も信頼性の高い `ADODB.Stream` をエンジンとして採用している。
'==============================================================================
' ThrottledFileTransferEngine (VBScript)
'
' 【概要】
' I/O負荷を制御(スロットリング)しながら大容量ファイルをコピーする。
'
' 【設計思想】
' - ADODB.Streamによるバイナリチャンク転送
' - チャンクごとのSleep制御によるIOPSスパイクの抑制
' - 進捗表示とエラーハンドリングの統合
'==============================================================================
Option Explicit
' --- 設定値 ---
Const CHUNK_SIZE = 1048576 ' チャンクサイズ: 1MB (メモリ負荷とI/O効率のバランス)
Const SLEEP_MS = 50 ' チャンクごとの待機時間 (ミリ秒)
' --------------
Dim objFSO, objShell
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objShell = CreateObject("WScript.Shell")
' 実行テスト
Dim src, dest
src = "C:\Temp\LargeData.zip" ' コピー元
dest = "D:\Backup\LargeData.zip" ' コピー先
Call ExecuteThrottledCopy(src, dest)
'------------------------------------------------------------------------------
' メインルーチン: スロットリング付きコピー実行
'------------------------------------------------------------------------------
Sub ExecuteThrottledCopy(strSrc, strDest)
Dim objSrcStream, objDestStream
Dim lngFileSize, lngBytesRead, lngTotalBytesCopied
Dim lngProgress, strProgressMsg
On Error Resume Next
' 1. 事前チェック
If Not objFSO.FileExists(strSrc) Then
WScript.Echo "Error: Source file not found -> " & strSrc
Exit Sub
End If
lngFileSize = objFSO.GetFile(strSrc).Size
' 2. ストリームの初期化 (バイナリモード)
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
' 書き込み先が存在する場合の処理(新規作成モード)
objDestStream.Open
' 3. 転送ループ
lngTotalBytesCopied = 0
Do Until objSrcStream.EOS
' チャンクの読み込み
' ※ADODB.StreamのReadは、残りのデータがCHUNK_SIZEより小さければその分だけ返す
Dim binChunk
binChunk = objSrcStream.Read(CHUNK_SIZE)
' 書き込み
objDestStream.Write binChunk
' 進捗計算
lngTotalBytesCopied = lngTotalBytesCopied + LenB(binChunk)
lngProgress = (lngTotalBytesCopied / lngFileSize) * 100
' 進捗表示 (コンソール/GUIへのフィードバック)
strProgressMsg = "Progress: " & Int(lngProgress) & "% (" & _
FormatBytes(lngTotalBytesCopied) & " / " & _
FormatBytes(lngFileSize) & ")"
WScript.Echo strProgressMsg ' 実際の実装ではログファイルやDBへ記録推奨
' --- スロットリング制御 ---
' ここで意図的にCPU/IOリソースを解放する
WScript.Sleep SLEEP_MS
' エラーチェック
If Err.Number <> 0 Then
WScript.Echo "Critical Error during transfer: " & Err.Description
Call Cleanup(objSrcStream, objDestStream)
Exit Sub
End If
Loop
' 4. 後処理
objDestStream.SaveToFile strDest, 2 ' adSaveCreateOverWrite
WScript.Echo "Transfer Completed Successfully."
Call Cleanup(objSrcStream, objDestStream)
On Error GoTo 0
End Sub
'------------------------------------------------------------------------------
' ユーティリティ: リソース解放
'------------------------------------------------------------------------------
Sub Cleanup(objSrc, objDest)
On Error Resume Next
If Not objSrc Is Nothing Then
If objSrc.State = 1 Then objSrc.Close
End If
If Not objDest Is Nothing Then
If objDest.State = 1 Then objDest.Close
End If
Set objSrc = Nothing
Set objDest = Nothing
End Sub
'------------------------------------------------------------------------------
' ユーティリティ: バイト数フォーマット
'------------------------------------------------------------------------------
Function FormatBytes(byVal lngBytes)
Dim arrUnits, intUnitIdx
arrUnits = Array("B", "KB", "MB", "GB", "TB")
intUnitIdx = 0
Do While lngBytes >= 1024 And intUnitIdx < UBound(arrUnits)
lngBytes = lngBytes / 1024
intUnitIdx = intUnitIdx + 1
Loop
FormatBytes = Round(lngBytes, 2) & " " & arrUnits(intUnitIdx)
End Function
4. プロフェッショナルが守るべき運用上の注意点
このコードをプロダクション環境に投入する際、以下の3点を必ず考慮せよ。
① チャンクサイズとスリープ時間の「黄金比」
コード内の `CHUNK_SIZE` と `SLEEP_MS` は、環境によって最適解が異なる。
- チャンクが大きすぎる場合: I/Oスパイクが発生し、スロットリングの効果が薄れる。
- チャンクが小さすぎる場合: メソッド呼び出しのオーバーヘッド(CPU負荷)が増大し、転送効率が著しく低下する。
- 推奨: 通常は 1MB〜4MB。ネットワーク越しなら 4MB 程度まで上げても良い。
② データベース連携における「トランザクション」の考え方
もしこのスクリプトが「ファイルの転送完了」をDBに記録する仕様なら、「転送完了」を記録する前に、必ずファイルの整合性チェック(Hash値の確認など)を行うこと。
不完全な転送を「成功」としてDBに書き込むことは、後続のプロセス(集計や解析)をすべて汚染する、エンジニアとしての致命的なミスである。
③ ログ出力の設計(Auditing)
`WScript.Echo` はデバッグ用だ。実務では、以下の情報を構造化ログ(CSVやDB)に記録せよ。
- `Start_Timestamp` / `End_Timestamp`
- `Source_Path` / `Dest_Path`
- `File_Size`
- `Throughput_Avg` (平均スループット)
- `Result_Status` (Success/Fail)
結論
「動けばいい」という考え方は、小規模なスクリプトの世界では通用する。しかし、エンタープライズな業務自動化においては、「システム全体への影響を最小化しながら、いかに堅牢にタスクを遂行するか」こそが、エンジニアの真価を問う指標となる。
FSOの `CopyFile` という甘い誘惑を断ち切り、ストリーム制御による「制御されたI/O」を実装せよ。それが、信頼されるアーキテクトへの第一歩である。
