¿Cómo ejecuto mis scripts de PowerShell en paralelo sin usar Jobs?

29

Si tengo un script que necesito ejecutar en varias computadoras, o con múltiples argumentos diferentes, ¿cómo puedo ejecutarlo en paralelo, sin tener que incurrir en la sobrecarga de generar un nuevo PSJobStart-Job ?

Como ejemplo, quiero volver a sincronizar la hora en todos los miembros del dominio , así:

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

Pero no quiero esperar a que cada PSSession se conecte e invoque el comando. ¿Cómo se puede hacer esto en paralelo, sin trabajos?

Mathias R. Jessen
fuente

Respuestas:

51

Actualización : si bien esta respuesta explica el proceso y la mecánica de los espacios de ejecución de PowerShell y cómo pueden ayudarlo a cargar cargas de trabajo no secuenciales de varios hilos, el compañero aficionado de PowerShell, Warren 'Cookie Monster' F, ha hecho un esfuerzo adicional e incorporó estos mismos conceptos en una sola herramienta llamado : hace lo que describo a continuación, y desde entonces lo ha expandido con interruptores opcionales para iniciar sesión y preparar el estado de la sesión, incluidos los módulos importados, cosas realmente geniales. ¡Le recomiendo que lo revise antes de crear su propia solución brillante!Invoke-Parallel


Con la ejecución de Parallel Runspace:

Reducción del tiempo de espera ineludible

En el caso específico original, el ejecutable invocado tiene una /nowaitopción que impide bloquear el subproceso de invocación mientras el trabajo (en este caso, la sincronización de tiempo) finaliza por sí solo.

Esto reduce en gran medida el tiempo de ejecución general desde la perspectiva de los emisores, pero la conexión a cada máquina todavía se realiza en orden secuencial. Conectarse a miles de clientes en secuencia puede llevar mucho tiempo dependiendo de la cantidad de máquinas que por una razón u otra sean inaccesibles, debido a una acumulación de esperas de tiempo de espera.

Para evitar tener que poner en cola todas las conexiones subsiguientes en caso de uno o varios tiempos de espera consecutivos, podemos enviar el trabajo de conectar e invocar comandos para separar espacios de ejecución de PowerShell, ejecutándolos en paralelo.

¿Qué es un espacio de ejecución?

Un Runspace es el contenedor virtual en el que se ejecuta su código de PowerShell, y representa / mantiene el entorno desde la perspectiva de una instrucción / comando de PowerShell.

En términos generales, 1 Runspace = 1 hilo de ejecución, por lo que todo lo que necesitamos para "multihilo" en nuestro script de PowerShell es una colección de espacios de ejecución que, a su vez, pueden ejecutarse en paralelo.

Al igual que el problema original, el trabajo de invocar comandos múltiples espacios de ejecución se puede dividir en:

  1. Crear un RunspacePool
  2. Asignación de un script de PowerShell o una pieza equivalente de código ejecutable al RunspacePool
  3. Invoque el código de forma asincrónica (es decir, no tener que esperar a que vuelva el código)

Plantilla RunspacePool

PowerShell tiene un acelerador de tipo llamado [RunspaceFactory]que nos ayudará en la creación de componentes de espacio de ejecución; pongámoslo a trabajar

1. Cree un RunspacePool y Open():

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

Los dos argumentos pasados ​​a CreateRunspacePool(), 1y 8es el número mínimo y máximo de espacios de ejecución permitidos para ejecutarse en un momento dado, nos dan un grado máximo de paralelismo efectivo de 8.

2. Cree una instancia de PowerShell, adjunte un código ejecutable y asígnelo a nuestro RunspacePool:

Una instancia de PowerShell no es lo mismo que el powershell.exeproceso (que es realmente una aplicación Host), sino un objeto de tiempo de ejecución interno que representa el código de PowerShell a ejecutar. Podemos usar el [powershell]acelerador de tipos para crear una nueva instancia de PowerShell dentro de 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 la instancia de PowerShell de forma asincrónica utilizando APM:

