Atualização - Embora esta resposta explique o processo e a mecânica dos espaços de execução do PowerShell e como eles podem ajudá-lo a cargas de trabalho não sequenciais multiencadeadas, o colega aficionado do PowerShell, Warren 'Cookie Monster' F , se esforçou e incorporou esses mesmos conceitos em uma única ferramenta chamado - ele faz o que eu descrevo abaixo, e ele o expandiu com opções opcionais para registro e estado de sessão preparado, incluindo módulos importados, coisas realmente legais - eu recomendo fortemente que você verifique antes de criar sua própria solução brilhante!Invoke-Parallel
Com a execução Parallel Runspace:
Reduzindo o tempo de espera inevitável
No caso específico original, o executável invocado possui uma /nowait
opção que impede o bloqueio do encadeamento de chamada enquanto o trabalho (nesse caso, ressincronização de tempo) termina por conta própria.
Isso reduz bastante o tempo de execução geral da perspectiva dos emissores, mas a conexão com cada máquina ainda é feita em ordem seqüencial. A conexão com milhares de clientes em sequência pode levar muito tempo, dependendo do número de máquinas inacessíveis por um motivo ou outro, devido a um acúmulo de esperas de tempo limite.
Para evitar a fila de todas as conexões subseqüentes no caso de um único ou alguns tempos limite consecutivos, podemos despachar a tarefa de conectar e chamar comandos para separar os Runspaces do PowerShell, executando em paralelo.
O que é um Runspace?
Um Runspace é o contêiner virtual no qual o código do PowerShell é executado e representa / mantém o Ambiente da perspectiva de uma instrução / comando do PowerShell.
Em termos gerais, 1 Runspace = 1 encadeamento de execução; portanto, tudo o que precisamos para "encadear" nosso script do PowerShell é uma coleção de Runspaces que, por sua vez, podem ser executados em paralelo.
Como o problema original, a tarefa de chamar comandos pode executar vários espaços de execução em:
- Criando um RunspacePool
- Atribuindo um script do PowerShell ou uma parte equivalente do código executável ao RunspacePool
- Invoque o código de forma assíncrona (ou seja, sem ter que esperar o retorno do código)
Modelo RunspacePool
O PowerShell tem um acelerador de tipo chamado [RunspaceFactory]
que nos ajudará na criação de componentes do espaço de execução - vamos colocá-lo em funcionamento
1. Crie um RunspacePool e Open()
:
$RunspacePool = [runspacefactory]::CreateRunspacePool(1,8)
$RunspacePool.Open()
Os dois argumentos transmitidos para CreateRunspacePool()
, 1
e 8
é o número mínimo e máximo de espaços de execução permitidos para execução a qualquer momento, fornecendo um grau máximo efetivo de paralelismo de 8.
2. Crie uma instância do PowerShell, anexe algum código executável e atribua-o ao nosso RunspacePool:
Uma instância do PowerShell não é igual ao powershell.exe
processo (que é realmente um aplicativo Host), mas um objeto de tempo de execução interno que representa o código do PowerShell a ser executado. Podemos usar o [powershell]
acelerador de tipo para criar uma nova instância do PowerShell no 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. Invoque a instância do PowerShell de forma assíncrona usando o APM:
Usando o que é conhecido na terminologia de desenvolvimento .NET como Modelo de Programação Assíncrona , podemos dividir a invocação de um comando em um Begin
método, fornecendo uma "luz verde" para executar o código e um End
método para coletar os resultados. Como neste caso não estamos realmente interessados em w32tm
receber feedback (não esperamos a saída de qualquer maneira), podemos fazer o que é devido simplesmente chamando o primeiro método
$PSinstance.BeginInvoke()
Agrupando-o em um RunspacePool
Usando a técnica acima, podemos agrupar as iterações seqüenciais de criação de novas conexões e de chamar o comando remoto em um fluxo de execução paralelo:
$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()
}
Supondo que a CPU tenha capacidade para executar todos os 8 espaços de execução de uma só vez, poderemos ver que o tempo de execução é bastante reduzido, mas com o custo de legibilidade do script devido aos métodos bastante "avançados" usados.
Determinando o grau ideal de paralelismo:
Poderíamos facilmente criar um RunspacePool que permita a execução de 100 espaços de execução ao mesmo tempo:
[runspacefactory]::CreateRunspacePool(1,100)
Mas no final das contas, tudo se resume a quantas unidades de execução nossa CPU local pode suportar. Em outras palavras, enquanto seu código estiver em execução, não faz sentido permitir mais espaços de execução do que os processadores lógicos para os quais enviar a execução de código.
Graças ao WMI, esse limite é bastante fácil de determinar:
$NumberOfLogicalProcessor = (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
[runspacefactory]::CreateRunspacePool(1,$NumberOfLogicalProcessors)
Se, por outro lado, o código que você está executando em si requer muito tempo de espera devido a fatores externos, como a latência da rede, você ainda pode se beneficiar da execução de espaços de execução mais simultâneos do que os processadores lógicos, então provavelmente deseja testar do intervalo, possíveis espaços de execução máximos para encontrar o ponto de equilíbrio :
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
}