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 /nowait
option 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:
- Création d'un RunspacePool
- Affectation d'un script PowerShell ou d'un morceau équivalent de code exécutable à RunspacePool
- 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()
, 1
et 8
sont 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.exe
processus (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 Begin
méthode, pour donner un "feu vert" pour exécuter le code et une End
mé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 w32tm
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 à 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
}