Comment exécuter mes scripts PowerShell en parallèle sans utiliser de travaux?

29

Si j'ai un script que je dois exécuter sur plusieurs ordinateurs, ou avec plusieurs arguments différents, comment puis-je l'exécuter en parallèle, sans avoir à supporter la surcharge de création d'un nouveau PSJob avecStart-Job ?

Par exemple, je veux resynchroniser l'heure sur tous les membres du domaine , comme ceci:

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

Mais je ne veux pas attendre que chaque session PSSession se connecte et appelle la commande. Comment cela peut-il se faire en parallèle, sans Jobs?

Mathias R. Jessen
la source

Réponses:

51

Mise à jour - Bien que cette réponse explique le processus et la mécanique des espaces d'exécution PowerShell et comment ils peuvent vous aider dans les charges de travail non séquentielles multithread, le camarade aficionado de PowerShell Warren 'Cookie Monster' F a fait un effort supplémentaire et a incorporé ces mêmes concepts dans un seul outil appelé - il fait ce que je décris ci-dessous, et il l'a depuis étendu avec des commutateurs optionnels pour la journalisation et l'état de session préparé, y compris des modules importés, des trucs vraiment cool - je vous recommande fortement de le vérifier avant de construire votre propre solution brillante!Invoke-Parallel


Avec l'exécution de Parallel Runspace:

Réduire les temps d'attente incontournables

Dans le cas spécifique d'origine, l'exécutable invoqué a une /nowaitoption qui empêche de bloquer le thread appelant pendant que le travail (dans ce cas, la resynchronisation temporelle) se termine de lui-même.

Cela réduit considérablement le temps d'exécution global du point de vue des émetteurs, mais la connexion à chaque machine se fait toujours dans un ordre séquentiel. La connexion à des milliers de clients dans l'ordre peut prendre beaucoup de temps en fonction du nombre de machines qui sont pour une raison ou une autre inaccessibles, en raison d'une accumulation d'attentes d'expiration.

Pour contourner la nécessité de mettre en file d'attente toutes les connexions suivantes en cas d'expiration unique ou de plusieurs expirations consécutives, nous pouvons répartir le travail de connexion et d'invocation de commandes vers des espaces d'exécution PowerShell séparés, exécutés en parallèle.

Qu'est-ce qu'un Runspace?

Un Runspace est le conteneur virtuel dans lequel votre code PowerShell s'exécute et représente / contient l'environnement du point de vue d'une instruction / commande PowerShell.

En termes généraux, 1 Runspace = 1 thread d'exécution, tout ce dont nous avons besoin pour "multi-thread" notre script PowerShell est une collection d'espaces d'exécution qui peuvent ensuite à leur tour s'exécuter en parallèle.

