Aggiornamento - Mentre questa risposta spiega il processo e la meccanica degli spazi di esecuzione di PowerShell e come possono aiutarvi a caricare carichi di lavoro non sequenziali multi-thread, il collega esperto di PowerShell Warren 'Cookie Monster' F ha fatto il possibile e ha incorporato questi stessi concetti in un unico strumento chiamato - fa quello che descrivo di seguito, e da allora lo ha ampliato con opzioni opzionali per la registrazione e lo stato della sessione preparato inclusi moduli importati, cose davvero interessanti - ti consiglio vivamente di verificarlo prima di creare la tua soluzione brillante!Invoke-Parallel
Con l'esecuzione Runspace parallela:
Riduzione del tempo di attesa inevitabile
Nel caso specifico originale, l'eseguibile invocato ha /nowait
un'opzione che impedisce di bloccare il thread invocante mentre il lavoro (in questo caso, la risincronizzazione temporale) termina da solo.
Ciò riduce notevolmente i tempi complessivi di esecuzione dal punto di vista degli emittenti, ma la connessione a ciascuna macchina viene comunque eseguita in ordine sequenziale. La connessione a migliaia di client in sequenza potrebbe richiedere molto tempo a seconda del numero di macchine che per un motivo o per l'altro sono inaccessibili, a causa di un accumulo di attese di timeout.
Per ovviare alla necessità di mettere in coda tutte le connessioni successive in caso di uno o più timeout consecutivi, è possibile inviare il lavoro di connessione e invocazione dei comandi per separare i Runpace di PowerShell, eseguendo in parallelo.
Che cos'è un Runspace?
Un Runspace è il contenitore virtuale in cui viene eseguito il codice PowerShell e rappresenta / mantiene l'ambiente dal punto di vista di un'istruzione / comando di PowerShell.
In termini generali, 1 Runspace = 1 thread di esecuzione, quindi tutto ciò di cui abbiamo bisogno per "multi-thread" il nostro script PowerShell è una raccolta di Runpace che a sua volta può essere eseguita in parallelo.
Come il problema originale, il lavoro di invocazione di comandi su più aree di esecuzione può essere suddiviso in:
- Creazione di un RunspacePool
- Assegnare uno script PowerShell o un pezzo equivalente di codice eseguibile a RunspacePool
- Richiamare il codice in modo asincrono (ovvero non dover attendere la restituzione del codice)
Modello RunspacePool
PowerShell ha un acceleratore di tipo chiamato [RunspaceFactory]
che ci aiuterà nella creazione dei componenti dello spazio di esecuzione: mettiamolo al lavoro
1. Crea un RunspacePool e Open()
esso:
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,8)
$RunspacePool.Open()
I due argomenti passati a CreateRunspacePool()
, 1
ed 8
è il numero minimo e massimo di spazi di esecuzione consentiti in un dato momento, dandoci un effettivo grado massimo di parallelismo di 8.
2. Crea un'istanza di PowerShell, allega del codice eseguibile e assegnalo al nostro RunspacePool:
Un'istanza di PowerShell non è la stessa del powershell.exe
processo (che in realtà è un'applicazione Host), ma un oggetto di runtime interno che rappresenta il codice PowerShell da eseguire. Possiamo usare l' [powershell]
acceleratore di tipo per creare una nuova istanza di PowerShell in 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. Richiamare l'istanza di PowerShell in modo asincrono tramite APM:
Utilizzando quello che è noto nella terminologia di sviluppo .NET come Modello di programmazione asincrona , possiamo dividere l'invocazione di un comando in un Begin
metodo, per dare una "luce verde" per eseguire il codice e un End
metodo per raccogliere i risultati. Dato che in questo caso non siamo veramente interessati a nessun feedback (non aspettiamo w32tm
comunque l'output ), possiamo fare il dovuto semplicemente chiamando il primo metodo
$PSinstance.BeginInvoke()
Avvolgendolo in un RunspacePool
Usando la tecnica sopra, possiamo avvolgere le iterazioni sequenziali di creazione di nuove connessioni e invocazione del comando remoto in un flusso di esecuzione parallelo:
$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()
}
Supponendo che la CPU abbia la capacità di eseguire tutti e 8 gli spazi di esecuzione contemporaneamente, dovremmo essere in grado di vedere che il tempo di esecuzione è notevolmente ridotto, ma a costo di leggibilità dello script a causa dei metodi piuttosto "avanzati" utilizzati.
Determinazione del grado ottimale di parallelismo:
Potremmo facilmente creare un RunspacePool che consenta l'esecuzione di 100 spazi di esecuzione contemporaneamente:
[runspacefactory]::CreateRunspacePool(1,100)
Ma alla fine, tutto si riduce a quante unità di esecuzione è in grado di gestire la nostra CPU locale. In altre parole, finché il codice è in esecuzione, non ha senso consentire più spazi di esecuzione rispetto ai processori logici ai quali inviare l'esecuzione del codice.
Grazie a WMI, questa soglia è abbastanza facile da determinare:
$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)
Se, d'altra parte, il codice che si sta eseguendo comporta un sacco di tempo di attesa a causa di fattori esterni come la latenza di rete, è comunque possibile trarre vantaggio dall'esecuzione di spazi di esecuzione più simultanei rispetto ai processori logici, quindi probabilmente si vorrebbe testare della gamma possibili spazi di corsa massimi per trovare il pareggio :
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
}