Powershell und parallele Verarbeitung

Die meisten Powershell-Skripte laufen einfach seriell ab. Das ist einfach, überschaubar aber nicht unbedingt sehr schnell. Viele Anfragen, z.B. an Exchange benötigen doch etwas länger und in der Zwischenzeit wartet der Prozess. Auch der Vergleich von zwei LDAP-Verzeichnissen kann schneller erfolgen, wenn der Export parallelisiert wird und die Daten am Ende zusammengeführt werden. Windows selbst ist schon lange ein "Multitasking"-System, auch wenn viele UNIX-Administratoren gerne daran rummäkeln. In den Anfangszeiten hat die Instanzierung eines Prozesse manchmal schon etwas länger gedauert.

Die Powershell selbst bietet mehrere Optionen Prozesse ein Stück weit parallel ablaufen zu lassen. Um die Funktion und Leistung an einem Beispiel zu zeigen, habe ich mir zwei Prozesse ausgedacht, die einfach "langsam" sind.

{start-sleep 1 ; write-host "Modul1:"$_; $_}

{start-sleep 5; write-host "Modul2:"$_}

Beide machen also nichts anderes, als die Information, die Sie per Pipeline erhalten haben, nach einiger Verzögerung weiter zu geben. Diese beiden Prozessen werden einfach miteinander auf die verschiedenen Weisen verbunden. Als Eingabe dient einfach eine Zahlenreihe von 1 bis 9. Die kann in Powershell auch einfach generiert werden.

1..9

Für die verschiedenen Alternativen gibt es schon mal eine kurze Vergleichstabelle:

Funktion Sequentiell Pipeline Job Process
Schwierigkeit Leicht Leicht Mittel Mittel
Geschwindigkeit
Messwert des Beispiels
Sehr Langsam
60Sek
Langsam
51Sek
Schnell
15Sek
nicht gemessen
Abhängigkeit vom Vater entfällt Ja Ja, Jobs werden beendet, wenn Vater endet Nein
Parameterübergabe Variable Pipeline Beim Aufruf per Parameter Beim Aufruf per Parameter
Ergebnisrückgabe
Wie können Werte an den Prozess übergeben und erhalten werden
Variable oder Pipeline Pipeline Abruf der Pipeline über Receive-Job. Auch während der Laufzeit möglich  
Jobkontrolle Entfällt Entfällt Get-Job Get-Process
Start-Transcript-Support Ja, aber nur global Ja, aber nur global Nein Ja, pro Prozess
Laufzeitumgebung, d.h. Zugriff auf im Vater definierte Variablen und geladene Snapins und Addons Entfällt Vererbt "Privater Raum". Module etc. müssen selbst nachgeladen werden. komplett eigenständig zu verwalten
Berechtigung Gleich Gleich Credentials optional Credentials optional
Synchronisation
Wie können Jobs untereinander kommunizieren ?
Entfällt Pipeline oneway Selbst zu entwickeln Selbst zu entwickeln
Throtting
Wie kann man die Anzahl der Jobs begrenzen
Entfällt, nur 1 aktiver Teil Entfällt, nur 1 aktiver Teil Manuell Manuell

Schauen wir uns die einzelnen Optionen der Verarbeitung an.

Sequentielle Verarbeitung

Fangen wir einfach mit der einfachsten Verarbeitung in Bausteinen ab.

# Liste von Eingabeobjekten erstellen
$liste1 = 1..10

# Eingabeobjekte im ersten Modul verarbeiten und in Liste2 speichern
$liste2 = foreach ($item in $liste1){
    start-sleep 1 ; write-host "Modul1:"$item; $item}

# Liste2 als Eingabe in Modul2 verarbeiten
foreach ($item in $liste2){
    start-sleep 5 ; write-host "Modul2:"$item}

# Ausgabe erfolgt an der Console

Die erste Verarbeitung benötigt 10 Sekunden um dann in Modul2 mit 50 Sekunden zu schlafen.

Insgesamt kommen also knapp 60 Sekunden Laufzeit zusammen. Man sieht auch bei der Bildschirmausgabe, dass der das Modul1 natürlich alles verarbeitet haben muss, ehe das langsamere Modul2 überhaupt erst anfangen kann.

Verketten mit der Pipeline

