web-dev-qa-db-fra.com

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

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 génération d'un nouveau PSJob avec Start-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?

29
Mathias R. Jessen

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 avec des charges de travail non séquentielles multithread, collègue Amateur de PowerShell Warren 'Cookie Monster' F a fait un effort supplémentaire et a incorporé ces mêmes concepts dans un seul outil appeléInvoke-Parallel 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 vérifiez-le avant de vous construire propre solution brillante !


Avec l'exécution de Parallel Runspace:

Réduire les temps d'attente incontournables

Dans le cas spécifique d'origine, l'exécutable appelé a une option /nowait 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 de délais d'attente.

Pour contourner la nécessité de mettre en file d'attente toutes les connexions suivantes en cas d'expiration unique ou de plusieurs délais d'attente consécutifs, nous pouvons répartir le travail de connexion et d'invocation de commandes vers des espaces d'exécution PowerShell séparés, en s'exécutant 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 dans la création de composants runspace - mettons-le au travail

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

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

Les deux arguments passés à CreateRunspacePool(), 1 Et 8 Est le nombre minimum et maximum d'espaces d'exécution autorisés à s'exécuter à un moment donné, ce qui nous donne une efficacité maximum degré de parallélisme 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 que le processus powershell.exe (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'accélérateur de type [powershell] 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 méthode Begin, pour donner un "feu vert" pour exécuter le et une méthode End 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 w32tm De toute 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 au bout du compte, tout se résume au nombre d'unités d'exécution que notre processeur 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 voudrez probablement tester de plage possible d'espaces d'exécution maximum pour trouver 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
}
52
Mathias R. Jessen

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 qu'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
5
Nate Stone

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.

3
Rosco

@ mathias-r-jessen a une grande 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 des tests AsyncTcpScan j'ai obtenu de bien meilleures performances en choisissant une valeur beaucoup plus grande pour MaxThreads. Ainsi, pourquoi ce module a un -MaxThreads paramè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 du ScriptBlock est délicat. J'ai mis à jour le code OP et l'ai intégré dans ce qui a été 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
1
phbits