PowerShell后台作业提升脚本性能:并行处理与实战指南

本文深入探讨PowerShell后台作业的功能与应用,包括作业类型选择、管理命令使用、数据处理技巧及大规模并发执行策略,帮助管理员优化脚本性能并提升自动化效率。

PowerShell后台作业解锁脚本性能

在当今复杂且不断扩展的IT环境中,没有人有时间等待PowerShell脚本缓慢执行完成,尤其是在时效性至关重要的情况下。作为希望广泛使用PowerShell自动化的管理员,您会希望编写能够轻松处理复杂任务的高级脚本。有时,由于各种原因性能较慢,等待脚本完成几乎违背了编写脚本的初衷。虽然像浏览器标签页一样打开多个PowerShell控制台并不一定不好,但如果您因为脚本速度慢而这样做,那就是一个问题。幸运的是,PowerShell有一个称为后台作业的功能,可以并行运行脚本和命令。这使您能够在后台执行长时间运行的任务,而不影响当前控制台。让我们更深入地了解PowerShell后台作业是什么,如何开始使用和管理它们,以及使用示例。

什么是PowerShell中的作业?

在所有情况下,作业都是PowerShell异步运行的任务,这意味着它运行命令或脚本而不影响提示符的可用性。PowerShell有几种后台作业类型:

  • RemoteJob:通过远程会话运行的命令和脚本。
  • ThreadJob:在与父PowerShell进程相同进程的单独线程中运行的命令和脚本。
  • BackgroundJob:在与父PowerShell进程分开的进程中运行的命令和脚本。

选择ThreadJob和BackgroundJob取决于进程隔离和性能。在同一进程中运行的脚本,即使是不同的线程,也可能影响父进程。如果ThreadJob遇到终止错误,则可能影响父进程。虽然BackgroundJob不是这种情况,但在单独进程中运行确实会增加一些开销,包括对象的处理方式,因为您无法从BackgroundJob接收实时对象。

通常,由于BackgroundJob更全面,它们是推荐的作业类型,并且是本文其余部分所讨论的内容。

管理PowerShell后台作业的基础知识

PowerShell后台作业管理是通过Microsoft.PowerShell.Core插件中的cmdlet完成的。查看可用命令的最简单方法是使用Get-Command查询所有以-Job结尾的命令,使用Format-Table别名以获得更清晰的输出。

1
Get-Command *-Job | ft -a

这是PowerShell 7.5的输出。

这是Windows PowerShell的输出。

仔细检查后,您会发现Resume-Job和Suspend-Job在PowerShell 7.5中不再可用,但本教程提供了适用于Windows PowerShell和PowerShell 7的示例。

PowerShell的一个优点是您可以从cmdlet名称推断它们的功能。想要启动一个作业?让我们试试Start-Job。

1
Start-Job -ScriptBlock {Write-Host 'this command runs in a background job'}

没有Write-Host输出,而是返回一个作业对象。后台作业异步运行,需要特定命令来检查其状态并查找其输出。如果您没有将该命令的输出分配给变量,如本例所示,请注意Id。使用Get-Job查找作业信息。

1
Get-Job -Id 4

现在,您可以看到状态标记为Completed。

要从作业接收数据,请检查HasMoreData属性是否为True,表示有可用输出。使用Receive-Job收集该数据,将输出分配给变量。

1
$jobOutput = Receive-Job -Id 4

在这种情况下,数据只是Write-Host的输出。$jobOutput变量为空。

如果您需要等待作业完成,可以使用哪个命令?让我们试试Wait-Job。

1
Wait-Job -Id 4

PowerShell暂停,直到作业完成。在这种情况下,它立即返回。但是,由于您在PowerShell中,可以使用管道将每个命令的输出传递给下一个命令,以形成一个简洁的一行命令。

1
Start-Job -ScriptBlock {Write-Host 'Pipeline'} | Wait-Job | Receive-Job

当您需要特定作业完成后再执行下一个命令时,Wait-Job cmdlet非常有用。

最后,要删除作业,请使用Remove-Job。

1
Remove-Job -Id 4

