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 /nowait
opció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:
- Crear un RunspacePool
- Asignación de un script de PowerShell o una pieza equivalente de código ejecutable al RunspacePool
- 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()
, 1
y 8
es 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.exe
proceso (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 Begin
método, para dar una "luz verde" para ejecutar el código, y un End
método para recopilar los resultados. Dado que en este caso no estamos realmente interesados en ninguna retroalimentación (de w32tm
todos 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
}