web-dev-qa-db-ja.com

ジョブを使用せずにPowerShellスクリプトを並行して実行するにはどうすればよいですか?

複数のコンピューターに対して、または複数の異なる引数を使用して実行する必要があるスクリプトがある場合、新しい PSJob with Start-Job

例として、 すべてのドメインメンバーの時刻を再同期したい のようにします。

$computers = Get-ADComputer -filter * |Select-Object -ExpandProperty dnsHostName
$creds = Get-Credential domain\user
foreach($computer in $computers)
{
    $session = New-PSSession -ComputerName $computer -Credential $creds
    Invoke-Command -Session $session -ScriptBlock { w32tm /resync /nowait /rediscover }
}

しかし、各PSSessionが接続してコマンドを呼び出すのを待ちたくありません。ジョブなしでこれをどのように並行して行うことができますか?

29

Update-この回答では、PowerShellランスペースのプロセスとメカニズム、およびマルチスレッド非シーケンシャルを支援する方法について説明しますワークロード、PowerShell愛好家仲間 Warren 'Cookie Monster' F はさらに進んで、これらの同じ概念を_Invoke-Parallel_ という単一のツールに組み込んだ-それは以下で説明することを行います、そして彼はそれ以降、ロギングのためのオプションのスイッチとインポートされたモジュールを含むセッション状態を準備してそれを拡張しました-本当にクールなもの-私はあなたに強くお勧めします それをチェックしてください あなた自身の光沢のあるソリューションを構築する前に!


並列ランスペース実行の場合:

避けられない待ち時間を減らす

元の特定のケースでは、呼び出された実行可能ファイルに_/nowait_オプションがあり、ジョブ(この場合、時刻の再同期)がそれ自体で終了する間、呼び出しスレッドをブロックしません。

これにより、発行者の観点から全体的な実行時間が大幅に短縮されますが、各マシンへの接続は引き続き順番に行われます。数千のクライアントに順番に接続すると、タイムアウトの待機が累積するため、何らかの理由でアクセスできないマシンの数によっては、長い時間がかかる場合があります。

1回または数回の連続したタイムアウトが発生した場合に後続のすべての接続をキューに入れる必要を回避するために、コマンドを接続して呼び出すジョブを個別のPowerShell Runspaceにディスパッチして、並列に実行できます。

ランスペースとは何ですか?

Runspace は、Powershellコードが実行される仮想コンテナーであり、PowerShellステートメント/コマンドの観点から環境を表します/保持します。

大まかに言うと、1 Runspace = 1スレッドの実行なので、PowerShellスクリプトを「マルチスレッド化」するために必要なのは、Runspaceのコレクションであり、次に、Runspaceを並行して実行できます。

元の問題と同様に、コマンドを呼び出すジョブは複数のランスペースに分解できます。

  1. RunspacePoolの作成
  2. PowerShellスクリプトまたは同等の実行可能コードをRunspacePoolに割り当てる
  3. 非同期でコードを呼び出す(つまり、コードが戻るのを待つ必要がない)

RunspacePoolテンプレート

PowerShellには _[RunspaceFactory]_ と呼ばれる型アクセラレータがあり、ランスペースコンポーネントの作成に役立ちます-機能させましょう

1. RunspacePoolとOpen() itを作成します。

_$RunspacePool = [runspacefactory]::CreateRunspacePool(1,8)
$RunspacePool.Open()
_

CreateRunspacePool()、_1_、および_8_に渡される2つの引数は、任意の時点で実行できる実行空間の最小数と最大数であり、有効な最大 並列度of 8。

2. PowerShellのインスタンスを作成し、それに実行可能コードを添付して、RunspacePoolに割り当てます。

PowerShellのインスタンスは、_powershell.exe_プロセス(実際にはホストアプリケーション)とは異なりますが、実行するPowerShellコードを表す内部ランタイムオブジェクトです。 _[powershell]_タイプアクセラレータを使用して、PowerShell内に新しいPowerShellインスタンスを作成できます。

_$Code = {
    param($Credentials,$ComputerName)
    $session = New-PSSession -ComputerName $ComputerName -Credential $Credentials
    Invoke-Command -Session $session -ScriptBlock {w32tm /resync /nowait /rediscover}
}
$PSinstance = [powershell]::Create().AddScript($Code).AddArgument($creds).AddArgument("computer1.domain.tld")
$PSinstance.RunspacePool = $RunspacePool
_

3. APMを使用してPowerShellインスタンスを非同期で呼び出します。

.NET開発用語で 非同期プログラミングモデル として知られているものを使用して、コマンドの呼び出しをBeginメソッドに分割し、「青信号」を実行してコード、および結果を収集するEndメソッド。この場合、私たちはフィードバックに本当に興味がないので(とにかく_w32tm_からの出力を待ちません)、最初のメソッドを呼び出すだけで正当な結果を得ることができます

_$PSinstance.BeginInvoke()
_

RunspacePoolにまとめる

上記の手法を使用して、新しい接続の作成とリモートコマンドの呼び出しの順次反復を並列実行フローでラップできます。

_$ComputerNames = Get-ADComputer -filter * -Properties dnsHostName |select -Expand dnsHostName

$Code = {
    param($Credentials,$ComputerName)
    $session = New-PSSession -ComputerName $ComputerName -Credential $Credentials
    Invoke-Command -Session $session -ScriptBlock {w32tm /resync /nowait /rediscover}
}

$creds = Get-Credential domain\user

$rsPool = [runspacefactory]::CreateRunspacePool(1,8)
$rsPool.Open()

