Frage Wie führe ich meine PowerShell-Skripte parallel ohne Verwendung von Jobs aus?


Wenn ich ein Skript habe, das ich für mehrere Computer oder mit mehreren verschiedenen Argumenten ausführen muss, wie kann ich es parallel ausführen, ohne dass ich den Overhead für das Erstellen eines neuen Skripts übernehmen muss? PSJob mit Start-Job?

Als Beispiel, Ich möchte die Zeit für alle Domänenmitglieder erneut synchronisieren, so:

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

Aber ich möchte nicht auf jede PSSession warten, um eine Verbindung herzustellen und den Befehl aufzurufen. Wie kann das parallel ohne Jobs gemacht werden?


26
2017-09-06 13:56


Ursprung




Antworten:


Aktualisieren  - Während diese Antwort den Prozess und die Funktionsweise von PowerShell-Runspaces erläutert und erläutert, wie sie Ihnen bei der Verarbeitung nicht sequenzieller Workloads mit mehreren Threads helfen können, ist dies ein weiterer PowerShell-Experte Warren 'Krümelmonster' F ist die extra Meile gegangen und hat diese Konzepte in ein einziges Tool namens  Invoke-Parallel  - Es tut, was ich unten beschreibe, und er hat es seitdem mit optionalen Schaltern für die Protokollierung und den vorbereiteten Sitzungsstatus einschließlich importierter Module erweitert, wirklich coole Sachen - ich empfehle Ihnen wärmstens Hör zu bevor Sie Ihre eigene glänzende Lösung aufbauen!


Mit paralleler Ausführung des Runspace:

Unentrinnbare Wartezeit reduzieren

Im ursprünglichen spezifischen Fall hat die aufgerufene ausführbare Datei a /nowait Option, die verhindert, dass der aufrufende Thread blockiert wird, während der Job (in diesem Fall die erneute Synchronisierung der Zeit) von selbst beendet wird.

Dies reduziert die Gesamtausführungszeit aus Sicht des Ausstellers erheblich, aber die Verbindung zu jeder Maschine wird immer noch in sequentieller Reihenfolge hergestellt. Die Verbindung zu Tausenden von Clients in Folge kann je nach Anzahl der Rechner, die aus irgendeinem Grund nicht erreichbar sind, aufgrund einer Häufung von Timeout-Wartezeiten sehr lange dauern.

Um herumzukommen und alle nachfolgenden Verbindungen im Falle eines einzelnen oder einiger aufeinanderfolgender Timeouts anstehen zu müssen, können wir den Befehl zum Verbinden und Aufrufen von Befehlen an separate PowerShell-Runspaces senden, die parallel ausgeführt werden.

Was ist ein Runspace?

EIN Runspace ist der virtuelle Container, in dem der Powershell-Code ausgeführt wird, und stellt die Umgebung aus der Perspektive einer PowerShell-Anweisung / eines PowerShell-Befehls dar.

Grob gesagt, 1 Runspace = 1 Thread der Ausführung, müssen wir also unser PowerShell-Skript nur mit einer Reihe von Runspaces "multi-threaden", die dann wiederum parallel ausgeführt werden können.

Wie beim ursprünglichen Problem kann der Aufruf von Befehlen für mehrere Runspaces unterteilt werden in:

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

RunspacePool Vorlage

PowerShell hat einen Typenbeschleuniger namens [RunspaceFactory] Das wird uns bei der Erstellung von Runspace-Komponenten helfen - lass es uns zum Laufen bringen

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

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

Die zwei Argumente, an die man ging CreateRunspacePool(), 1 und 8 ist die minimale und maximale Anzahl von Runspaces, die zu einem bestimmten Zeitpunkt ausgeführt werden dürfen maximal Grad der Parallelität von 8.

2. Erstellen Sie eine Instanz von PowerShell, fügen Sie ausführbaren Code hinzu und weisen Sie sie unserem RunspacePool zu:

Eine Instanz von PowerShell ist nicht die gleiche wie die powershell.exe Prozess (der in Wirklichkeit eine Host-Anwendung ist), aber ein internes Laufzeitobjekt, das den auszuführenden PowerShell-Code darstellt. Wir können das benutzen [powershell] Geben Sie accelerator ein, um eine neue PowerShell-Instanz in PowerShell zu 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:

Mit dem, was in .NET-Entwicklungsterminologie als bekannt ist Asynchrones Programmiermodell, können wir den Aufruf eines Befehls in einen aufteilen Begin Methode, um ein "grünes Licht" zu geben, um den Code auszuführen, und a End Methode, um die Ergebnisse zu sammeln. Da wir in diesem Fall nicht wirklich an Rückmeldungen interessiert sind (wir warten nicht auf die Ausgabe von w32tm wie auch immer), wir können dafür sorgen, dass wir einfach die erste Methode aufrufen

$PSinstance.BeginInvoke()

Wrapping es in einem RunspacePool

Mithilfe der obigen Technik können wir die sequenziellen Iterationen zum Erstellen neuer Verbindungen und zum Aufrufen des Remote-Befehls in einem parallelen Ausführungsfluss umschließen:

$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 sehen können, dass die Ausführungszeit stark reduziert ist, aber auf Kosten der Lesbarkeit des Skripts aufgrund der eher "fortgeschrittenen" Methoden.


Bestimmung des optimalen Grades des Parallismus:

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

[runspacefactory]::CreateRunspacePool(1,100)

Aber am Ende des Tages kommt es darauf an, wie viele Ausführungseinheiten unsere lokale CPU bewältigen kann. Mit anderen Worten, solange der Code ausgeführt wird, ist es nicht sinnvoll, mehr Runspaces zuzulassen als logische Prozessoren, an die die Ausführung von Code gesendet werden soll.

Dank WMI ist dieser Schwellenwert relativ 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 Netzwerklatenz, viel Wartezeit erfordert, können Sie immer noch mehr simultane Runspaces ausführen als logische Prozessoren, so dass Sie wahrscheinlich testen möchten der Bereich möglich maximale runspaces zu finden die Gewinnzone erreichen:

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
}

45
2017-09-06 13:56



Wenn die Jobs im Netzwerk warten, z. Wenn Sie PowerShell-Befehle auf Remotecomputern ausführen, können Sie leicht die Anzahl der logischen Prozessoren überschreiten, bevor Sie auf einen CPU-Engpass stoßen. - Michael Hampton♦
Nun, das ist wahr. Änderte es ein wenig und lieferte ein Beispiel für das Testen - Mathias R. Jessen
Wie kann man sicherstellen, dass die Arbeit am Ende erledigt ist? (Muss nach Abschluss aller Skriptblöcke etwas benötigt werden) - sjzls
@NickW Große Frage. Ich werde eine Nachverfolgung der Jobs vornehmen und später das Produktionspotenzial "ernten", bleiben Sie dran - Mathias R. Jessen
@ MathiasR.Jessen Sehr gut geschriebene Antwort! Ich freue mich auf das Update. - Signal15


Wenn Sie zu dieser Diskussion hinzufügen, fehlt ein Collector zum Speichern der Daten, die aus dem Runspace erstellt werden, und eine Variable zum Überprüfen des Status des Runspace, d. H. Ob er 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

5
2018-02-09 16:22





Auschecken PoshRSJob. Es bietet dieselben / ähnliche Funktionen wie die nativen * -Job-Funktionen, verwendet jedoch Runspaces, die dazu neigen, viel schneller und reaktionsschneller zu sein als die Standard-Powershell-Jobs.


1
2017-12-06 23:21