Как запустить сценарии 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 подключится и вызовет команду. Как это можно сделать параллельно, без Джобса?

Матиас Р. Ессен
источник

Ответы:

51

Обновление. Несмотря на то, что в этом ответе объясняется процесс и механизмы пространств выполнения PowerShell и как они могут помочь вам в многопоточных непоследовательных рабочих нагрузках, коллега по PowerShell Уоррен 'Cookie Monster' F приложил дополнительные усилия и соединил эти же концепции в одном инструменте. звонил - он делает то, что я описываю ниже, и с тех пор он расширил его дополнительными переключателями для регистрации и подготовленного состояния сеанса, включая импортированные модули, действительно классные вещи - я настоятельно рекомендую вам проверить это перед созданием собственного блестящего решения!Invoke-Parallel


С параллельным выполнением Runspace:

Сокращение неизбежного времени ожидания

В исходном конкретном случае вызываемый исполняемый файл имеет /nowaitпараметр, который предотвращает блокировку вызывающего потока, пока задание (в данном случае повторная синхронизация времени) завершается само по себе.

Это значительно сокращает общее время выполнения с точки зрения эмитентов, но подключение к каждой машине все еще выполняется в последовательном порядке. Последовательное подключение к тысячам клиентов может занять много времени в зависимости от количества машин, которые по тем или иным причинам недоступны из-за накопления времени ожидания.

Чтобы обойти необходимость ставить в очередь все последующие соединения в случае одного или нескольких последовательных тайм-аутов, мы можем направить работу по подключению и вызову команд в отдельные пространства выполнения PowerShell, выполняя их параллельно.

Что такое Runspace?

Пространство выполнения представляет собой виртуальный контейнер , в котором ваш код выполняется PowerShell, и представляет / держит среды с точки зрения заявления / команды PowerShell.

В общих чертах, 1 Runspace = 1 поток выполнения, поэтому все, что нам нужно для «многопоточности» нашего скрипта PowerShell, - это набор Runspaces, которые затем могут выполняться параллельно.

Как и в случае с исходной проблемой, задачу вызова команд из нескольких пространств выполнения можно разбить на:

  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. Асинхронно вызовите экземпляр 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
}
Матиас Р. Ессен
источник
4
Если задания ожидают в сети, например, вы выполняете команды PowerShell на удаленных компьютерах, вы можете легко преодолеть количество логических процессоров, прежде чем столкнетесь с каким-либо узким местом ЦП.
Майкл Хэмптон
Ну, это правда. Немного изменил его и предоставил пример для тестирования
Матиас Р. Джессен
Как убедиться, что вся работа выполнена в конце? (Может понадобиться что-нибудь после того, как все блоки скриптов закончены)
sjzls
@NickW Отличный вопрос. Я сделаю последующие действия по отслеживанию рабочих мест и «получению» потенциальной продукции позже сегодня, следите за обновлениями
Матиас Р. Джессен
1
@ MathiasR.Jessen Очень хорошо написанный ответ! С нетерпением жду обновления.
Сигнал15,
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, но использует пространства выполнения, которые, как правило, работают намного быстрее и быстрее, чем стандартные задания Powershell.

Rosco
источник
1

У @ 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
phbits
источник