Обновление. Несмотря на то, что в этом ответе объясняется процесс и механизмы пространств выполнения PowerShell и как они могут помочь вам в многопоточных непоследовательных рабочих нагрузках, коллега по PowerShell Уоррен 'Cookie Monster' F приложил дополнительные усилия и соединил эти же концепции в одном инструменте. звонил - он делает то, что я описываю ниже, и с тех пор он расширил его дополнительными переключателями для регистрации и подготовленного состояния сеанса, включая импортированные модули, действительно классные вещи - я настоятельно рекомендую вам проверить это перед созданием собственного блестящего решения!Invoke-Parallel
С параллельным выполнением Runspace:
Сокращение неизбежного времени ожидания
В исходном конкретном случае вызываемый исполняемый файл имеет /nowait
параметр, который предотвращает блокировку вызывающего потока, пока задание (в данном случае повторная синхронизация времени) завершается само по себе.
Это значительно сокращает общее время выполнения с точки зрения эмитентов, но подключение к каждой машине все еще выполняется в последовательном порядке. Последовательное подключение к тысячам клиентов может занять много времени в зависимости от количества машин, которые по тем или иным причинам недоступны из-за накопления времени ожидания.
Чтобы обойти необходимость ставить в очередь все последующие соединения в случае одного или нескольких последовательных тайм-аутов, мы можем направить работу по подключению и вызову команд в отдельные пространства выполнения PowerShell, выполняя их параллельно.
Что такое Runspace?
Пространство выполнения представляет собой виртуальный контейнер , в котором ваш код выполняется PowerShell, и представляет / держит среды с точки зрения заявления / команды PowerShell.
В общих чертах, 1 Runspace = 1 поток выполнения, поэтому все, что нам нужно для «многопоточности» нашего скрипта PowerShell, - это набор Runspaces, которые затем могут выполняться параллельно.
Как и в случае с исходной проблемой, задачу вызова команд из нескольких пространств выполнения можно разбить на:
- Создание RunspacePool
- Назначение сценария PowerShell или эквивалентного фрагмента исполняемого кода для RunspacePool
- Вызвать код асинхронно (т.е. не нужно ждать возврата кода)
Шаблон 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. Асинхронно вызовите экземпляр 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
}