Comme le problème d'origine, le travail d'invocation de commandes sur plusieurs espaces d'exécution peut être divisé en:

  1. Création d'un RunspacePool
  2. Affectation d'un script PowerShell ou d'un morceau équivalent de code exécutable à RunspacePool
  3. Appeler le code de manière asynchrone (c'est-à-dire ne pas avoir à attendre le retour du code)

Modèle RunspacePool

PowerShell a un accélérateur de type appelé [RunspaceFactory]qui nous aidera à créer des composants runspace - mettons-le au travail

1. Créez un RunspacePool et Open()il:

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

Les deux arguments sont passés à CreateRunspacePool(), 1et 8sont le nombre minimum et maximum d'espaces d'exécution autorisés à s'exécuter à un moment donné, ce qui nous donne un degré de parallélisme maximal effectif de 8.

2. Créez une instance de PowerShell, attachez-y du code exécutable et affectez-le à notre RunspacePool:

Une instance de PowerShell n'est pas la même chose que le powershell.exeprocessus (qui est en réalité une application hôte), mais un objet d'exécution interne représentant le code PowerShell à exécuter. Nous pouvons utiliser l' [powershell]accélérateur de types pour créer une nouvelle instance PowerShell dans 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. Appelez l'instance PowerShell de manière asynchrone à l'aide d'APM:

En utilisant ce qui est connu dans la terminologie de développement .NET sous le nom de modèle de programmation asynchrone , nous pouvons diviser l'invocation d'une commande en une Beginméthode, pour donner un "feu vert" pour exécuter le code et une Endméthode pour collecter les résultats. Étant donné que dans ce cas, nous ne sommes pas vraiment intéressés par les commentaires (nous n'attendons pas la sortie de w32tmtoute façon), nous pouvons le faire en appelant simplement la première méthode

$PSinstance.BeginInvoke()

Envelopper dans un RunspacePool

En utilisant la technique ci-dessus, nous pouvons encapsuler les itérations séquentielles de création de nouvelles connexions et d'appel de la commande distante dans un flux d'exécution parallèle:

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

En supposant que le CPU a la capacité d'exécuter les 8 espaces d'exécution à la fois, nous devrions être en mesure de voir que le temps d'exécution est considérablement réduit, mais au détriment de la lisibilité du script en raison des méthodes plutôt "avancées" utilisées.


Déterminer le degré optimal de parallélisme:

Nous pourrions facilement créer un RunspacePool qui permet l'exécution de 100 runspaces en même temps:

[runspacefactory]::CreateRunspacePool(1,100)

Mais à la fin de la journée, tout se résume au nombre d'unités d'exécution que notre CPU local peut gérer. En d'autres termes, tant que votre code s'exécute, il n'est pas logique d'autoriser plus d'espaces d'exécution que vous n'avez de processeurs logiques pour distribuer l'exécution du code.

Grâce à WMI, ce seuil est assez facile à déterminer:

$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)

Si, d'autre part, le code que vous exécutez lui-même entraîne beaucoup de temps d'attente en raison de facteurs externes tels que la latence du réseau, vous pouvez toujours bénéficier de l'exécution de plus d'espaces d'exécution simultanés que de processeurs logiques, vous devriez donc probablement tester de plage possible d'espaces d'exécution maximum pour trouver le seuil de rentabilité :

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
}
Mathias R. Jessen
la source
4
Si les travaux sont en attente sur le réseau, par exemple si vous exécutez des commandes PowerShell sur des ordinateurs distants, vous pouvez facilement dépasser le nombre de processeurs logiques avant de rencontrer un goulot d'étranglement CPU.
Michael Hampton
Eh bien, c'est vrai. Changé un peu et fourni un exemple pour les tests
Mathias R. Jessen
Comment s'assurer que tout le travail est fait à la fin? (Peut-être besoin de quelque chose après la fin de tous les blocs de script)
sjzls
@NickW Grande question. Je ferai un suivi sur le suivi des emplois et la "récolte" de la production potentielle plus tard dans la journée, restez à l'écoute
Mathias R. Jessen
1
@ MathiasR.Jessen Réponse très bien écrite! Dans l'attente de la mise à jour.
Signal15
5

Ajoutant à cette discussion, ce qui manque est un collecteur pour stocker les données qui sont créées à partir de l'espace d'exécution, et une variable pour vérifier l'état de l'espace d'exécution, c'est-à-dire s'il est terminé ou non.

#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
Nate Stone
la source
3

Découvrez PoshRSJob . Il fournit des fonctions identiques / similaires aux fonctions natives * -Job, mais utilise des espaces d'exécution qui ont tendance à être beaucoup plus rapides et plus réactifs que les travaux Powershell standard.

Rosco
la source
1

@ mathias-r-jessen a une excellente réponse bien qu'il y ait des détails que j'aimerais ajouter.

Fils max

En théorie, les threads devraient être limités par le nombre de processeurs système. Cependant, lors du test d' AsyncTcpScan, j'ai obtenu de bien meilleures performances en choisissant une valeur beaucoup plus élevée pour MaxThreads. C'est pourquoi ce module a un -MaxThreadsparamètre d'entrée. Gardez à l'esprit que l'allocation de trop de threads nuira aux performances.

Retour de données

Récupérer des données de la ScriptBlockest délicat. J'ai mis à jour le code OP et l'ai intégré à ce qui était utilisé pour AsyncTcpScan .

AVERTISSEMENT: je n'ai pas pu tester le code suivant. J'ai apporté quelques modifications au script OP en fonction de mon expérience de travail avec les applets de commande 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
phbits
la source