Usando lo que se conoce en la terminología de desarrollo de .NET como el Modelo de programación asincrónica , podemos dividir la invocación de un comando en un Beginmétodo, para dar una "luz verde" para ejecutar el código, y un Endmétodo para recopilar los resultados. Dado que en este caso no estamos realmente interesados ​​en ninguna retroalimentación (de w32tmtodos modos, no esperamos la salida de la información ), podemos hacerlo simplemente llamando al primer método

$PSinstance.BeginInvoke()

Envolviéndolo en un RunspacePool

Usando la técnica anterior, podemos envolver las iteraciones secuenciales de crear nuevas conexiones e invocar el comando remoto en un flujo de ejecución 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()
}

Suponiendo que la CPU tiene la capacidad de ejecutar los 8 espacios de ejecución a la vez, deberíamos poder ver que el tiempo de ejecución se reduce considerablemente, pero a costa de la legibilidad del script debido a los métodos más "avanzados" utilizados.


Determinación del grado óptimo de paralismo:

Podríamos crear fácilmente un RunspacePool que permita la ejecución de 100 espacios de ejecución al mismo tiempo:

[runspacefactory]::CreateRunspacePool(1,100)

Pero al final del día, todo se reduce a cuántas unidades de ejecución puede manejar nuestra CPU local. En otras palabras, mientras su código se esté ejecutando, no tiene sentido permitir más espacios de ejecución de los que tiene procesadores lógicos para enviar la ejecución del código.

Gracias a WMI, este umbral es bastante fácil de determinar:

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

Si, por otro lado, el código que está ejecutando genera mucho tiempo de espera debido a factores externos como la latencia de la red, aún puede beneficiarse de ejecutar más espacios de ejecución simultáneos que los procesadores lógicos, por lo que probablemente desee probar del rango de espacios de ejecución máximos posibles para encontrar el punto de equilibrio :

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
fuente
44
Si los trabajos están esperando en la red, por ejemplo, si está ejecutando comandos de PowerShell en computadoras remotas, podría superar fácilmente la cantidad de procesadores lógicos antes de llegar a cualquier cuello de botella de la CPU.
Michael Hampton
Bueno, eso es verdad. Lo cambió un poco y brindó un ejemplo para las pruebas
Mathias R. Jessen, el
¿Cómo asegurarse de que todo el trabajo se realiza al final? (Es posible que necesite algo después de que todos los bloques de script
hayan
@NickW Gran pregunta. Haré un seguimiento sobre el seguimiento de los trabajos y "cosechar" la producción potencial más tarde hoy, estad atentos
Mathias R. Jessen
1
@ MathiasR.Jessen ¡Respuesta muy bien escrita! Esperando la actualización.
Señal15
5

Además de esta discusión, lo que falta es un recopilador para almacenar los datos que se crean desde el espacio de ejecución, y una variable para verificar el estado del espacio de ejecución, es decir, si está completo o no.

#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
Piedra de Nate
fuente
3

Echa un vistazo a PoshRSJob . Proporciona funciones iguales / similares a las funciones nativas * -Job, pero utiliza espacios de ejecución que tienden a ser mucho más rápidos y más receptivos que los trabajos estándar de Powershell.

Rosco
fuente
1

@ mathias-r-jessen tiene una gran respuesta, aunque hay detalles que me gustaría agregar.

Max hilos

En teoría, los hilos deberían estar limitados por el número de procesadores del sistema. Sin embargo, al probar AsyncTcpScan logré un rendimiento mucho mejor al elegir un valor mucho mayor para MaxThreads. Por eso ese módulo tiene un -MaxThreadsparámetro de entrada. Tenga en cuenta que asignar demasiados hilos dificultará el rendimiento.

Devolución de datos

Recuperar datos del ScriptBlockes complicado. He actualizado el código OP y lo he integrado en lo que se usó para AsyncTcpScan .

ADVERTENCIA: no pude probar el siguiente código. Hice algunos cambios en el script OP basado en mi experiencia trabajando con los cmdlets de 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
fuente