Búsqueda de sitios web

Dar sentido al paralelo para cada objeto en PowerShell 7


Probablemente haya oído hablar del nuevo parámetro paralelo con ForEach-Object en la última vista previa de PowerShell 7 Personalmente. He estado esperando esto durante mucho tiempo. Solía usar solo flujos de trabajo de PowerShell porque ofrecía una forma de ejecutar comandos en paralelo. Tener esta característica como parte del lenguaje es una adición bienvenida. Pero esto no es magia y hay consecuencias en el mundo real cuando lo usas. El parámetro -Parallel activará una colección de espacios de ejecución y ejecutará su bloque de script en cada uno. Ejecutar algo en paralelo no significa en orden.

A continuación se muestra un ejemplo del nuevo parámetro en acción.

Introduje un intervalo de suspensión aleatorio para ayudar a demostrar que los comandos se ejecutan en paralelo. El resultado se escribe en la canalización a medida que se completa cada operación paralela. Profundicemos un poco más en esto.

Tubería recta

A continuación se muestra un ejemplo sencillo de canalización que utiliza ForEach-Object.

Measure-Command {
    1..5000 | ForEach-Object { [math]::Sqrt($_) * 2 }
}

Esto me llevó 34 ms completarlo. Aquí está el mismo comando pero ejecútelo usando -parallel.

Measure-Command {
    1..5000 | ForEach-Object -parallel { [math]::Sqrt($_) * 2 }
}

Esto tomó 89 segundos. Claramente, hay una sobrecarga involucrada al usar este parámetro.

Sin embargo, existe margen de mejora potencial. De forma predeterminada, el comando activa 5 espacios de ejecución a la vez. Puede aumentar o disminuir esto con el parámetro -ThrottleLimit.

Measure-Command {
    1..5000 | ForEach-Object -parallel { [math]::Sqrt($_) * 2 } -throttlelimit 25
}

¡Esta versión terminó en casi 5 segundos! En comparación, utilizando el enumerador ForEach:

Measure-Command {
    foreach ($x in (1..5000)) { [math]::Sqrt($x) * 2 }
}

tarda unos 7 ms.

Ejecutando bloques de scripts

Aumentemos la complejidad y hagamos algo un poco más práctico. Aquí hay un bloque de script que me dará un objeto que muestra el uso de archivos en una carpeta determinada.

$t = {
    param ([string]$Path)
    Get-ChildItem -path $path -recurse -file -erroraction SilentlyContinue |
        Measure-Object -sum -Property Length -Maximum -Minimum |
        Select-Object @{Name = "Computername"; Expression = { $env:COMPUTERNAME } },
        @{Name = "Path"; Expression = { Convert-Path $path } },
        Count, Sum, Maximum, Minimum
}

Measure-Command {
    "c:\work", "c:\scripts", "d:\temp", "C:\users\jeff\Documents", "c:\windows" | 
    ForEach-Object -process { Invoke-Command -scriptblock $t -argumentlist $_ }
}

Ejecutar esto localmente en mi escritorio con Windows 10 con 32 GB de memoria tomó un poco más de 34 segundos. Aquí está el mismo comando usando -Parallel.

Measure-Command {
    "c:\work", "c:\scripts", "d:\temp", "C:\users\jeff\Documents", "c:\windows" | ForEach-Object -parallel {
        $path = $_
        Get-ChildItem -path $path -recurse -file -erroraction SilentlyContinue |
            Measure-Object -sum -Property Length -Maximum -Minimum |
            Select-Object @{Name = "Computername"; Expression = { $env:COMPUTERNAME } },
            @{Name = "Path"; Expression = { Convert-Path $path } },
            Count, Sum, Maximum, Minimum

    } 
}

Deberías notar una gran diferencia en la sintaxis. Debido a que cada bloque de script paralelo se ejecuta en un nuevo espacio de ejecución, es más difícil pasar variables como $t. Hay maneras de evitar esto. Para mí, la solución más sencilla para la demostración fue simplemente copiar el bloque de script.

Este comando tardó unos 50 segundos en ejecutarse. Aunque si subía el límite del acelerador a 6, se completaba en unos 20 segundos.

Comandos remotos

Donde -Parallel comienza a tener sentido es con comandos que involucran computadoras remotas o cualquier cosa que contenga algún tipo de variabilidad. Aquí tienes un comando tradicional que puedes ejecutar.

