Wie führe ich meine PowerShell-Skripts parallel aus, ohne Jobs zu verwenden?

29

Wenn ich ein Skript habe, das auf mehreren Computern ausgeführt werden muss, oder mit mehreren unterschiedlichen Argumenten, wie kann ich es parallel ausführen, ohne dass der Aufwand für das ErstellenStart-Job eines neuen PSJob-Objekts anfällt ?

Als Beispiel, möchte ich die Zeit für alle Domain-Mitglieder neu synchronisieren :

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

Ich möchte jedoch nicht darauf warten, dass jede PSSession eine Verbindung herstellt und den Befehl aufruft. Wie geht das parallel ohne Jobs?

Mathias R. Jessen
quelle

Antworten:

51

Update - Während diese Antwort den Prozess und die Funktionsweise von PowerShell-Runspaces erläutert und erläutert, wie sie Ihnen helfen können, nicht sequenzielle Workloads für mehrere Threads zu erstellen, hat ein PowerShell-Fan, Warren 'Cookie Monster' F , die Extrameile auf sich genommen und dieselben Konzepte in ein einziges Tool integriert genannt - es macht das, was ich unten beschreibe, und er hat es seitdem mit optionalen Schaltern für die Protokollierung und den vorbereiteten Sitzungsstatus erweitert, einschließlich importierter Module, wirklich coole Sachen - ich empfehle dringend, dass Sie es ausprobieren, bevor Sie Ihre eigene glänzende Lösung erstellen!Invoke-Parallel


Bei paralleler Ausführung von Runspace:

Verkürzung der unausweichlichen Wartezeit

Im ursprünglichen speziellen Fall hat die aufgerufene ausführbare Datei ein /nowait Option, die verhindert, dass der aufrufende Thread blockiert wird, während der Auftrag (in diesem Fall die erneute Zeitsynchronisierung) von selbst abgeschlossen wird.

Dies reduziert die Gesamtausführungszeit aus Sicht des Emittenten erheblich, die Verbindung zu jedem Computer wird jedoch nacheinander hergestellt. Das Herstellen einer Verbindung zu Tausenden von Clients in Folge kann je nach Anzahl der Computer, auf die aus dem einen oder anderen Grund aufgrund einer Häufung von Wartezeiten aufgrund von Zeitüberschreitungen nicht zugegriffen werden kann, einige Zeit in Anspruch nehmen.

Um zu vermeiden, dass bei einem oder mehreren aufeinanderfolgenden Timeouts alle nachfolgenden Verbindungen in die Warteschlange gestellt werden müssen, können wir den Auftrag zum Verbinden und Aufrufen von Befehlen an separate PowerShell-Runspaces weiterleiten und parallel ausführen.

Was ist ein Runspace?

Ein Runspace ist der virtuelle Container, in dem Ihr Powershell-Code ausgeführt wird und der die Umgebung aus der Perspektive einer PowerShell-Anweisung / eines PowerShell-Befehls darstellt / enthält.

Allgemein ausgedrückt ist 1 Runspace = 1 Ausführungsthread. Alles, was wir zum "Multithreading" unseres PowerShell-Skripts benötigen, ist eine Sammlung von Runspaces, die dann wiederum parallel ausgeführt werden können.

Wie beim ursprünglichen Problem kann der Job zum Aufrufen von Befehlen in mehrere Runspaces unterteilt werden in:

  1. Einen RunspacePool erstellen
  2. Zuweisen eines PowerShell-Skripts oder eines entsprechenden ausführbaren Codes zum RunspacePool
  3. Rufen Sie den Code asynchron auf (dh Sie müssen nicht auf die Rückkehr des Codes warten)

RunspacePool-Vorlage

In PowerShell gibt es einen Typbeschleuniger namens [RunspaceFactory] Typbeschleuniger, der uns bei der Erstellung von Runspace-Komponenten unterstützt

1. Erstellen Sie einen RunspacePool und Open()es:

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

Die beiden Argumente, die an übergeben wurden CreateRunspacePool(), 1und 8sind die minimale und maximale Anzahl von Runspaces, die zu einem bestimmten Zeitpunkt ausgeführt werden dürfen. Dies ergibt einen effektiven maximalen Parallelitätsgrad von 8.

2. Erstellen Sie eine Instanz von PowerShell, hängen Sie einen ausführbaren Code an und weisen Sie ihn unserem RunspacePool zu:

Eine Instanz von PowerShell ist nicht dasselbe wie der powershell.exeProzess (der eigentlich eine Host-Anwendung ist), sondern ein internes Laufzeitobjekt, das den auszuführenden PowerShell-Code darstellt. Mit dem Typbeschleuniger können Sie [powershell]eine neue PowerShell-Instanz in PowerShell erstellen:

$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. Rufen Sie die PowerShell-Instanz asynchron mit APM auf:

Unter Verwendung der in der .NET-Entwicklungsterminologie als asynchrones Programmiermodell bekannten BeginMethode können wir den Aufruf eines Befehls in eine Methode aufteilen , um grünes Licht für die Ausführung des Codes zu geben, und eine EndMethode zum Sammeln der Ergebnisse. Da wir in diesem Fall nicht wirklich an Feedback interessiert sind (wir warten w32tmsowieso nicht auf die Ausgabe von ), können wir dies durch einfachen Aufruf der ersten Methode erreichen

$PSinstance.BeginInvoke()

Einpacken in einen RunspacePool

Mit der obigen Technik können wir die sequentiellen Iterationen des Erstellens neuer Verbindungen und des Aufrufs des Remote-Befehls in einem parallelen Ausführungsablauf umbrechen:

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

Unter der Annahme, dass die CPU die Kapazität hat, alle 8 Runspaces gleichzeitig auszuführen, sollten wir feststellen können, dass sich die Ausführungszeit erheblich verkürzt, jedoch auf Kosten der Lesbarkeit des Skripts aufgrund der eher "fortgeschrittenen" verwendeten Methoden.


Ermittlung des optimalen Parallismusgrades:

Wir könnten leicht einen RunspacePool erstellen, der die gleichzeitige Ausführung von 100 Runspaces ermöglicht:

[runspacefactory]::CreateRunspacePool(1,100)

Letztendlich kommt es jedoch darauf an, wie viele Ausführungseinheiten unsere lokale CPU verarbeiten kann. Mit anderen Worten, solange Ihr Code ausgeführt wird, ist es nicht sinnvoll, mehr Runspaces zuzulassen, als Sie über logische Prozessoren verfügen, an die die Ausführung des Codes gesendet werden kann.

Dank WMI ist dieser Schwellenwert recht einfach zu bestimmen:

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

Wenn andererseits der Code, den Sie selbst ausführen, aufgrund externer Faktoren wie der Netzwerklatenz viel Wartezeit verursacht, können Sie trotzdem davon profitieren, dass mehr Runspaces gleichzeitig ausgeführt werden, als über logische Prozessoren verfügen. Daher möchten Sie wahrscheinlich testen von Bereich möglichen maximalen Runspaces, um die Gewinnschwelle zu finden :

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
quelle
4
Wenn die Jobs im Netzwerk warten, z. B. wenn Sie PowerShell-Befehle auf Remotecomputern ausführen, können Sie die Anzahl der logischen Prozessoren leicht überschreiten, bevor Sie einen CPU-Engpass feststellen.
Michael Hampton
Nun, das stimmt. Änderte es ein bisschen und lieferte ein Beispiel für das Testen
Mathias R. Jessen
Wie kann sichergestellt werden, dass alle Aufgaben am Ende erledigt sind? (
Möglicherweise
@ NickW Gute Frage. Ich werde die Nachverfolgung der Jobs und die "Ernte" des potenziellen Outputs noch heute nachverfolgen. Bleiben Sie dran
Mathias R. Jessen,
1
@ MathiasR.Jessen Sehr gut geschriebene Antwort! Freue mich auf das Update.
Signal15
5

Zusätzlich zu dieser Diskussion fehlt ein Kollektor zum Speichern der aus dem Runspace erstellten Daten und eine Variable zum Überprüfen des Status des Runspace, dh ob dieser abgeschlossen ist oder nicht.

#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
quelle
3

Schauen Sie sich PoshRSJob an . Es bietet dieselben / ähnliche Funktionen wie die nativen * -Job-Funktionen, verwendet jedoch Runspaces, die in der Regel viel schneller und reaktionsschneller sind als die Standard-Powershell-Jobs.

Rosco
quelle
1

@ mathias-r-jessen hat eine großartige Antwort, obwohl ich einige Details hinzufügen möchte.

Max Threads

Theoretisch sollten Threads durch die Anzahl der Systemprozessoren begrenzt werden. Beim Testen von AsyncTcpScan erzielte ich jedoch eine weitaus bessere Leistung, indem ich einen viel größeren Wert für wählte MaxThreads. Warum hat dieses Modul einen -MaxThreadsEingabeparameter? Beachten Sie, dass die Zuweisung zu vieler Threads die Leistung beeinträchtigt.

Daten zurücksenden

Es ScriptBlockist schwierig, Daten von der abzurufen. Ich habe den OP-Code aktualisiert und in das integriert, was für AsyncTcpScan verwendet wurde .

WARNUNG: Ich konnte den folgenden Code nicht testen. Ich habe aufgrund meiner Erfahrung mit den Active Directory-Cmdlets einige Änderungen am OP-Skript vorgenommen.

# 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
quelle