任何剩余的作业在PowerShell会话关闭时都会被删除。

后台作业与Start-Process的区别

如果您认为后台作业是Start-Process的一个花哨包装,您并不完全错误。虽然它们都运行与主PowerShell会话分开的进程,但后台作业可以继续与父会话交互以检查状态和执行其他操作。Start-Process没有相同的功能。

复制前面的示例,尝试以下命令——在Windows PowerShell中将pwsh替换为powershell。

1
Start-Process pwsh -ArgumentList '-c Write-Host "this command runs in a background job"'

运行该命令会瞬间打开和关闭PowerShell控制台,没有留下任何是否工作的指示。

使用Start-Process复制后台作业需要大量工作来构建进程的标准输入/输出重定向,然后执行对象序列化以加载回原始父PowerShell进程。所有这些努力都是多余的,以重新创建后台作业中已经存在的功能。当启动GUI应用程序或使用不同凭据运行进程时,您可能更喜欢Start-Process而不是后台作业,以实现与父PowerShell会话的进程隔离。

如何在脚本中使用后台作业

将您的PowerShell代码转换为作为后台作业运行可能需要一些努力。后台作业不提供对父会话的交互式输入。例如,检查以下命令中的作业会给出Blocked状态。

1
Start-Job -ScriptBlock {Read-Host 'Data please: ';Read-Host 'More data: '}

要将数据传递给作业,您可以使用-ArgumentList参数和具有param()块的scriptblock。

1
Start-Job -ScriptBlock {param($string)Write-Host $string} -ArgumentList 'Output from job'

此屏幕截图显示-ArgumentList参数中的字符串输出被传递并由作业处理。

还需要考虑错误处理。如果发生停止错误,作业将失败。例如,如果您运行以下简单脚本,然后查询作业,它会返回Failed状态。

1
2
Start-Job -ScriptBlock {Throw 'error'}
Get-Job -Id 14

因此,处理任何错误并仅在需要时强制脚本停止错误非常重要。

使这个过程 significantly easier的一种方法是在交互式会话中调试脚本,注意输入、输出和错误,然后添加步骤以记录到文件以跟踪执行信息。日志记录可以包括简单的检查点以表示脚本的哪些部分已执行,或者可以采取更全面的方法,记录错误和对象属性。

如何处理PowerShell后台作业的数据

PowerShell不是一种强类型编程语言。这可能不是问题,但在不同的执行上下文中工作时需要注意这一点。从后台作业返回的对象经过序列化和反序列化过程,这会影响功能。

从后台作业接收数据几乎与从PowerShell中的任何其他cmdlet接收数据一样容易。使用Receive-Job收集数据输出。例如,以下PowerShell命令获取文件对象。

1
Start-Job -ScriptBlock {Get-Item C:\tmp\test.txt} | Wait-Job | Receive-Job

返回的对象看起来像合法的FileInfo对象,但使用以下代码仔细检查类型。

1
2
$file = Start-Job -ScriptBlock {Get-Item C:\tmp\test.txt} | Wait-Job | Receive-Job
$file.GetType()

如果您在交互式会话中执行相同的操作,则会获得预期的类型。

1
2
$file = Get-Item C:\tmp\test.txt
$file.GetType()

结果不同,因为PowerShell序列化对象,然后在从后台作业返回时反序列化对象。属性或数据被保留,但方法——使用括号调用的操作或函数——不可用。

例如,这些命令在交互式会话中有效,但如果您从作业接收FileInfo对象,则无效,如此处所示。

后台作业非常适合与仅数据对象(如PSCustomObject)一起使用。

如何扩展PowerShell后台作业

虽然在后台运行单个PowerShell任务很有帮助,但真正的好处是通过同时运行多个后台作业来实现的,尤其是在时间紧迫的情况下。

例如,也许您需要尽快在所有服务器上运行gpupdate以应用组策略更改。使用foreach循环且没有后台作业,此任务在每个服务器上顺序运行,如以下PowerShell脚本所示。