Der erste und sicher den meisten Personen bekannte Weg ist die Nutzung einer Pipeline, bei der die Ausgaben eines Prozesses an einen anderen Prozess übergeben werden. Auch wenn die Abarbeitung sequentiell erfolgt, da der nachgeschaltete Prozess auf die Daten des Vorgängers warten muss, ist die Verarbeitung schon etwas effektiver.

1..10 | `
    %{start-sleep 1 ; write-host "Modul1:"$_; $_} | `
        %{start-sleep 5; write-host "Modul2:"$_}

Einen Geschwindigkeitsrekord erreichen wir damit natürlich auch nicht, aber Modul2 kann zumindest schon nach einer Sekunden anfangen, zu verarbeiten. Leider wartet Modul2 so lange.

Statt 60 Sekunden sollte dieses Skript in ca. 51 Sekunden abgeschlossen sein. Die Bildschirmausgabe zeigt auch, dass die Queue an sich sequentiell bleibt und das erste Modul erst weiter machen kann, wenn das zweite Modul seine Ausgabe erledigt hat.

Es ist also nicht so, dass Modul1 seine Ergebnisse nach 10 Sekunden in eine Pipeline geworfen hat und dann pausiert, sondern jedes Element wandert bei dem Beispiel durch die Pipeline durch.

Die Verkettung von Prozessen per Pipeline bringt im Bezug auf die Laufzeit keine Vorteile, wohl aber im Bereich der Speicherbelastung, da eine Zwischenspeicherung nicht mehr erforderlich ist.

Start-Job

Wenn Sie nun aber auf der einen Seite Daten relativ schnell ermitteln und die langsamere Nachverarbeitung parallel erfolgen kann, dann ist Start-Job ein mittel der Wahl. Über das "Start-Job"-Commandlet kann einfach ein Skriptblock in den "Hintergrund" gesendet werden. Über GET-Job lässt sich der Status des Jobs abfrage und über "Receive-Job" kann das Ergebnis des Jobs (Pipelineausgabe) einfach wieder abgerufen werden.

# Start einer Ausgabe
PS C:\> start-job {write-host "test";"pipetest"}

Id Name State HasMoreData Location
-- ---- ----- ----------- --------
11 Job11 Running True localhost

PS C:\> Get-Job

Id Name State HasMoreData Location
-- ---- ----- ----------- --------
11 Job11 Completed True localhost

PS C:\>$a= receive-Job 11
test

PS C:\>$a
pipetest

PS C:\> Remove-Job 11

Ausgaben der Jobs mit "Write-Host" oder der Pipeline landen aber nicht im Fenster des aufrufenden Programms, sondern müssen mit "Receive-Job" abgeholt werden.

Achtung:
Sowohl Ausgaben mit "Write-Host" als auch direkt als STDOUT erscheinen bei einem einfachen "Receive-Job" vermengt. Bei einer Zuweisung an eine Variable werden aber nur die Daten von STDOUT kopiert.

Der Anruf leert dann aber die bis dahin angefallene Warteschlange, wenn Sie dies nicht mit "-keep" unterbinden. Interessant bei Start-Job ist die Option, alternative Credentials anzugeben. Im Taskmanager sehen Sie die Jobs als eigene Prozesse unter dem aufrufenden Prozess.

Zurück zu unserem Performance Beispiel als "Job"

# Modul2 als Job parallel starten

1..10 | `
    %{start-sleep 1 ; write-host "Modul1:"$_; $_} | `
        %{$object = $_ ;
          write-host "StartJob: $object"
          start-job `
             -scriptblock {
                    start-sleep 5;
                    $data = $input;
                    write-host "Modul2:$data";
                    $data
                    } `
             -Inputobject $object
          }
wait-job -state running
get-job | receive-job

Hier dauert es zwar nun wieder simulierte 10 Sekunden aber die Jobs laufen parallel mit ab und da der letzte Job bei Sekunde 10 startet und 5 Sekunden läuft, ist das ganze System nach 15 Sekunden "durch".

Ein Start-Job kann direkt einen Skript-Block enthalten oder ein eigenständige Powershell-Skript starten. Es ist nicht möglich, als Bestandteil eines Jobs auf eine Funktion im gleichen Skript zu verweisen. Der Job hat also keine "Kopie" der Laufzeitumgebung.

