PS Distributed

Irgendwie hat es mich an meine Diplomarbeit erinnert. Auf der Seite PS Parallel habe ich verschiedene Ansätze einer parallelen Verarbeitung durch Jobs etc. beschrieben: Diesmal hat es aber nicht gereicht, die Befehle auf einem PC auszuführen oder mit Start-Job zu starten. Eine echte Verteilung der Aufträge war gefordert um die Performance zu steigern. Zudem sollte der Abbruch eines Skripts die anderen nicht beschädigen. Bei meine Diplomarbeiten habe ich damals noch "Mandelbrotmengen" verteilt berechnet, die auf damaligem 286/386-Systemen noch lange gedauert haben. Ein Steuerungs-Programm hat die "Aufträge auf eine Novell-Queue gelegt und die >200 PCs der Firma haben nachts dann die Aufträge abgearbeitet und die Ergebnisse zurück gemeldet. Nicht viel anders funktioniert ja auch das Seti@Home-Projekt. Nur kommt hier natürlich weder ein Novell noch ein anderer "Share" zum Einsatz. Dieser Weg ist und bleibt aber eine ganz einfache Möglichkeit parallelisierbare Aufgaben einfach zu verteilen.

Queue und Jobs

Ein klassische Ansatz, den ich schon 1991 bei meiner Diplomarbeit entwickelt habe nutzt eine eine Queue zur Verwaltung der Aufträge, die der steuernde Prozess dort anlegt und die Jobserver holen sich hier die Aufträge ab und arbeiten diese ab. Um das möglichst einfach und unabhängig zu halten, habe ich damals und auch heute einfach ein Verzeichnis als Queue gewählt, auf das alle Systeme zugreifen können.

Der Auftraggeber erstellt einfach Job-Dateien, die alle Informationen zur Verarbeitung enthalten und legt diese als Datei im Verzeichnis ab. Die Datei hat einfach die Endung ".JOB"

Die Jobserver sind einfache Skripte, die immer wieder im dem Verzeichnis, was natürlich auch ein Netzwerkshare sein kann, nach Aufträgen suchen. Sobald ein Jobserver einen Auftrag findet, benennt Sie die Datei z.B. nach ".RUN" um. Wenn ihnen dies gelingt, dann ist dieser Prozess für die Ausführung zuständig, ansonsten war ein anderer Jobserver schneller und es beginnt von vorne.

Der ausführende Prozess liest dann die benötigten Daten ein, macht seine Arbeit und nach dem Ende benennt er die Auftragsdatei nach "*.DONE" um.

Der Prozess kann optimiert werden, wenn die Jobdateien z.B. eine laufende aufsteigende Nummer haben und die Jobserver so der Reihenfolge nachgehen können. Interessant kann auch eine Überwachung der Jobs sein, z.B. kann der ausführende Prozess die .RUN-Datei einfach mi einem "Lock" versehen. Bricht der Job ungeplant ab, dann ist die Dateisperre in der Regel wieder gelöst und der Auftraggeber könnte eine .RUN-Datei nach einiger Zeit nach .JOB zurück benennen, damit ein anderer Server damit arbeiten kann

Sicherheit und Dienstkonten

Ehe sie nun in die Vollen gehen, sollten Sie sich noch ein paar Gedanken zur Sicherheit machen nutze diese Technik nur für Skripte die ich selbst geschrieben habe und der Jobserver selbst ist das Skript mit der Business Logik, der über die JOB-Datei nur seine Parameter bekommt. Die Job-Datei enthält also nicht den Aufruf des Skripts selbst. Nicht auszudenken, wenn ein Jobserver mit privilegierten Rechten (z.B. Exchange Admin) läuft und jemand anderes eine JOB-Datei anlegt und im Auftrag ausführen lässt.

Wenn ich aber sicherstelle, dass in dem Share wirklich NUR autorisierte Personen einen Auftrag anlegen dürfen, dann kann ich natürlich auch die auszuführenden Befehle entsprechend Parametrisieren oder sogar

Auftraggeber

Ich nehme mal einfach ein Beispiel, bei dem ich die lokalen Prozesse mit einzelnen parallelen Skripten verarbeiten will. So erstelle ich die Jobs.

[string]$queuedir = "C:\temp\psdistribute\queue\"

get-process | %{ `
   [string]$command = "get-process -id "+($_.id.tostring())+"; #Read-Host ""press Key"""
   $command | out-file -filepath ($queuedir + $_.id.tostring() + "." + [guid]::NewGuid().tostring() + ".job") `
   }

Als Folge finden sich dann in dem Verzeichnis für jede Mailbox eine JOB-Datei mit dem gewünschten Aufruf als Inhalt.

Das ist natürlich nur ein theoretisches Muster. Interessanter wird es, wenn man z.B. umfangreichere Aktionen an Postfächern ausführen möchte und hier parallel arbeiten kann. Das könnte dann wie folgt vereinfacht aussehen

get-mailbox | %{ `
   [string]$command = "add-mailboxpermission -identity "+ ($_.alias.tostring()) + " -User service -AccessRights ReadPermission "
   $command | out-file -filepath ("\\server\share\"+$_.alias+"."+[guid]::NewGuid().tostring()+".job") `
   }

