如何在不使用Jobs的情况下并行运行PowerShell脚本?


29

如果我有一个脚本需要在多台计算机上运行,​​或者需要使用多个不同的参数运行,那么我该如何并行执行该脚本,而又不会产生使用生成新PSJobStart-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连接并调用命令。没有乔布斯,如何并行完成此任务?

Answers:


51

更新 -虽然这个答案说明了过程和PowerShell运行空间的机制,以及他们如何能帮助你的多线程非连续工作负载,同胞的PowerShell爱好者股神“饼干怪兽” F已经加倍努力,并纳入这些相同的概念到一个单一的工具称为 -它会执行我在下面描述的操作,自那以后,他用可选的开关进行了扩展,用于记录日志并准备了会话状态,包括导入的模块,非常酷的东西-我强烈建议您在构建自己的闪亮解决方案之前先进行检查Invoke-Parallel


使用并行运行空间执行:

减少不可避免的等待时间

在原始的特定情况下,调用的可执行文件具有一个/nowait选项,该选项可防止在作业(在这种情况下为时间重新同步)自行完成时阻止调用线程。

从发行者的角度来看,这大大减少了总体执行时间,但是连接到每台机器仍然是按顺序进行的。顺序连接到数千个客户端可能要花费很长时间,具体取决于由于超时等待的累积而由于某种原因或另一种原因而无法访问的计算机的数量。

为了避免在单个或几个连续超时的情况下必须将所有后续连接排队,我们可以将连接和调用命令的工作分派到单独的PowerShell Runspace,并并行执行。

什么是运行空间?

运行空间是在其中执行Powershell代码的虚拟容器,并从PowerShell语句/命令的角度表示/保存环境。

广义上讲,1个Runspace = 1个执行线程,因此我们需要对PowerShell脚本进行“多线程”操作的是Runspace的集合,然后可以依次并行执行。

像原始问题一样,调用命令多个运行空间的工作可以分解为:

  1. 创建一个RunspacePool
  2. 将PowerShell脚本或等效的可执行代码段分配给RunspacePool
  3. 异步调用代码(即不必等待代码返回)

RunspacePool模板

PowerShell有一个称为类型加速器的类型加速器[RunspaceFactory],它将帮助我们创建运行空间组件-让我们开始使用它

1.创建一个RunspacePool并执行以下操作Open()

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

这两个参数传递给CreateRunspacePool()1而且8是最小的,并在任何给定的时间执行的运行空间的最大数量,给我们一个有效的最大并行度的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个运行空间的能力,我们应该能够看到执行时间大大减少,但是由于使用了相当“先进”的方法,因此以脚本的可读性为代价。


确定最佳并行度:

我们可以轻松地创建一个RunspacePool,以允许同时执行100个运行空间:

[runspacefactory]::CreateRunspacePool(1,100)

但是归根结底,这取决于我们本地CPU可以处理多少个执行单元。换句话说,只要您的代码正在执行,那么允许的运行空间就比逻辑处理器可将代码执行分派到的运行空间要多。

由于有了WMI,可以很容易地确定此阈值:

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

另一方面,如果由于网络延迟等外部因素导致您正在执行的代码本身导致大量等待时间,那么您仍然可以通过运行比逻辑处理器更多的同时运行空间而受益,因此您可能需要进行测试可能的最大运行空间范围以找到收支平衡

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
}

4
如果作业正在网络上等待,例如您正在远程计算机上运行PowerShell命令,则在遇到任何CPU瓶颈之前,您可以轻松地超出逻辑处理器的数量。
迈克尔·汉普顿

好吧,那是真的。对其进行了一些更改,并提供了测试示例
Mathias R. Jessen 2014年

如何确保所有工作都在最后完成?(在所有脚本块完成后可能需要执行某些操作)
sjzls

@NickW很好的问题。我将在今天晚些时候跟踪工作并“收获”潜在产出,敬请期待
Mathias R. Jessen 2014年

1
@ MathiasR.Jessen答案非常好!期待更新。
2014年

5

在此讨论之外,缺少的是一个收集器,用于存储从运行空间创建的数据,以及一个变量来检查运行空间的状态,即是否已完成。

#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

3

查看PoshRSJob。它提供与本机* -Job函数相同/相似的功能,但使用Runspaces,与标准Powershell作业相比,Runspaces往往更快,响应更快。


1

@ mathias-r-jessen有一个很好的答案,尽管我想补充一些细节。

最大线程

从理论上讲,线程应受系统处理器数量的限制。但是,在测试AsyncTcpScan时,我通过为选择更大的值获得了更好的性能MaxThreads。因此,该模块为何具有-MaxThreads输入参数。请记住,分配过多的线程会影响性能。

返回数据

从中获取数据ScriptBlock非常棘手。我已经更新了OP代码并将其集成到用于AsyncTcpScan的代码中

警告:我无法测试以下代码。根据对Active Directory cmdlet的使用经验,对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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.