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

Mat*_*sen 32 automation powershell performance

如果我有一个脚本需要在多台计算机上运行,​​或者有多个不同的参数,我如何并行执行它,而不必产生产生新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 }
}
Run Code Online (Sandbox Code Playgroud)

但我不想等待每个 PSSession 连接并调用命令。如果没有乔布斯,这如何并行完成?

Mat*_*sen 58

更新 - 虽然这个答案解释了 PowerShell 运行空间的过程和机制以及它们如何帮助您处理多线程非顺序工作负载,但 PowerShell 爱好者Warren 'Cookie Monster' F已经加倍努力,将这些相同的概念合并到一个工具中调用 - 它执行我在下面描述的内容,并且他已经使用可选开关扩展它以进行日志记录和准备会话状态,包括导入的模块,非常酷的东西 - 我强烈建议您在构建自己的闪亮解决方案之前检查它Invoke-Parallel


使用并行运行空间执行:

减少不可避免的等待时间

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

从发行人的角度来看,这大大减少了整体执行时间,但连接到每台机器仍然是按顺序完成的。由于超时等待的累积,顺序连接到数千个客户端可能需要很长时间,具体取决于由于某种原因或其他原因无法访问的机器数量。

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

什么是运行空间?

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

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

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

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

运行空间池模板

PowerShell 有一个类型加速器[RunspaceFactory],它可以帮助我们创建运行空间组件——让我们开始使用它

1. 创建一个 RunspacePool 并且Open()它:

$RunspacePool = [runspacefactory]::CreateRunspacePool(1,8)
$RunspacePool.Open()
Run Code Online (Sandbox Code Playgroud)

这两个参数传递给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
Run Code Online (Sandbox Code Playgroud)

3. 使用 APM 异步调用 PowerShell 实例:

使用 .NET 开发术语中已知的异步编程模型,我们可以将命令的调用拆分为一个Begin方法,为执行代码提供“绿灯”,以及End收集结果的方法。由于我们在这种情况下对任何反馈并不真正感兴趣(我们不会等待任何输出w32tm),我们可以通过简单地调用第一个方法来完成

$PSinstance.BeginInvoke()
Run Code Online (Sandbox Code Playgroud)

将其包装在 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()
}
Run Code Online (Sandbox Code Playgroud)

假设 CPU 有能力一次执行所有 8 个运行空间,我们应该能够看到执行时间大大减少,但由于使用了相当“高级”的方法,因此以脚本的可读性为代价。


确定最佳平行度:

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

[runspacefactory]::CreateRunspacePool(1,100)
Run Code Online (Sandbox Code Playgroud)

但归根结底,这一切都取决于我们的本地 CPU 可以处理多少个执行单元。换句话说,只要你的代码正在执行,允许比逻辑处理器更多的运行空间是没有意义的。

多亏了 WMI,这个阈值很容易确定:

$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)
Run Code Online (Sandbox Code Playgroud)

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

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
}
Run Code Online (Sandbox Code Playgroud)

  • 如果作业在网络上等待,例如您在远程计算机上运行 PowerShell 命令,则在遇到任何 CPU 瓶颈之前,您很容易超过逻辑处理器的数量。 (4认同)

小智 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
Run Code Online (Sandbox Code Playgroud)