Wird der Vaterprozess geschlossen, dann werden damit auch alle Jobs entfernt. Da Jobs nach dem Start nicht mehr direkt gesteuert werden können, eignen Sie sich für klar umrissene Aufgaben, die auch ein Ende haben und weniger für Langläufer.

Übrigens können Sie keine "Powershell Events" nutzen, um zwischen Jobs der gleichen Instanz zu kommunizieren. Events können selbst aber Jobs starten. Auf der anderen Seite können Sie in einem Job aber kein "Start-Transcript" nutzen, um die Ausgaben zu protokollieren.

-AsJob

Eine Sonderform von Jobs verbirgt sich hinter der Option "-AsJob", den verschiedene Commandlets zulassen. Damit ist es möglich, diesen Befehl per Powershell Remoting auf einem anderen Computer (oder natürlich auch wieder lokal) zu starten und damit zu parallelisieren. Allerdings ist das kein normaler "Job" mehr, sondern eher ein Prozess.

Prozess

Bei einem Prozess wird nicht ein Job innerhalb bzw. genauer unterhalb der startenden Umgebung aufgemacht, sondern eine ganz eigene Instanz eines Prozesses gestartet. Das muss kein Powershell-Script sein. Das kann ein beliebiger ausführbarer Code sein, also auch eine EXE oder BAT-Datei. Es kann sogar ein Dokument sein, zu dem es eine Programmverknüpfung gibt. So kann durch den Aufruf einer DOC-Datei z.B. Word gestartet werden und über den Parameter "-verb" kann sogar z.B. "Print" ausgewählt werden.

Diese Prozesse laufen eigenständig ab. Sie müssen sich also auch eine entsprechende "Umgebung" für ihre Aktionen schaffen. Sie können keine Module oder Variablen des aufrufenden Programme nutzen. Auch gibt es nicht wirklich eine einfache Schnittstelle zur Übergabe und Rückerhalt von Daten.

Ein Prozess kann natürlich über Kommandozeilen Informationen erhalten, aber strukturierte Daten muss man notfalls über Dateien oder andere Wege der Kommunikation zwischen Prozessen austauschen. Es ist aber möglich, zumindest STDIN, STDOUT und STDERR über Dateien umzuleiten. Das ist aber natürlich ein Rückschritt zur objektorientierten Übergabe in einer reinen Powershell-Umgebung. Import-CliXML und Export-CliXML können helfen, strukturierte Daten bis zu bestimmten Größen bereitzustellen.

Diese Unabhängigkeit bietet aber auch den Weg, alternative Anmeldedaten zu verwenden. Zudem ist der gestartete Prozess nicht vom Vater abhängig, d.h. läuft auch weiter, wenn der startende Prozess nicht mehr aktiv ist. Es gibt mehrere Wege einen Prozess zu starten:

invoke-command -scriptblock {write-host "hallo"}

start-process notepad.exe

$child = [system.diagnostics.process]::Start("powershell", "-file c:\temp\test.ps1 -param1 value1")
Get-process -id $child.id

Für diese Funktion habe ich keine Last/Performance-Tests gemacht, da die Anforderungen hierzu doch eher individuell sind und eine 10fach durchlaufende Schleife den PC nicht wirklich stresst. Das Instanzieren von 10 weiteren Prozessen aber schon eher, so dass die gemessenen Zeiten nicht gültig wären.

Eigene Prozesse sind eher etwas, wenn es um die Verteilung auf verschiedene Systeme oder direkt die entfernte Ausführung geht oder ein Nachgeschalteter Prozess gestartet werden soll, ohne das der Vaterprozess lange aktiv bleiben muss.

Ein Einsatzfeld könnte die Aktivierung von Powershell-Skripten per Taskplaner sein, welcher eiben geplanten Prozess immer nur einmal startet. Hier könnte ein kleiner "Starter" das eigentliche Modul als Prozess starten und sich selbst wieder beenden, damit der Taskplaner ihn erneut aufwecken kann. Dies kann aber auch ganz schnell zu Überlastsituationen führen.

Throtting und Synchronisation

Wer parallel mehrere Aufgaben abarbeiten lässt, muss ich um mindestens zwei Dinge sorgen.

Ganz allgemein sollten sie schon überlegen, wo eine parallele Verarbeitung heute wirklich sinnvoll ist. Erst mit den Jobs oder Prozessen ist eine merkliche Verbesserung möglich.

Weitere Links

Keywords:Powershell Parallel