Jak uruchomić równolegle moje skrypty PowerShell bez korzystania z Zadań?

29

Jeśli mam skrypt, który muszę uruchomić na wielu komputerach lub z wieloma różnymi argumentami, jak mogę go wykonać równolegle, bez konieczności ponoszenia kosztów związanych z odrodzeniemStart-Job nowego PSJob ?

Na przykład chcę ponownie zsynchronizować czas na wszystkich członkach domeny , na przykład:

$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 }
}

Ale nie chcę czekać na połączenie każdej sesji PSS i wywołanie polecenia. Jak można to zrobić równolegle, bez Jobsa?

Mathias R. Jessen
źródło

Odpowiedzi:

51

Aktualizacja - Podczas gdy ta odpowiedź wyjaśnia proces i mechanikę przestrzeni roboczych PowerShell oraz sposób, w jaki mogą one pomóc w wielowątkowym niesekwencyjnym obciążeniom, kolega z PowerShell, Warren „Cookie Monster” F , posunął się o krok dalej i włączył te same koncepcje w jedno narzędzie o nazwie - robi to, co opisuję poniżej, i od tego czasu rozszerzył go o opcjonalne przełączniki do rejestrowania i stanu przygotowanej sesji, w tym zaimportowane moduły, naprawdę fajne rzeczy - zdecydowanie zalecamy sprawdzenie tego przed zbudowaniem własnego błyszczącego rozwiązania!Invoke-Parallel


Z równoległym wykonywaniem Runspace:

Skrócenie nieuniknionego czasu oczekiwania

W oryginalnym konkretnym przypadku wywoływany plik wykonywalny ma /nowaitopcję, która zapobiega blokowaniu wątku wywołującego, gdy zadanie (w tym przypadku synchronizacja czasu) kończy się samo.

To znacznie skraca całkowity czas wykonania z punktu widzenia emitentów, ale połączenie z każdą maszyną wciąż odbywa się w kolejności sekwencyjnej. Łączenie się z tysiącami klientów w sekwencji może zająć dużo czasu w zależności od liczby maszyn, które z tego lub innego powodu są niedostępne z powodu kumulacji czasu oczekiwania.

Aby obejść się w kolejce wszystkich kolejnych połączeń w przypadku pojedynczego lub kilku kolejnych przekroczeń czasu, możemy wysłać zadanie łączenia i wywoływania poleceń w celu oddzielenia obszarów roboczych programu PowerShell, wykonując je równolegle.

Co to jest Runspace?

Obszar roboczy to wirtualny kontener, w którym wykonuje się kod programu PowerShell, i reprezentuje / przechowuje środowisko z perspektywy instrukcji / polecenia PowerShell.

W szerokim ujęciu, 1 Runspace = 1 wątek wykonania, więc wszystko, czego potrzebujemy do „wielowątkowego” naszego skryptu PowerShell, to zbiór Runspaces, które mogą z kolei być wykonywane równolegle.

Podobnie jak w przypadku pierwotnego problemu, zadanie wywoływania poleceń wielu obszarów roboczych można podzielić na:

  1. Tworzenie RunspacePool
  2. Przypisywanie skryptu PowerShell lub równoważnego fragmentu kodu wykonywalnego do RunspacePool
  3. Wywołaj kod asynchronicznie (tzn. Nie musisz czekać na zwrócenie kodu)

Szablon RunspacePool

PowerShell ma akcelerator typu o nazwie [RunspaceFactory], który pomoże nam w tworzeniu komponentów obszaru roboczego - uruchommy go

1. Utwórz RunspacePool i Open()to:

$RunspacePool = [runspacefactory]::CreateRunspacePool(1,8)
$RunspacePool.Open()

Dwa argumenty przekazane do CreateRunspacePool(), 1i 8to minimalna i maksymalna liczba obszarów roboczych, które mogą zostać wykonane w danym momencie, co daje nam efektywny maksymalny stopień równoległości równy 8.

2. Utwórz instancję PowerShell, dołącz do niej kod wykonywalny i przypisz go do naszego RunspacePool:

Wystąpienie programu PowerShell nie jest tym samym co powershell.exeproces (który tak naprawdę jest aplikacją hosta), ale wewnętrzny obiekt wykonawczy reprezentujący kod programu PowerShell do wykonania. Możemy użyć [powershell]akceleratora typu, aby utworzyć nową instancję PowerShell w 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. Wywołaj instancję PowerShell asynchronicznie przy użyciu APM:

