Como executo meus scripts do PowerShell em paralelo sem usar tarefas?

29

Se eu tenho um script que preciso executar em vários computadores, ou com vários argumentos diferentes, como posso executá-lo em paralelo, sem ter que suportar a sobrecarga de gerar um novo PSJobStart-Job ?

Como exemplo, eu quero sincronizar novamente o horário em todos os membros do domínio , assim:

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

Mas não quero esperar que cada PSSession se conecte e invoque o comando. Como isso pode ser feito em paralelo, sem Jobs?

Mathias R. Jessen
fonte

Respostas:

51

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 /nowaitopçã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:

  1. Criando um RunspacePool
  2. Atribuindo um script do PowerShell ou uma parte equivalente do código executável ao RunspacePool
  3. 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(), 1e 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.exeprocesso (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 Beginmétodo, fornecendo uma "luz verde" para executar o código e um Endmétodo para coletar os resultados. Como neste caso não estamos realmente interessados ​​em w32tmreceber 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
}
Mathias R. Jessen
fonte
4
Se os trabalhos estiverem aguardando na rede, por exemplo, você estiver executando comandos do PowerShell em computadores remotos, poderá facilmente ultrapassar o número de processadores lógicos antes de encontrar algum gargalo na CPU.
Michael Hampton
Bem, isso é verdade. Mudou um pouco e forneceu um exemplo para teste
Mathias R. Jessen
Como garantir que todo o trabalho seja feito no final? (Pode precisar de alguma coisa depois de todos os blocos de script terminado)
sjzls
@NickW Ótima pergunta. Farei um acompanhamento sobre o rastreamento dos trabalhos e a "colheita" da produção potencial ainda hoje, fique atento
Mathias R. Jessen
11
@ MathiasR.Jessen Resposta muito bem escrita! Ansioso pela atualização.
precisa
5

Adicionando a esta discussão, o que falta é um coletor para armazenar os dados criados no espaço de execução e uma variável para verificar o status do espaço de execução, isto é, está completo ou não.

#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
fonte
3

Confira PoshRSJob . Ele fornece funções iguais / semelhantes às funções nativas * -Job, mas usa Runspaces, que tendem a ser muito mais rápidos e responsivos que os trabalhos padrão do Powershell.

Rosco
fonte
1

@ mathias-r-jessen tem uma ótima resposta, embora haja detalhes que eu gostaria de adicionar.

Max Threads

Em teoria, os threads devem ser limitados pelo número de processadores do sistema. No entanto, ao testar o AsyncTcpScan , obtive um desempenho muito melhor escolhendo um valor muito maior para MaxThreads. Portanto, por que esse módulo possui um -MaxThreadsparâmetro de entrada. Lembre-se de que alocar muitos threads prejudicará o desempenho.

Retornando dados

Recuperar dados do ScriptBlocké complicado. Atualizei o código OP e o integrei ao que foi usado para o AsyncTcpScan .

AVISO: Não foi possível testar o seguinte código. Fiz algumas alterações no script OP com base na minha experiência de trabalho com os cmdlets do 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
fonte