Measure-Command {
    'srv1', 'srv2', 'win10', 'dom1', 'srv1', 'srv2', 'win10', 'dom1' | ForEach-Object {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 } -MaxEvents 100 -ComputerName $_
    }
}

Solo tengo unas pocas máquinas virtuales para realizar pruebas, así que repito el comando con ellas. Esto se completó en aproximadamente 3 segundos. En comparación con el enfoque paralelo:

Measure-Command {
    'srv1', 'srv2', 'win10', 'dom1', 'srv1', 'srv2', 'win10', 'dom1' | ForEach-Object -parallel {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 } -MaxEvents 100 -ComputerName $_
    }
}

que tomó poco menos de 1 segundo. Aumentemos el tamaño de la muestra.

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1')*5 | 
    ForEach-Object {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
    }
}

Esto tomó 9,7 segundos en comparación con esto:

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1')*5  |
        ForEach-Object -parallel {
            Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        }
}

que se completó en 3,7 segundos

PowerShell a escala

Veamos realmente cómo se ve esto a escala. Incluso intentaré simular la latencia del servidor y de la red.

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1') * 100 | ForEach-Object {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        #simulate network/server latency
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
    }
}

En mi escritorio, esto tardó más de 18 minutos en completarse y devolvió casi 41.000 registros. Aquí está la versión predeterminada de PowerShell 7 usando -parallel:

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1') * 100 | ForEach-Object -parallel {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
    }
}

Ahora el comando se completa en 3 minutos y 35 segundos.

El valor límite de aceleración predeterminado puede cambiar. Jeffrey Snover ha dicho que cree que debería ser más alto y creo que estoy de acuerdo. Veamos qué sucede cuando se usa el valor ThrottleLimit predeterminado de Invoke-Command.

Measure-Command {
    $d = ('srv1', 'srv2', 'win10', 'dom1') * 100 | ForEach-Object -parallel {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  -ComputerName $_
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
    } -throttlelimit 32
}

¡Esto se completó en 49 segundos!

Una alternativa de flujo de trabajo

Para una última comparación, aquí hay un flujo de trabajo ciertamente aproximado que se aproxima a los otros ejemplos. Cambié a Get-Eventlog porque no creo que al flujo de trabajo le guste enviar la tabla hash de filtrado a través de una conexión serializada.

Workflow GetLogs {

    Param()

    Sequence {
        $computers = ('srv1.company.pri', 'srv2.company.pri', 'win10.company.pri', 'dom1.company.pri') * 100
        foreach -parallel ($computer in $computers) {

            inlinescript {
                Write-Verbose "$(Get-Date) Processing $using:computer"
                Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
                Get-Eventlog -LogName system -EntryType error, Warning -ComputerName $using:computer
            } #inline
        } #foreach
    } #sequence
} #workflow
}

$d = getlogs -pscomputername localhost -ErrorAction SilentlyContinue

Esto no es para lo que fueron pensados los flujos de trabajo de PowerShell, aunque espero que muchas personas hayan intentado usarlos de esta manera. Este comando se completó en 10:05, aunque probablemente habría mejorado con un límite de aceleración más alto.

Conclusiones

La única conclusión absoluta que puede sacar es que necesita hacer sus propias pruebas para determinar si usar -parallel tiene sentido para su comando o proyecto. Personalmente, mi conclusión es que el paralelo funciona mejor para operaciones a gran escala con mucha variabilidad. Es posible que obtenga resultados comparables al escribir sus comandos a escala con Invoke-Command.

$computers = ('srv1', 'srv2', 'win10', 'dom1') * 100
$d = Invoke-Command {
        Get-WinEvent -FilterHashtable @{Logname = "system"; Level = 2, 3 }  
        Start-Sleep -Seconds (Get-Random -Minimum 1 -Maximum 5)
} -throttlelimit 32 -ComputerName $computers

Sí, hay gastos generales ya que las PSSessions deben configurarse y eliminarse. Para mí, esto se completó en 1:56, lo que sigue siendo un rendimiento aceptable y, como beneficio adicional, no requiere PowerShell 7.

Planeo seguir experimentando con esta función y me encantaría conocer sus experiencias al usarla. Creo que eventualmente habrá un consenso comunitario sobre cuándo usar esto, ¡pero por ahora tenemos el placer de descubrirlo!

Artículos relacionados