Windowsのスクリプト言語として、もはやなくてはならないPowerShell。普段使っているように、既存のコマンドドレッドを単純に呼び出すといったことだけでも、その便利さは十分体感できるものです。しかし、便利なのはたくさん用意されているコマンドだけではなく、知る人ぞ知る様々な「技」がたくさんあります。今回はその技の1つ、PowerShellの非同期実行の方法をご紹介します。
非同期実行とは?
あれこれ説明するよりも実際に見てみましょう。処理時間のかかる2つのPowerShellのコマンドの後に、テキストファイルを作成するコマンドを実行します。
Get-Service | SELECT *Get-Process | SELECT *New-Item -Path "C:\PSTest\Test.txt" -ItemType File
通常のPowerShellの実行結果からすると、テキストファイルが実際に作成されるのは、時間のかかる二つのコマンドが実行されてから、終了するまでしばらく時間が経過した後となってしまいます。
一方、非同期で実行した場合は、スクリプト実行直後にテキストファイルが作成されます。
これが非同期実行です。同期実行は複数の命令を投げた場合に、1つずつ上から順次に処理して結果が出るまで待機します。終了したら次の命令を実行してゆくことになります。非同期実行はリソースの空いている限り、命令が同時に実行されていきます。
言うまでもありませんが、非同期実行の最大の効果はその処理速度です。最近のハードウェアのリソースは一度に複数の仮想化環境をサポートするくらい高性能なものとなっています。CPU、メモリ、ハードディスク、ネットワークなど、これらのリソースを使うのに、1つずつ仕事をさせるというのではとてももったいない使い方です。ハードウェアの本領が発揮されません。リソースがゆるすかぎりたくさんの仕事をさせてこそ本当の実力が分かります。
また、非同期実行とはいっても、命令の渡し方にしても、CPUだけ、ハードディスクだけといったように一つのリソースに偏るような命令の渡し方ではなく、まんべんなく偏りのない命令の渡し方というのもポイントになります。
PowerShellの非同期実行といっても、方法は一つだけではなく、いくつか方法があります。「バックグランドジョブ」「PowerShell Workflow」「asyncとawait」などがあります。今回は.Net FameworkでPowerShellのために提供されていると言ってもよい「System.Management.Automation.Runspaces Namespace」の「RunspacePoolクラス」を利用したPowerShellの非同期実行の方法を紹介します。RunspacePoolの中で実行されたPowerShellのコマンドは非同期に実行することが可能になります。
非同期実行 PowerShellサンプル
まずは具体的なコードです。
: $PSCmds = @(
2:
3: "Get-Service | SELECT *",
4:
5: "Get-Process | SELECT *",
6:
7: "New-Item -Path 'C:\PSTest\Test.txt' -ItemType File"
8:
9: )
10:
11: $res = AsyncPowershell $PSCmds
12:
13: function AsyncPowershell($Cmds)
14: {
15: try {
16: $MaxRunspace = $Cmds.Length
17: $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $MaxRunspace)
18: $RunspacePool.Open()
19:
20: $aryPowerShell = New-Object System.Collections.ArrayList
21: $aryIAsyncResult = New-Object System.Collections.ArrayList
22: for ( $i = 0; $i -lt $MaxRunspace; $i++ )
23: {
24: $Cmd = $Cmds[$i]
25: $PowerShell = [PowerShell]::Create()
26: $PowerShell.RunspacePool = $RunspacePool
27: $PowerShell.AddScript($Cmd)
28: $PowerShell.AddCommand("Out-String")
29: $IAsyncResult = $PowerShell.BeginInvoke()
30:
31: $aryPowerShell.Add($PowerShell)
32: $aryIAsyncResult.Add($IAsyncResult)
33: }
34:
35: while ( $aryPowerShell.Count -gt 0 )
36: {
37: for ( $i = 0; $i -lt $aryPowerShell.Count; $i++ )
38: {
39: $PowerShell = $aryPowerShell[$i]
40: $IAsyncResult = $aryIAsyncResult[$i]
41:
42: if($PowerShell -ne $null)
43: {
44: if($IAsyncResult.IsCompleted)
45: {
46: $Result = $PowerShell.EndInvoke($IAsyncResult)
47: Write-host $Result
48: $PowerShell.Dispose()
49: $aryPowerShell.RemoveAt($i)
50: $aryIAsyncResult.RemoveAt($i)
51: {break outer}
52: }
53: }
54: }
55: Start-Sleep -Milliseconds 100
56: }
57: } catch [Exception] {
58: Write-Host $_.Exception.Message;
59: } finally {
60: $RunspacePool.Close()
61: }
62: }
上から見ていきましょう。
AsyncPowershellという関数に、配列文字列として複数のPowerShellのコマンドを渡しています。
非同期実行は、RunspacePool を作って、開いて利用します。try~catch finallyで確実に一度開いたRunspacePoolを使い終わったら必ず閉じるようにしておきます。開きっぱなしにしておくと、いつかリソースが枯渇してしまいRunspacePoolを開くことができなくなってしまうのでご注意を。
RunspacePoolを作成します。引数として渡したコマンドの数だけRunspacePoolを作成し$RunspacePoolに格納します。
最小値を1、最大値を実行させたいコマンド数としています。CreateRunspacePool()として、動的に RunspacePoolを作成して実行することもできますが、この場合処理速度がやや遅くなります。また、CreateRunspacePool(1, 1)のように、 RunspacePoolが実行したい命令数より少ない場合は同期処理と同じようにRunspacePoolが空き次第、次の命令を実行ということになります。
非同期処理のメリットを最大限に発揮するには、あらかじめRunspacePoolの数を不足ないように設定して実行する必要があります。
この2つのArrayListは、それぞれのPowerShellコマンドとその実行結果を格納するために定義します。最終的にそれぞれのコマンドが終了次第、随時結果を出力するために利用します。
for{ }で引数で渡されたコマンドを1つずつ実行していきます。コマンドの実行は、System.Management.Automation.PowerShellクラスから行います。このクラスは.Net FrameworkからPowerShellのコマンドを実行するためのクラスです。.Net Frameworkのクラスとして提供されているので、System.Management.Automation.Runspacesクラス同様、もちろんVBやC#からも利用することが可能です。
$PowerShell = [PowerShell]::Create()でPowerShellのオブジェクトを作成します。$PowerShell.RunspacePoolには先に定義した$RunspacePoolを指定します。これによってPowerShellのコマンドがRunspacePool内で非同期に実行されます。
AddScriptで実行するコマンドを指定します。
$PowerShell.AddCommand("Out-String") は実行結果としての戻り値をテキストとして取得できるようにパイプラインとして追加しておきます。
$IAsyncResult = $PowerShell.BeginInvoke() はBeginInvokeでコマンドを実行して、実行結果としてIAsyncResultを取得して$aryIAsyncResultに格納します。
$aryPowerShellの中には実行したPowerShellオブジェクトが格納されています。
このwhileの中で実行したコマンドの結果を取得します。実行結果を取得し次第、$aryPowerShellから1つずつPowerShellオブジェクトを削除してゆき、最終的に$aryPowerShellが空になれば終了となります。 実行結果は非同期実行の場合は順番に取得するのではなく、終了したものから随時取得していきます。$aryIAsyncResultには実行結果が格納されています。
実行されたPowerShellのオブジェクト$PowerShell = $aryPowerShell[$i]ごとに、実行結果$IAsyncResult = $aryIAsyncResult[$i]を確認します。
実行結果を取得するごとに、if($IAsyncResult.IsCompleted)でコマンドが終了したかどうかを判断しています。
コマンドが終了していたら、$Result = $PowerShell.EndInvoke($IAsyncResult)で実行結果を取得して、Write-host $Resultで実行結果を出力します。終了したPowerShellオブジェクトと実行結果オブジェクトは削除して、再度ループに入ります。$aryPowerShellが空になるまで繰り返されます。
大まかな処理の流れです。
1. RunspacePoolの作成2. RunspacePoolのオープン3. RunspacePool内でコマンドを一つずつ実行4.実行したPowerShellと実行結果をオブジェクトとしてArrayListに格納5.コマンドの数だけ繰り返す6.すべてのコマンドが起動完了7.格納したArrayList内のオブジェクトをLoopで一つずつチェック8.コマンドの実行結果がIsCompletedか否かの判断9.コマンドが終了した場合結果を取得10.実行結果を出力11.出力完了したオブジェクとはArrayListから削除12.すべてのコマンドの結果を取得し終わるまで繰り返す13. ArrayListが空になれば終了14. RunspacePoolを閉じる
非同期実行のポイント
RunspacePoolを使ってのPowerShellのコマンド実行は、まず、すべてのコマンドをRunspacePool内で起動さてしまうこと。すべて起動し終わってから、コマンドの実行結果は、Loopで実行状態を確認しつつ、随時取得してゆくという部分になります。
BeginInvokeの直後に、EndInvokeをしてしまうと、同期実行と同様に、一つのコマンドが終了するまで、待機状態になってしまうのでご注意ください。
リモートでの実行
以上、ご紹介したのは、ローカルで実行する場合のサンプルとなります。PowerShellの非同期実行はもちろんリモートからでも可能となっています。
1: $PSCmds = @(
2:
3: "Get-Service | SELECT *",
4:
5: "Get-Process | SELECT *",
6:
7: "New-Item -Path 'C:\PSTest\Test.txt' -ItemType File"
8:
9: )
10:
11: $res = AsyncPowershell $PSCmds "Server01" "Administrator" "PASSWORD"
12:
13: function AsyncPowershell($Cmds, $Server, $AdminAccount, $AdminPassword)
14: {
15: try {
16: $MaxRunspace = $Cmds.Length
17:
18: $SecurePassword = ConvertTo-SecureString $AdminPassword -AsPlainText -Force
19: $Cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $AdminAccount, $SecurePassword;
20: $RunspaceConnectionInfo = New-Object System.Management.Automation.Runspaces.WSManConnectionInfo
21: $RunspaceConnectionInfo.Credential = $Cred
22:
23: $RunspaceConnectionInfo.ComputerName = $Server
24: $RunspacePool = [RunspaceFactory]::CreateRunspacePool(1, $MaxRunspace, $RunspaceConnectionInfo)
25: $RunspacePool.Open()
26:
27: $aryPowerShell = New-Object System.Collections.ArrayList
28: $aryIAsyncResult = New-Object System.Collections.ArrayList
29: for ( $i = 0; $i -lt $MaxRunspace; $i++ )
30: {
31: $Cmd = $Cmds[$i]
32: $PowerShell = [PowerShell]::Create()
33: $PowerShell.RunspacePool = $RunspacePool
34: $PowerShell.AddScript($Cmd)
35: $PowerShell.AddCommand("Out-String")
36: $IAsyncResult = $PowerShell.BeginInvoke()
37:
38: $aryPowerShell.Add($PowerShell)
39: $aryIAsyncResult.Add($IAsyncResult)
40: }
41:
42: while ( $aryPowerShell.Count -gt 0 )
43: {
44: for ( $i = 0; $i -lt $aryPowerShell.Count; $i++ )
45: {
46: $PowerShell = $aryPowerShell[$i]
47: $IAsyncResult = $aryIAsyncResult[$i]
48:
49: if($PowerShell -ne $null)
50: {
51: if($IAsyncResult.IsCompleted)
52: {
53: $Result = $PowerShell.EndInvoke($IAsyncResult)
54: Write-host $Result
55: $PowerShell.Dispose()
56: $aryPowerShell.RemoveAt($i)
57: $aryIAsyncResult.RemoveAt($i)
58: {break outer}
59: }
60: }
61: }
62: Start-Sleep -Milliseconds 100
63: }
64: } catch [Exception] {
65: Write-Host $_.Exception.Message;
66: } finally {
67: $RunspacePool.Close()
68: }
69: }
リモート実行で異なるのは、RunspacePoolに認証情報を追加する部分だけです。
呼び出す関数に、実行対象のサーバー名、アカウント、パスワードの引数を追加しています。
ConvertTo-SecureStringでパスワードをセキュア状態に変更してから、System.Management.Automation.Runspaces.WSManConnectionInfoのConnectionInfoオブジェクトを作成して、Credentialとして引き渡します。 CreateRunspacePoolの第三の引数として、ConnectionInfoオブジェクトを指定して、RunspacePoolを作成してOpenします。
これでリモートでの非同期実行の準備が完了しました。
VB、C#でもPowerShellを非同期実行 ~Hyper-Vプロビジョニング~
先にも少し紹介しましたが、PowerShellの非同期実行は、.Net FrameworkのSystem.Management.Automation.PowerShellクラスとSystem.Management.Automation.Runspaces クラスを利用して実現しています。
従ってVBやC#からもPowerShellのコマンドを非同期で実行することが可能です。
この最大のメリットは、筆者の守備範囲であるHyper-Vのプロビジョニングにおいて発揮されます。Hyper-VのプロビジョニングはすべてWMIをつかってVisualStudioで開発することが可能です。しかし、やはり簡単なPowerShellで、かつ、開発効率のよいVisualStudioを使ってプロビジョニングシステムを開発したいというのは当然の発想といえます。そこで、System.Management.Automation.PowerShellクラスを利用して、VisualStudioでVBやC#からPowerShellのコマンドを実行させます。
さらに、Hype-Vの操作には時間のかかる操作がいくつかあります。たとえば、仮想マシンのエクスポートや、LiveMigrationがこれに該当します。数十ギガにおよぶディスクのコピーが発生する場合は、高速な共通ストレージなどが無い環境では相当な時間を要するものになります。そんなときに、是非おすすめしたいのが、System.Management.Automation.Runspaces クラスを利用したPowerShellの非同期実行なのです。
VBとC#のサンプルも紹介しておきます。時間のかかる仮想マシンのエクスポートと当時に、他の仮想マシンを起動しつつ、Hyper-V上の仮想マシン一覧情報を取得しています。
VB PowerShell非同期実行サンプル
1: '非同期のコマンド指定
2: Dim strAsyncCmds() As String = {"Export-VM ?Name VM01 ?Path D:\Exp", "Start-VM VM02", "Get-VM", "Start-VM VM03"}
3:
4: 'コマンドを実行
5: AsyncPowershell(strServer, strAccount, strPassword, strAsyncCmds)
6:
7: Public Sub AsyncPowershell(ByVal strServer As String, ByVal strAccount As String, ByVal strPassword As String, ByVal strCmds() As String)
8: Dim objRunspacePool As RunspacePool = Nothing
9: Try
10: 'パスワードを暗号化
11: Dim objSecureString As New SecureString()
12: For Each c As String In strPassword.ToCharArray()
13: objSecureString.AppendChar(c)
14: Next
15: Dim objPSCredential As New PSCredential(strAccount, objSecureString)
16:
17: 'WinRMを利用してリモートでPowerShellを実行
18: Dim objUri As New Uri("http://" + strServer + ":5985/wsman")
19: Dim objWSManConnectionInfo As New WSManConnectionInfo(objUri, "http://schemas.microsoft.com/powershell/Microsoft.PowerShell", objPSCredential)
20:
21: Dim intRunspace As Integer = strCmds.Length
22: objRunspacePool = RunspaceFactory.CreateRunspacePool(1, intRunspace, objWSManConnectionInfo)
23: 'objRunspacePool.ApartmentState = Threading.ApartmentState.STA
24: 'objRunspacePool.ThreadOptions = PSThreadOptions.UseNewThread
25: objRunspacePool.Open()
26:
27: Dim aryPowerShell As New ArrayList
28: Dim aryIAsyncResult As New ArrayList
29: Dim strCmd As String = ""
30: For i As Integer = 0 To strCmds.Length - 1
31: strCmd = strCmds(i)
32:
33: Dim objPowerShell As PowerShell = PowerShell.Create()
34: objPowerShell.RunspacePool = objRunspacePool
35: objPowerShell.AddScript(strCmd)
36: objPowerShell.AddCommand("Out-String")
37: 'PowerShellコマンド実行
38: Dim objIAsyncResult As IAsyncResult = objPowerShell.BeginInvoke()
39:
40: aryPowerShell.Add(objPowerShell)
41: aryIAsyncResult.Add(objIAsyncResult)
42: Next
43:
44: Do While (aryPowerShell.Count > 0)
45: For i As Integer = 0 To aryPowerShell.Count - 1
46:
47: Dim objPowerShell As PowerShell = aryPowerShell(i)
48: Dim objIAsyncResult As IAsyncResult = aryIAsyncResult(i)
49:
50: If Not IsNothing(objPowerShell) Then
51: If objIAsyncResult.IsCompleted Then
52: Dim objResultCollection As PSDataCollection(Of PSObject) = objPowerShell.EndInvoke(objIAsyncResult)
53:
54: 'PowerShellコマンドの実行結果を出力
55: For Each objResult As PSObject In objResultCollection
56: Debug.WriteLine(objResult.ToString)
57: Next
58:
59: 'PowerShellのエラーを出力
60: Dim objErrors As Collection(Of ErrorRecord) = objPowerShell.Streams.Error.ReadAll
61: For Each objErrorRecord As ErrorRecord In objErrors
62: Debug.WriteLine(objErrorRecord.Exception.Message)
63: Next
64:
65: objPowerShell.Dispose()
66: aryPowerShell.RemoveAt(i)
67: aryIAsyncResult.RemoveAt(i)
68: Exit For
69: End If
70: End If
71:
72: Next
73: Threading.Thread.Sleep(100)
74: Loop
75: Catch ex As Exception
76: Debug.WriteLine(ex.ToString)
77: Finally
78: objRunspacePool.Close()
79: End Try
80: End Sub
C# PowerShell非同期実行サンプル
1: //非同期のコマンド指定
2: string[] strAsyncCmds = {"Export-VM ?Name VM01 ?Path D:\\Exp", "Start-VM VM02", "Get-VM", "Start-VM VM03"};
3:
4: //コマンドを実行
5: AsyncPowershell(strServer, strAccount, strPassword, strAsyncCmds);
6:
7: static void AsyncPowershell(string strServer, string strAccount, string strPassword, string[] strCmds)
8: {
9: //パスワードを暗号化
10: SecureString objSecureString = new SecureString();
11: foreach (char c in strPassword.ToCharArray()) { objSecureString.AppendChar(c); }
12: PSCredential objPSCredential = new PSCredential(strAccount, objSecureString);
13:
14: //WinRMを利用してリモートでPowerShellを実行
15: Uri objUri = new Uri("http://" + strServer + ":5985/wsman");
16: WSManConnectionInfo objWSManConnectionInfo = new WSManConnectionInfo(objUri, "http://schemas.microsoft.com/powershell/Microsoft.PowerShell", objPSCredential);
17:
18: int intRunspace = strCmds.Length;
19: using (RunspacePool objRunspacePool = RunspaceFactory.CreateRunspacePool(1, intRunspace, objWSManConnectionInfo))
20: {
21: //objRunspacePool.ApartmentState = Threading.ApartmentState.STA
22: //objRunspacePool.ThreadOptions = PSThreadOptions.UseNewThread
23: objRunspacePool.Open();
24:
25: ArrayList aryPowerShell = new ArrayList();
26: ArrayList aryIAsyncResult = new ArrayList();
27: string strCmd = "";
28: for (int i = 0; i <= strCmds.Length - 1; i++)
29: {
30: strCmd = strCmds[i];
31:
32: PowerShell objPowerShell = PowerShell.Create();
33: objPowerShell.RunspacePool = objRunspacePool;
34: objPowerShell.AddScript(strCmd);
35: objPowerShell.AddCommand("Out-String");
36: //PowerShellコマンド実行
37: IAsyncResult objIAsyncResult = objPowerShell.BeginInvoke();
38:
39: aryPowerShell.Add(objPowerShell);
40: aryIAsyncResult.Add(objIAsyncResult);
41: }
42:
43: while ((aryPowerShell.Count > 0))
44: {
45:
46: for (int i = 0; i <= aryPowerShell.Count - 1; i++)
47: {
48: PowerShell objPowerShell = (PowerShell)aryPowerShell[i];
49: IAsyncResult objIAsyncResult = (IAsyncResult)aryIAsyncResult[i];
50:
51: if ((objPowerShell != null))
52: {
53: if (objIAsyncResult.IsCompleted)
54: {
55: PSDataCollection<PSObject> objResultCollection = objPowerShell.EndInvoke(objIAsyncResult);
56:
57: //PowerShellコマンドの実行結果を出力
58: foreach (PSObject objResult in objResultCollection)
59: {
60: Debug.WriteLine(objResult.ToString());
61: }
62:
63: //PowerShellのエラーを出力
64: Collection<ErrorRecord> objErrors = objPowerShell.Streams.Error.ReadAll();
65: foreach (ErrorRecord objErrorRecord in objErrors)
66: {
67: Debug.WriteLine(objErrorRecord.Exception.Message);
68: }
69:
70: objPowerShell.Dispose();
71: aryPowerShell.RemoveAt(i);
72: aryIAsyncResult.RemoveAt(i);
73: break;
74: }
75: }
76: }
77: System.Threading.Thread.Sleep(100);
78: }
79: }
80: }
以上、今回は知る人ぞ知る、PowerShellの「技」の一つをご紹介しました。PowerShellの非同期実行によって、より効率的なサーバー管理などにの役立てていただければ幸いです。
PowerShellとVisualStudioによるHyper-Vプロビジョニングにも是非お試しください。
サンプルコードをこちらからダウンロードいただけます。 →SampleCode.zip(53.6KB)