Как запустить сценарии PowerShell параллельно без использования заданий?
Если у меня есть сценарий, который мне нужно запустить на нескольких компьютерах, или с несколькими различными аргументами, как я могу выполнить его параллельно, без дополнительных затрат на создание новой PSJob с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 подключится и вызовет команду. Как это можно сделать параллельно, без Джобса?
4 ответа
Обновление. Хотя в этом ответе объясняется процесс и механизмы пространств выполнения PowerShell и как они могут помочь вам в многопоточных непоследовательных рабочих нагрузках, сторонник PowerShell Уоррен 'Cookie Monster' F приложил дополнительные усилия и объединил эти же концепции в одном инструменте. называется Invoke-Parallel
- он делает то, что я опишу ниже, и с тех пор он расширил его дополнительными переключателями для регистрации и подготовленного состояния сеанса, включая импортированные модули, действительно классные вещи - я настоятельно рекомендую вам проверить это перед созданием собственного блестящего решения!
С параллельным выполнением Runspace:
Сокращение неизбежного времени ожидания
В исходном конкретном случае вызываемый исполняемый файл имеет /nowait
опция, которая предотвращает блокировку вызывающего потока, пока задание (в данном случае, повторная синхронизация времени) заканчивается самостоятельно.
Это значительно сокращает общее время выполнения с точки зрения эмитентов, но подключение к каждой машине все еще выполняется в последовательном порядке. Последовательное подключение к тысячам клиентов может занять много времени в зависимости от количества машин, которые по тем или иным причинам недоступны из-за накопления времени ожидания.
Чтобы обойти необходимость ставить в очередь все последующие соединения в случае одного или нескольких последовательных тайм-аутов, мы можем направить работу по подключению и вызову команд в отдельные пространства выполнения PowerShell, выполняя их параллельно.
Что такое Runspace?
Runspace - это виртуальный контейнер, в котором выполняется код powershell, и который представляет / содержит среду с точки зрения оператора / команды PowerShell.
В общих чертах, 1 Runspace = 1 поток выполнения, поэтому все, что нам нужно для "многопоточности" нашего скрипта PowerShell, - это набор Runspaces, которые затем могут выполняться параллельно.
Как и в случае с исходной проблемой, задачу вызова команд из нескольких пространств выполнения можно разбить на:
- Создание RunspacePool
- Назначение сценария PowerShell или эквивалентного фрагмента исполняемого кода для RunspacePool
- Вызвать код асинхронно (т.е. не нужно ждать возврата кода)
Шаблон RunspacePool
PowerShell имеет ускоритель типов [RunspaceFactory]
это поможет нам в создании компонентов runspace - давайте начнем работать
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. Асинхронно вызовите экземпляр PowerShell, используя APM:
Используя то, что известно в терминологии разработки.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()
}
Предполагая, что ЦП способен выполнять все 8 пространств выполнения одновременно, мы должны увидеть, что время выполнения значительно сокращается, но за счет читабельности сценария из-за довольно "продвинутых" используемых методов.
Определение оптимальной степени параллизма:
Мы могли бы легко создать RunspacePool, который позволяет одновременно выполнять 100 пространств выполнения:
[runspacefactory]::CreateRunspacePool(1,100)
Но в конечном итоге все сводится к тому, сколько единиц исполнения может обрабатывать наш локальный процессор. Другими словами, до тех пор, пока выполняется ваш код, не имеет смысла разрешать больше пространств выполнения, чем у вас есть логические процессоры для отправки выполнения кода.
Благодаря 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
}
В дополнение к этому обсуждению отсутствует сборщик для хранения данных, созданных из пространства выполнения, и переменная для проверки состояния пространства выполнения, т. Е. Заполнено оно или нет.
#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
Проверьте PoshRSJob. Он предоставляет те же / аналогичные функции, что и нативные функции *-Job, но использует пространства выполнения, которые, как правило, работают намного быстрее и быстрее, чем стандартные задания Powershell.
У @mathias-r-jessen отличный ответ, но я хотел бы добавить некоторые детали.
Макс потоков
В теории потоки должны быть ограничены количеством системных процессоров. Однако во время тестирования AsyncTcpScan я достиг гораздо лучшей производительности, выбрав гораздо большее значение для MaxThreads
, Таким образом, почему этот модуль имеет -MaxThreads
входной параметр. Имейте в виду, что выделение слишком большого количества потоков снизит производительность.
Возвращение данных
Получение данных от ScriptBlock
это сложно. Я обновил код OP и интегрировал его в то, что использовалось для AsyncTcpScan.
ВНИМАНИЕ: я не смог протестировать следующий код. Я внес некоторые изменения в сценарий OP, основываясь на своем опыте работы с командлетами Active Directory.
# 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