PowerShell: 複数の異なるタスクを並行稼働させる
要旨
Windows Server から複数のサーバに同時に問い合わせ、応答データを集計して出力したい作業がある。Linux では & でバックグラウンドに回したり、GNU Parallel を使ったりするのだが、似たようなことが PowerShell でもできる。
利用には若干の工夫が必要なためメモしておく。
サンプルスクリプト(変更前)
下記のように複数のサーバに ssh で問い合わせ、結果を表示するようなスクリプトを想定する。
# サーバ A からデータ取得
function Get-ServerDataA {
param (
[string]$arg1 = "",
[string]$arg2 = ""
)
$res = & ssh [email protected] batch1.sh $arg1 $arg2
return $res
}
# サーバ B からデータ取得
function Get-ServerDataB {
param (
[string]$arg1 = "",
[string]$arg2 = ""
)
$res = & ssh [email protected] batch2.sh $arg1 $arg2
return $res
}
# 順次実行する
$resultA = Get-ServerDataA "ABC" "DEF"
$resultB = Get-ServerDataB "PQR" "XYZ"
# 取得結果を出力
Write-Host $resultA
Write-Host $resultB
Get-ServerDataA と Get-ServerDataB の応答がそれぞれ 30秒, 40秒かかるとしたら、単純計算で 70秒は掛かることになる。PowerShell 側ではただ待っているだけなので、この2つの呼び出しを並列化し、すべて完了したら結果を表示したい。並列化の想定通りであれば、遅い方の時間 40秒+αで終わるはず。
サンプルスクリプト(並列化)
上記スクリプトを並列化する。
## function をスクリプトブロックに変換し変数に入れる
# サーバ A からデータ取得
$function_Get_ServerDataA = {
param (
[string]$arg1 = "",
[string]$arg2 = ""
)
$res = & ssh [email protected] batch1.sh $arg1 $arg2
return $res
}
# サーバ B からデータ取得
$function_Get_ServerDataB = {
param (
[string]$arg1 = "",
[string]$arg2 = ""
)
$res = & ssh [email protected] batch2.sh $arg1 $arg2
return $res
}
# バックグラウンドジョブで並列実行
$jobA = Start-Job -ScriptBlock $function_Get_ServerDataA -ArgumentList "ABC", "DEF"
$jobB = Start-Job -ScriptBlock $function_Get_ServerDataB -ArgumentList "PQR", "XYZ"
# 全ジョブの終了を待つ
Get-Job | Wait-Job | Out-Null
# 各ジョブの結果を取り出す
$resultA = Receive-Job $jobA
$resultB = Receive-Job $jobB
# 終了しただけ残っているジョブを削除しておく
Get-Job | Remove-Job
# 取得結果を出力
Write-Host $resultA
Write-Host $resultB
ポイントは下記の通り。
- ジョブ管理系の *-Job コマンドレットを使用する
- 実行させたい処理単位をスクリプトブロックとして定義し、変数に入れておく
- バックグラウンド内のジョブは、外部で定義した function やグローバル変数が (普通には) 参照できないため、引数として渡す
- Start-Job で並列化したい分だけ実行を掛け、Wait-Job で全ジョブ終了を待つ。
- Recieve-Job で、各 JOB 応答オブジェクトから結果を取り出す。
- 各ジョブは実行後も消えずに残っているので、後処理として Remove-Job ですべて削除する。
共通の関数を複数のジョブで使いたいときは、function 定義自体をスクリプトブロックとして 変数に入れ、Start-Job に -InitializationScript オプションで渡す等の工夫が必要。
検証
上記のようなコード例は実機で並行稼働できることを実証済みだが、ピンとこない方のために手元で確認できるコードを書いてみた。Windows 10 標準の PowerShell 5.1 および、最新の PowerShell 7 の双方で動作確認してある。
サーバAの処理が10秒、サーバBの処理が5秒と想定し、Sleep で代用する。
順次実行: Test-Sequential.ps1
# サーバ A からデータ取得
function Get-ServerDataA {
param ([string]$arg1 = "", [string]$arg2 = "")
Start-Sleep -s 10
$res = "SERVER A: ${arg1}-${arg2}"
return $res
}
# サーバ B からデータ取得
function Get-ServerDataB {
param ([string]$arg1 = "", [string]$arg2 = "")
Start-Sleep -s 5
$res = "SERVER B: ${arg1}-${arg2}"
return $res
}
# 順次実行する
$resultA = Get-ServerDataA "ABC" "DEF"
$resultB = Get-ServerDataB "PQR" "XYZ"
# 取得結果を出力
Write-Host $resultA
Write-Host $resultB
並列実行: Test-Parallel.ps1
# サーバ A からデータ取得
$func_Get_ServerDataA = {
param ([string]$arg1 = "", [string]$arg2 = "")
Start-Sleep -s 10
$res = "SERVER A: ${arg1}-${arg2}"
return $res
}
# サーバ B からデータ取得
$func_Get_ServerDataB = {
param ([string]$arg1 = "", [string]$arg2 = "")
Start-Sleep -s 5
$res = "SERVER B: ${arg1}-${arg2}"
return $res
}
# 並列実行+終了待ち
$jobA = Start-Job -ScriptBlock $func_Get_ServerDataA -ArgumentList "ABC","DEF"
$jobB = Start-Job -ScriptBlock $func_Get_ServerDataB -ArgumentList "PQR","XYZ"
Get-Job | Wait-Job | Out-Null
# 結果取得+JOB後片付け
$resultA = Receive-Job $jobA
$resultB = Receive-Job $jobB
Get-Job | Remove-Job
# 取得結果を出力
Write-Host $resultA
Write-Host $resultB
速度比較
実行時間を測定できる Measure-Command コマンドレットを利用し、秒数のみ表示してみる。
(1) 順次実行バージョン
PS C:\> Measure-Command { .\Test-Sequential.ps1 } | Format-List TotalSeconds
SERVER A: ABC-DEF
SERVER B: PQR-XYZ
TotalSeconds : 15.024752
(2) 並列実行化
PS C:\> Measure-Command { .\Test-Parallel.ps1 } | Format-List TotalSeconds
SERVER A: ABC-DEF
SERVER B: PQR-XYZ
TotalSeconds : 10.1969016
想定通り、長く掛かる方のジョブの時間の約 10 秒で完了している。
このコードではエラーハンドリングは行っていない。Wait-Job にはタイムアウトオプションもあるため、複数のジョブのうち1つが失敗したら結果を表示せず、エラー終了する処理を入れるべきだと思う。