foreach($ComputerName in $ComputerNames)
{
    $PSinstance = [powershell]::Create().AddScript($Code).AddArgument($creds).AddArgument($ComputerName)
    $PSinstance.RunspacePool = $rsPool
    $PSinstance.BeginInvoke()
}
_

CPUに8つのランスペースすべてを一度に実行する能力があると仮定すると、実行時間は大幅に短縮されますが、使用される「高度な」メソッドにより、スクリプトが読みやすくなります。


最適な視差の程度を決定する:

同時に100の実行空間を実行できるRunspacePoolを簡単に作成できます。

_[runspacefactory]::CreateRunspacePool(1,100)
_

しかし結局のところ、それはすべて、ローカルCPUが処理できる実行ユニットの数にかかっています。つまり、コードが実行されている限り、コードの実行をディスパッチする論理プロセッサよりも多くのランスペースを許可しても意味がありません。

WMIのおかげで、このしきい値はかなり簡単に決定できます。

_$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)
_

一方、実行しているコード自体がネットワーク遅延などの外部要因により待機時間が長くなる場合でも、論理プロセッサよりも多くのランスペースを同時に実行することでメリットを得られるため、おそらくテストすることをお勧めします。見つけることができる範囲の可能な最大ランスペースbreak-even

_foreach($n in ($NumberOfLogicalProcessors..($NumberOfLogicalProcessors*3)))
{
    Write-Host "$n: " -NoNewLine
    (Measure-Command {
        $Computers = Get-ADComputer -filter * -Properties dnsHostName |select -Expand dnsHostName -First 100
        ...
        [runspacefactory]::CreateRunspacePool(1,$n)
        ...
    }).TotalSeconds
}
_
52

この説明に加えて、不足しているのは、ランスペースから作成されたデータを格納するコレクターと、ランスペースのステータスをチェックする変数です。つまり、完了したかどうかです。

#Add an collector object that will store the data
$Object = New-Object 'System.Management.Automation.PSDataCollection[psobject]'

#Create a variable to check the status
$Handle = $PSinstance.BeginInvoke($Object,$Object)

#So if you want to check the status simply type:
$Handle

#If you want to see the data collected, type:
$Object
5
Nate Stone

チェックアウト PoshRSJob 。ネイティブの* -Job関数と同じ/類似の関数を提供しますが、標準のPowershellジョブよりもはるかに高速で応答性が高い傾向があるRunspaceを使用します。

3
Rosco

@ mathias-r-jessenにはすばらしい answer がありますが、追加したい詳細があります。

最大スレッド

理論的には、スレッドはシステムプロセッサの数によって制限されます。ただし、テスト中に AsyncTcpScanMaxThreadsにはるかに大きな値を選択することで、パフォーマンスが大幅に向上しました。したがって、そのモジュールに-MaxThreads入力パラメータ。割り当てるスレッドが多すぎると、パフォーマンスが低下することに注意してください。

データを返す

ScriptBlockからデータを取得するのは難しいです。 OPコードを更新し、 AsyncTcpScan で使用したものに統合しました。

警告:次のコードをテストできませんでした。 Active Directoryコマンドレットを使用した経験に基づいて、OPスクリプトにいくつかの変更を加えました。

# Script to run in each thread.
[System.Management.Automation.ScriptBlock]$ScriptBlock = {

    $result = New-Object PSObject -Property @{ 'Computer' = $args[0];
                                               'Success'  = $false; }

    try {
            $session = New-PSSession -ComputerName $args[0] -Credential $args[1]
            Invoke-Command -Session $session -ScriptBlock { w32tm /resync /nowait /rediscover }
            Disconnect-PSSession -Session $session
            $result.Success = $true
    } catch {

    }

    return $result

} # End Scriptblock

function Invoke-AsyncJob
{
    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true)]
        [System.Management.Automation.PSCredential]
        # Credential object to login to remote systems
        $Credentials
    )

    Import-Module ActiveDirectory

    $Results = @()

    $AllJobs = New-Object System.Collections.ArrayList

    $AllDomainComputers = Get-ADComputer -Filter * -Properties dnsHostName

    $HostRunspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(2,10,$Host)

    $HostRunspacePool.Open()

    foreach($DomainComputer in $AllDomainComputers)
    {
        $asyncJob = [System.Management.Automation.PowerShell]::Create().AddScript($ScriptBlock).AddParameters($($($DomainComputer.dnsName),$Credentials))

        $asyncJob.RunspacePool = $HostRunspacePool

        $asyncJobObj = @{ JobHandle   = $asyncJob;
                          AsyncHandle = $asyncJob.BeginInvoke()    }

        $AllJobs.Add($asyncJobObj) | Out-Null
    }

    $ProcessingJobs = $true

    Do {

        $CompletedJobs = $AllJobs | Where-Object { $_.AsyncHandle.IsCompleted }

        if($null -ne $CompletedJobs)
        {
            foreach($job in $CompletedJobs)
            {
                $result = $job.JobHandle.EndInvoke($job.AsyncHandle)

                if($null -ne $result)
                {
                    $Results += $result
                }

                $job.JobHandle.Dispose()

                $AllJobs.Remove($job)
            } 

        } else {

            if($AllJobs.Count -eq 0)
            {
                $ProcessingJobs = $false

            } else {

                Start-Sleep -Milliseconds 500
            }
        }

    } While ($ProcessingJobs)

    $HostRunspacePool.Close()
    $HostRunspacePool.Dispose()

    return $Results

} # End function Invoke-AsyncJob
1
phbits