1
2
3
4
5
6
$servers = Get-ADComputer -filter {OperatingSystem -like "*Windows Server*"}
foreach ($server in $servers) {
Invoke-Command -ComputerName $Server.Name -ScriptBlock {
gpupdate.exe /force
}
}

该脚本顺序处理每个服务器,在移动到下一个服务器之前在一个服务器上执行更新。对于拥有数百或数千台服务器的企业,此过程可能需要很长时间。

但是,使用后台作业,PowerShell可以快速完成此操作。

第一个想法可能是在Invoke-Command上使用-AsJob参数,最终得到以下结果。

1
2
3
4
5
6
$servers = Get-ADComputer -filter {OperatingSystem -like "*Windows Server*"}
foreach ($server in $servers) {
Invoke-Command -ComputerName $Server.Name -AsJob -ScriptBlock {
gpupdate.exe /force
}
}

由于作业开销和服务器数量的原因,此脚本可能会使本地机器过载。您可以通过使用以下命令监视正在运行的作业数量来管理此问题:

1
(Get-Job -State Running).Count

可以同时运行的作业数量因主机系统而异,但对于像这样的简单脚本,一个好的起点可能是20。让我们相应地更新脚本,如果计数为20或更多,则让其睡眠五秒钟,以防止系统过载。

1
2
3
4
5
6
7
8
9
$servers = Get-ADComputer -filter {OperatingSystem -like "*Windows Server*"}
foreach ($server in $servers) {
while ((Get-Job -State "Running").Count -ge 20) {
Start-Sleep -Seconds 5
}
Invoke-Command -ComputerName $Server.Name -AsJob -ScriptBlock {
gpupdate.exe /force
}
}

现在,您有一个解决方案,可以快速且可扩展的方式将组策略更改应用于AD中的所有服务器。在您的环境中运行这样的脚本之前,请正确获取服务器来源。例如,您可能有一个组织单位,可以与-SearchBase参数一起使用以针对特定服务器而不是所有服务器。

请注意,使用带有-AsJob参数的Invoke-Command在技术上启动的是RemoteJob,而不是BackgroundJob。但是,RemoteJob的处理方式与BackgroundJob相同,因此Get-Job、Wait-Job和Receive-Job仍然适用于此方法。

如何使用后台作业大规模处理数据

运行许多作业后,跟踪每个作业的状态可能令人生畏,尤其是在前面的示例中,脚本在AD中的每个服务器上运行命令。要按作业跟踪状态,您可以通过多种方式为每个作业指定有意义的名称:

  • Start-Job中的-Name参数。
  • Invoke-Command中的-JobName参数。
  • 作业本身的Location属性。

如果您正在运行一组针对特定资源、文件共享中的文件夹或网络上的IP地址的作业,则可以使用Start-Job中的-Name参数跟踪该信息。

1
2
3
4
5
6
7
$fileShares = Get-Content C:\temp\fileshares.txt
foreach ($share in $fileShares) {
Start-Job -Name $share -ScriptBlock {
param($share)
# 用$share做点什么
} -ArgumentList $share
}

运行Get-Job显示每个作业的共享名称。

或者,如果您正在使用RemoteJob,如gpupdate示例中,您可以使用-JobName参数或Location属性。例如,在远程系统上运行作业时,运行以下PowerShell命令后,Location属性中会出现远程主机的名称。

1
Invoke-Command -AsJob -ScriptBlock {write-host 'yes'} -ComputerName dc.domain.local

这使您能够轻松地以可扩展的方式运行每个作业状态的报告,例如以下示例。

1
2
3
4
5
6
7
8
9
$report = while ((Get-Job -HasMoreData $true).Count -gt 0) {
foreach ($job in (Get-Job -HasMoreData $true)) {
[pscustomobject]@{
Share = $job.Name
JobState = $job.State
Data = Receive-Job -Job $job
}
}
}

如果您有长时间运行的作业,则可能需要等待它们完成后再运行报告。

Anthony Howell是一位IT策略师,在基础设施和自动化技术方面拥有丰富经验。他的专业知识包括PowerShell、DevOps、云计算以及在Windows和Linux环境中工作。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计