Come posso eseguire i miei script PowerShell in parallelo senza usare Jobs?


29

Se ho uno script che devo eseguire su più computer o con più argomenti diversi, come posso eseguirlo in parallelo, senza dover sostenere il sovraccarico di generareStart-Job un nuovo PSJob con ?

Ad esempio, voglio risincronizzare l'ora su tutti i membri del dominio , in questo modo:

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

Ma non voglio aspettare che ogni sessione PSSession si connetta e invochi il comando. Come può essere fatto in parallelo, senza Jobs?

Risposte:


51

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 /nowaitun'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:

  1. Creazione di un RunspacePool
  2. Assegnare uno script PowerShell o un pezzo equivalente di codice eseguibile a RunspacePool
  3. 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(), 1ed 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.exeprocesso (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 Beginmetodo, per dare una "luce verde" per eseguire il codice e un Endmetodo per raccogliere i risultati. Dato che in questo caso non siamo veramente interessati a nessun feedback (non aspettiamo w32tmcomunque 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
}

4
Se i lavori sono in attesa sulla rete, ad es. Se si eseguono comandi PowerShell su computer remoti, è possibile andare facilmente oltre il numero di processori logici prima di toccare qualsiasi collo di bottiglia della CPU.
Michael Hampton

Bene, è vero. L'ho cambiato un po 'e ha fornito un esempio per i test
Mathias R. Jessen,

Come assicurarsi che tutto il lavoro sia svolto alla fine? (Potrebbe aver bisogno di qualcosa dopo che tutti i blocchi di script sono finiti)
sjzls

@NickW Ottima domanda. Farò un follow-up sul monitoraggio dei lavori e sulla "raccolta" del potenziale prodotto più tardi oggi, restate sintonizzati
Mathias R. Jessen

1
@ MathiasR.Jessen Risposta molto ben scritta! In attesa dell'aggiornamento.
Signal15

5

Aggiungendo a questa discussione, ciò che manca è un raccoglitore per archiviare i dati creati dallo spazio di esecuzione e una variabile per verificare lo stato dello spazio di esecuzione, ovvero se è stato completato o meno.

#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

3

Dai un'occhiata a PoshRSJob . Fornisce le stesse funzioni / simili delle funzioni native * -Job, ma utilizza Runpace che tendono ad essere molto più veloci e più reattivi rispetto ai lavori Powershell standard.


1

@ mathias-r-jessen ha un'ottima risposta anche se ci sono dettagli che vorrei aggiungere.

Discussioni Max

In teoria i thread dovrebbero essere limitati dal numero di processori di sistema. Tuttavia, durante il test di AsyncTcpScan ho ottenuto prestazioni molto migliori scegliendo un valore molto più grande per MaxThreads. Quindi perché quel modulo ha un -MaxThreadsparametro di input. Tieni presente che l'allocazione di troppi thread ostacolerà le prestazioni.

Restituzione dei dati

Il recupero dei dati da ScriptBlockè complicato. Ho aggiornato il codice OP e integrato in quello che è stato utilizzato per AsyncTcpScan .

ATTENZIONE: non sono stato in grado di testare il seguente codice. Ho apportato alcune modifiche allo script OP sulla base della mia esperienza con i cmdlet di Active Directory.

# 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.