Як паралельно запускати сценарії 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 любитель Warren 'Cookie Monster' F пройшов додаткову милю і включив ці самі поняття в єдиний інструмент зателефонував - він робить те, що я описую нижче, і з тих пір він розширив його додатковими перемикачами для реєстрації та підготовленим станом сеансу, включаючи імпортні модулі, дуже класні речі - настійно рекомендую перевірити це перед тим , як створити власне блискуче рішення!Invoke-Parallel


Із паралельним виконанням Runspace:

Скорочення неминучого часу очікування

У первинному конкретному випадку викликаний виконуваний файл має /nowaitопцію, яка запобігає блокуванню потоку виклику, поки завдання (у цьому випадку повторна синхронізація) закінчується самостійно.

Це значно скорочує загальний час виконання з точки зору емітентів, але підключення до кожної машини все ще відбувається в послідовному порядку. Підключення до тисяч клієнтів послідовно може зайняти багато часу, залежно від кількості машин, які з тих чи інших причин є недоступними, через накопичення часу очікування.

Щоб обійти необхідність встановлення черги на всі наступні з'єднання у випадку одного або декількох послідовних тайм-аутів, ми можемо відправити завдання з підключення та виклику команд для відділення PowerShell Runspaces, виконуючи паралельно.

Що таке пробірка?

Простір виконання являє собою віртуальний контейнер , в якому ваш код виконується 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 Чудове запитання. Я відстежуватиму роботу з відстеження робочих місць та "збирання" потенційного випуску пізніше сьогодні,
Mathias R. Jessen

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 функції, але використовує Runspaces, які, як правило, набагато швидше і більш чуйні, ніж стандартні завдання Powershell.


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.