Używając tego, co znane jest w terminologii programistycznej .NET jako Asynchronous Programming Model , możemy podzielić wywołanie polecenia na Beginmetodę, aby dać „zielone światło” do wykonania kodu i Endmetodę zbierania wyników. Ponieważ w tym przypadku tak naprawdę nie jesteśmy zainteresowani żadnymi opiniami (i tak nie czekamy na dane wyjściowe w32tm), możemy to zrobić, po prostu wywołując pierwszą metodę

$PSinstance.BeginInvoke()

Podsumowanie w RunspacePool

Używając powyższej techniki, możemy owinąć sekwencyjne iteracje tworzenia nowych połączeń i wywoływania polecenia zdalnego w równoległym przepływie wykonania:

$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()
}

Zakładając, że procesor ma zdolność do wykonywania wszystkich 8 przestrzeni roboczych jednocześnie, powinniśmy być w stanie zauważyć, że czas wykonania jest znacznie skrócony, ale kosztem czytelności skryptu ze względu na raczej „zaawansowane” metody.


Określenie optymalnego stopnia paraliżu:

Możemy łatwo stworzyć RunspacePool, który pozwala na wykonanie 100 przestrzeni roboczych jednocześnie:

[runspacefactory]::CreateRunspacePool(1,100)

Ale pod koniec dnia wszystko sprowadza się do liczby jednostek wykonawczych, które może obsłużyć nasz lokalny procesor. Innymi słowy, dopóki twój kod jest wykonywany, nie ma sensu zezwalać na więcej przestrzeni roboczych niż na procesory logiczne, do których można wysłać wykonanie kodu.

Dzięki WMI ten próg jest dość łatwy do ustalenia:

$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)

Jeśli z drugiej strony sam kod, który sam wykonujesz, wymaga dużo czasu oczekiwania z powodu czynników zewnętrznych, takich jak opóźnienie sieci, nadal możesz korzystać z większej liczby jednoczesnych przestrzeni roboczych niż z procesorów logicznych, więc prawdopodobnie powinieneś przetestować zakresu możliwych maksymalnych przestrzeni roboczych w celu znalezienia progu rentowności :

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
}
Mathias R. Jessen
źródło
4
Jeśli zadania czekają w sieci, np. Uruchamiasz polecenia PowerShell na zdalnych komputerach, możesz z łatwością przekroczyć liczbę procesorów logicznych, zanim trafisz na jakiekolwiek wąskie gardło procesora.
Michael Hampton
To prawda. Trochę go zmieniłem i podałem przykład do testowania
Mathias R. Jessen
Jak upewnić się, że cała praca została wykonana na końcu? (Może trzeba coś zrobić po zakończeniu wszystkich bloków skryptu)
sjzls
@NickW Świetne pytanie. Później zajmę się śledzeniem zleceń i „zbieraniem” potencjalnej produkcji, bądźcie czujni
Mathias R. Jessen,
1
@ MathiasR.Jessen Bardzo dobrze napisana odpowiedź! Czekamy na aktualizację.
Signal15
5

Dodając do tej dyskusji, brakuje kolektora do przechowywania danych utworzonych z obszaru roboczego oraz zmiennej do sprawdzania statusu obszaru roboczego, tj. Czy jest on ukończony, czy nie.

#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
Nate Stone
źródło
3

Sprawdź PoshRSJob . Zapewnia takie same / podobne funkcje jak natywne funkcje * -Job, ale używa Runspaces, które wydają się być znacznie szybsze i bardziej responsywne niż standardowe zadania PowerShell.

Rosco
źródło
1

@ mathias-r-jessen ma świetną odpowiedź, choć są pewne szczegóły, które chciałbym dodać.

Maksymalna liczba wątków

Teoretycznie wątki powinny być ograniczone liczbą procesorów systemowych. Jednak podczas testowania AsyncTcpScan osiągnąłem znacznie lepszą wydajność, wybierając znacznie większą wartość MaxThreads. Dlatego ten moduł ma -MaxThreadsparametr wejściowy. Pamiętaj, że przydzielenie zbyt wielu wątków obniży wydajność.

Zwracane dane

Odzyskiwanie danych ScriptBlockjest trudne. Zaktualizowałem kod OP i zintegrowałem go z tym, co było używane w AsyncTcpScan .

OSTRZEŻENIE: Nie byłem w stanie przetestować następującego kodu. Wprowadziłem pewne zmiany w skrypcie OP na podstawie mojego doświadczenia w pracy z poleceniami cmdlet 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
źródło