Ich addiere eine GUID im Dateinamen, damit mehrere unterschiedliche Jobs auf die gleiche Mailbox sich nicht überschreiben. Im Verzeichnis sieht das dann wie folgt aus:

Für jedes Postfach gibt es einen passende Datei, in der der Befehl dazu enthalten ist. Durch die Wahl von ".JOB" als Erweiterung zeigt der Explorer diese Dateien als "Task Scheduler Task Objekt an. Das ist es natürlich nicht.

Worker

Nun brauchen wir noch ein paar "Jobserver". Technisch ist das auch nur ein Skript, welches in meinem Fall als Administrator manuell gestartet wird und in diesem Fall die Aufträge abarbeitet. Ich hatte zuerst immer nur eine Datei geholt, wenn aber dies eine JOB-Datei ist, die ich z.B.: wegen Berechtigungen nicht umbenennen kann, dann hängt das Skript fest. Also versuche ich besser alle JOB-Dateien abzuarbeiten, was aber dazu führen kann, dass nach einer Zeit ich JOB-Dateien prüfe, die schon jemand anderes abgearbeitet hat. Wenn es aber nicht um hundertausende Jobs geht, dann ist das auch tolerierbar.

# Worker for PS Distributed
[string]$queuedir = "C:\temp\psdistribute\queue\"

while ($true) {
	write-host "Checking for Jobs"
	foreach ($job in (get-childitem -path ($queuedir+"*.job") -erroraction silentlycontinue)) {
		write-host ("Checking " + $queuedir+$job.basename+".job")
		write "Renaming JOB-File"
		$error.clear()
		$RenameError = $Null
		rename-item `
			-path ($queuedir+$job.basename+".job") `
			-newname ($job.basename+".run") `
			-ErrorAction silentlycontinue `
			-ErrorVariable RenameError
		if ($RenameError) {
			write-host "  not my job - someone else caught it"
		}
		else {
			$error.clear()
			write-host "Processing Job:" ($queuedir+$job.basename+".run")
			# Hier muss dann ihr Code rein, der sich die Daten aus dem Job holt und etwas damit macht, z.B. 
				$command = Get-content -path ($queuedir+$job.basename+".run")
				invoke-expression $command
			# Ende der Verarbeitung
			write-host "Processing End"
			if ($error) {
				rename-item ($queuedir+$job.basename+".run") ($queuedir+$job.basename+".err")
			}
			else {
				rename-item ($queuedir+$job.basename+".run") ($queuedir+$job.basename+".done")
			}
		}
	}
   write-host "Waiting 5 Seconds" -nonewline
   start-sleep -seconds 1
}

Ich musste an mehreren Stellen aber mit "-errorpreference" arbeiten, da ich mir zum einen die Fehlerbehandlung nicht komplett abschalten wollte, aber rote Meldungen nur erscheinen sollen, wenn wirklich ein Fehler vorliegt. Der Versuch den Namen einer Datei per Rename-Item zu ändern, liefert den ersten Fehler, wenn die Datei von einem anderen Prozess schon umbenannt wurde.

Aber auch "Get-Childitem" kommt anscheinend durcheinander, wenn während der Schleife ein anderer Prozess Dateien umbenennt. Das ist natürlich nur ein "Rumpf"-Skript ohne Fehlerbehandlung und vor allem ohne Rückmeldung.

Wenn Sie zum Testen von einer PowerShell aus mehrere Worker starten wollen, dann können Sie dies einfach mit folgendem Aufruf erreichen.

start-process powershell -ArgumentList ".\worker.ps1"

Allerdings schließt sich dann das Fenster natürlich sofort wieder, wenn sie den Worker mit "CTRL-C" o.ä. abbrechen.

Perfektionisten werden nun beides in einem Skript kombinieren, z.B. am Anfang mit "Start-Process" das Skript samt Parameter mehrfach als "Worker" starten und dann in den Worker-Teil abzweigen und dann die Jobs erzeugen und die Ergebnisse einsammeln. Interessant ist die Parallelisierung aber vor allem im Einsatz mit mehreren Systemen.

Beispiel mit Performance

Ein Kollege von mir hat basierend auf dieser Technik die Aufgabe optimiert, bei allen Postfächern einer Exchange Umgebung die Berechtigungen zu ändern. Ein sequentieller Lauf auf einem PC hat ein vielfaches der Zeit benötigt und auch die Parallelisierung auf dem gleichen PC war mit Start-Job zwar schneller aber nicht so einfach zu kontrollieren. Über die JOB-Dateien und mehrere Job-Server konnte die Verarbeitung deutlich beschleunigt werden.

Da hie als einziger Parameter nur die Mailbox-Identity übermittelt werden musste, konnte der Alias gleich als Dateiname für die JOB-Datei genutzt werden. Das ausführende Skript musste also nur den Dateinamen direkt als Parameter einlesen und hat die Berechtigungen für das Dienstkonto entsprechend gesetzt. So kann das Skript sogar permanent laufen oder per Taskplaner gestartet werden. Auch eine Art "RBAC".

Weitere Links