Cómo construir un acelerador de cohetes para scripts de PowerCLI 

Tarde o temprano, cualquier administrador del sistema VMware puede automatizar las tareas de rutina. Todo comienza desde la línea de comandos, luego viene PowerShell o VMware PowerCLI.

Suponga que ha dominado PowerShell un poco más allá de iniciar ISE y usar cmdlets estándar de módulos que funcionan con algún tipo de magia. Cuando comience a contar cientos de máquinas virtuales, encontrará que las secuencias de comandos que ayudaron en escalas pequeñas funcionan notablemente más lentamente en las grandes. 

En esta situación, 2 herramientas ayudarán:

  • PowerShell Runspaces : un enfoque que le permite paralelizar la ejecución de procesos en subprocesos separados; 
  • Get-View es una función básica de PowerCLI, un análogo de Get-WMIObject en Windows. Este cmdlet no arrastra objetos relacionados con él, sino que recibe información en forma de un objeto simple con tipos de datos simples. En muchos casos, sale más rápido.

A continuación, hablaré brevemente sobre cada herramienta y mostraré ejemplos de uso. Analizaremos guiones específicos y veremos cuándo uno funciona mejor, cuando el segundo. ¡Vamos!



Primera etapa: espacio de ejecución


Entonces, Runspace está diseñado para el procesamiento paralelo de tareas fuera del módulo principal. Por supuesto, puede iniciar otro proceso que consumirá algo de memoria, procesador, etc. Si su script se ejecuta en un par de minutos y gasta un gigabyte de memoria, probablemente no necesitará Runspace. Pero para guiones en decenas de miles de objetos es necesario.
Puede comenzar el desarrollo desde aquí: 
Inicio del uso de espacios de ejecución de PowerShell: Parte 1

Lo que da uso de Runspace:

  • velocidad al limitar la lista de comandos ejecutables,
  • tareas paralelas
  • la seguridad.

Aquí hay un ejemplo de Internet cuando Runspace ayuda:
« – , vSphere. vCenter , . , PowerShell.
, VMware vCenter .  
PowerShell runspaces, ESXi Runspace . PowerShell , , ».

: How to Show Virtual Machine I/O on an ESXi Dashboard

En el siguiente caso, Runspace ya no está en el negocio:
“Estoy tratando de escribir un script que recolecte muchos datos de la VM y, si es necesario, escriba nuevos datos. El problema es que hay muchas máquinas virtuales y una máquina tarda entre 5 y 8 segundos ". 

Fuente: Multithreading PowerCLI con RunspacePool

Get-View es necesario aquí, pasemos a ello. 

Segunda etapa: Get-View


Para comprender la utilidad de Get-View, recuerde cómo funcionan los cmdlets en general. 

Se necesitan cmdlets para obtener información convenientemente sin tener que estudiar los libros de referencia de API y reinventar la rueda. Lo que en los viejos tiempos se escribía en cien o dos líneas de código, PowerShell le permite hacer un comando. Para esta conveniencia pagamos velocidad. Dentro de los cmdlets, no hay magia: el mismo guión, pero de un nivel inferior, escrito por las hábiles manos de un maestro de la soleada India.

Ahora, para comparar con Get-View, tome el cmdlet Get-VM: accede a la máquina virtual y devuelve un objeto compuesto, es decir, le agrega otros objetos relacionados: VMHost, Datastore, etc.  

Get-View en su lugar no atornilla nada extra en el objeto devuelto. Además, le permite indicar rígidamente exactamente qué información necesitamos, lo que facilitará el objeto en la salida. En Windows Server en general, y en Hyper-V en particular, el cmdlet Get-WMIObject es un análogo directo: la idea es exactamente la misma.

Get-View es inconveniente en las operaciones de rutina en las funciones de puntos. Pero cuando se trata de miles y decenas de miles de objetos, no tiene precio.
Lea más en el blog de VMware: Introducción a Get-View

Ahora mostraré todo en un caso real. 

Escribimos un script para descargar una VM


Una vez mi colega me pidió que optimizara su guión. La tarea es una rutina normal: busque todas las máquinas virtuales con un parámetro duplicado cloud.uuid (sí, esto es posible al clonar una máquina virtual en vCloud Director). 

La solución obvia que viene a la mente:

  1. Obtenga una lista de todas las máquinas virtuales.
  2. De alguna manera analizar la lista.

La versión original era un guión tan simple:

function Get-CloudUUID1 {
   #    
   $vms = Get-VM
   $report = @()

   #   ,     2 :    Cloud UUID.
   #     PS-   VM  UUID
   foreach ($vm in $vms)
   {
       $table = "" | select VM,UUID

       $table.VM = $vm.name
       $table.UUID = ($vm | Get-AdvancedSetting -Name cloud.uuid).Value
          
       $report += $table
   }
#   
   $report
}
#     

Todo es extremadamente simple y claro. Está escrito en un par de minutos con un descanso para tomar café. Atornille el filtro y listo.

Pero mida el tiempo:





2 minutos 47 segundos cuando procesa casi 10k VM. Una ventaja es la falta de filtros y la necesidad de ordenar manualmente el resultado. Obviamente, el script está pidiendo optimización.

Los Ransepses son los primeros en acudir al rescate cuando necesita obtener métricas de host con vCenter al mismo tiempo o si necesita procesar decenas de miles de objetos. Veamos qué dará este enfoque.

Activamos la primera velocidad: espacios de ejecución de PowerShell

Lo primero que viene a la mente para este script es ejecutar el ciclo no en serie, sino en subprocesos paralelos, recopilar todos los datos en un objeto y filtrarlo. 

Pero hay un problema: PowerCLI no nos permitirá abrir muchas sesiones independientes en vCenter y arrojará un error gracioso:

You have modified the global:DefaultVIServer and global:DefaultVIServers system variables. This is not allowed. Please reset them to $null and reconnect to the vSphere server.

Para resolverlo, primero debe pasar la información de la sesión a la secuencia. Recordamos que PowerShell funciona con objetos que se pueden pasar como parámetro a al menos una función, al menos a ScriptBlock. Pasemos la sesión como tal objeto sin pasar por $ global: DefaultVIServers (Connect-VIServer con la tecla -NotDefault):

$ConnectionString = @()
foreach ($vCenter in $vCenters)
   {
       try {
           $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -AllLinked -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
       }
       catch {
           if ($er.Message -like "*not part of a linked mode*")
           {
               try {
                   $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
               }
               catch {
                   throw $_
               }
              
           }
           else {
               throw $_
           }
       }
   }

Ahora implementamos multihilo a través de Runspace Pools.  

El algoritmo es como sigue:

  1. Obtenga una lista de todas las máquinas virtuales.
  2. En hilos paralelos obtenemos cloud.uuid.
  3. Recopilamos datos de secuencias en un solo objeto.
  4. Filtramos el objeto a través de la agrupación por el valor del campo CloudUUID: aquellos en los que el número de valores únicos es superior a 1 y existen las máquinas virtuales deseadas.

Como resultado, obtenemos el script:


function Get-VMCloudUUID {
   param (
       [string[]]
       [ValidateNotNullOrEmpty()]
       $vCenters = @(),
       [int]$MaxThreads,
       [System.Management.Automation.PSCredential]
       [System.Management.Automation.Credential()]
       $Credential
   )

   $ConnectionString = @()

   #     
   foreach ($vCenter in $vCenters)
   {
       try {
           $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -AllLinked -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
       }
       catch {
           if ($er.Message -like "*not part of a linked mode*")
           {
               try {
                   $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
               }
               catch {
                   throw $_
               }
              
           }
           else {
               throw $_
           }
       }
   }

   #    
   $Global:AllVMs = Get-VM -Server $ConnectionString

   # !
   $ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
   $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)
   $RunspacePool.ApartmentState = "MTA"
   $RunspacePool.Open()
   $Jobs = @()

# ScriptBlock  !)))
#      
   $scriptblock = {
       Param (
       $ConnectionString,
       $VM
       )

       $Data = $VM | Get-AdvancedSetting -Name Cloud.uuid -Server $ConnectionString | Select-Object @{N="VMName";E={$_.Entity.Name}},@{N="CloudUUID";E={$_.Value}},@{N="PowerState";E={$_.Entity.PowerState}}

       return $Data
   }
#  

   foreach($VM in $AllVMs)
   {
       $PowershellThread = [PowerShell]::Create()
#  
       $null = $PowershellThread.AddScript($scriptblock)
#  ,      
       $null = $PowershellThread.AddArgument($ConnectionString)
       $null = $PowershellThread.AddArgument($VM)
       $PowershellThread.RunspacePool = $RunspacePool
       $Handle = $PowershellThread.BeginInvoke()
       $Job = "" | Select-Object Handle, Thread, object
       $Job.Handle = $Handle
       $Job.Thread = $PowershellThread
       $Job.Object = $VM.ToString()
       $Jobs += $Job
   }

#  ,     
#      
   While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0)
   {
       $Remaining = "$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)"

       If ($Remaining.Length -gt 60) {
           $Remaining = $Remaining.Substring(0,60) + "..."
       }

       Write-Progress -Activity "Waiting for Jobs - $($MaxThreads - $($RunspacePool.GetAvailableRunspaces())) of $MaxThreads threads running" -PercentComplete (($Jobs.count - $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) / $Jobs.Count * 100) -Status "$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining - $remaining"

       ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
           $Job.Thread.EndInvoke($Job.Handle)     
           $Job.Thread.Dispose()
           $Job.Thread = $Null
           $Job.Handle = $Null
       }
   }

   $RunspacePool.Close() | Out-Null
   $RunspacePool.Dispose() | Out-Null
}


function Get-CloudUUID2
{
   [CmdletBinding()]
   param(
   [string[]]
   [ValidateNotNullOrEmpty()]
   $vCenters = @(),
   [int]$MaxThreads = 50,
   [System.Management.Automation.PSCredential]
   [System.Management.Automation.Credential()]
   $Credential)

   if(!$Credential)
   {
       $Credential = Get-Credential -Message "Please enter vCenter credentials."
   }

   #   Get-VMCloudUUID,    
   $AllCloudVMs = Get-VMCloudUUID -vCenters $vCenters -MaxThreads $MaxThreads -Credential $Credential
   $Result = $AllCloudVMs | Sort-Object Value | Group-Object -Property CloudUUID | Where-Object -FilterScript {$_.Count -gt 1} | Select-Object -ExpandProperty Group
   $Result
}

La belleza de este script es que puede usarse en otros casos similares, simplemente reemplazando el ScriptBlock y los parámetros que se pasarán a la secuencia. Explotalo!

Medimos el tiempo:



55 segundos. Ya mejor, pero aún más rápido. 

Pasamos a la segunda velocidad: GetView

Descubrimos qué está mal.
Primero y obvio: el cmdlet Get-VM tarda mucho tiempo en completarse.
Segundo: el cmdlet Get-AdvancedOptions se ejecuta aún más.
Primero, tratemos con el segundo. 

Get-AdvancedOptions es conveniente en objetos VM individuales, pero muy lento cuando se trabaja con muchos objetos. Podemos obtener la misma información del propio objeto de máquina virtual (Get-VM). Es solo que está bien enterrado en el objeto ExtensionData. Armados con filtrado, aceleramos el proceso de obtención de los datos necesarios.

Con un movimiento de muñeca, esto es:


VM | Get-AdvancedSetting -Name Cloud.uuid -Server $ConnectionString | Select-Object @{N="VMName";E={$_.Entity.Name}},@{N="CloudUUID";E={$_.Value}},@{N="PowerState";E={$_.Entity.PowerState}}

Se convierte en esto:


$VM | Where-Object {($_.ExtensionData.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value -ne $null} | Select-Object @{N="VMName";E={$_.Name}},@{N="CloudUUID";E={($_.ExtensionData.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value}},@{N="PowerState";E={$_.summary.runtime.powerstate}}

La conclusión es la misma que Get-AdvancedOptions, pero funciona muchas veces más rápido. 

Ahora a Get-VM. No se ejecuta rápidamente, ya que trata con objetos complejos. Surge una pregunta lógica: ¿por qué necesitamos información adicional y un monstruoso PSObject en este caso, cuando solo necesitamos el nombre de la VM, su estado y el valor del atributo complicado?  

Además, el freno frente a Get-AdvancedOptions ha dejado el guión. Usar Runspace Pools ahora parece excesivo, ya que ya no hay necesidad de paralelizar una tarea lenta en transmisiones con sentadillas al transferir una sesión. La herramienta es buena, pero no para este caso. 

Observamos el resultado de ExtensionData: no es más que un objeto Get-View. 

Llamemos a la antigua técnica de los maestros de PowerShell: una línea que usa filtros, géneros y agrupaciones. Todo el horror anterior colapsa elegantemente en una línea y se ejecuta en una sesión:


$AllVMs = Get-View -viewtype VirtualMachine -Property Name,Config.ExtraConfig,summary.runtime.powerstate | Where-Object {($_.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value -ne $null} | Select-Object @{N="VMName";E={$_.Name}},@{N="CloudUUID";E={($_.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value}},@{N="PowerState";E={$_.summary.runtime.powerstate}} | Sort-Object CloudUUID | Group-Object -Property CloudUUID | Where-Object -FilterScript {$_.Count -gt 1} | Select-Object -ExpandProperty Group

Medimos el tiempo:



9 segundos para casi 10k objetos con filtrado según la condición deseada. ¡Multa!

En lugar de una conclusión, un

resultado aceptable depende directamente de la elección de la herramienta. A menudo es difícil decir con certeza qué se debe elegir exactamente para lograrlo. Cada uno de los métodos de aceleración de script enumerados es bueno dentro de los límites de su aplicabilidad. Espero que este artículo lo ayude en la difícil tarea de comprender los conceptos básicos de la automatización de procesos y su optimización en su infraestructura.

PD: El autor agradece a todos los miembros de la comuna por su ayuda y apoyo en la preparación del artículo. Incluso aquellos con patas. E incluso quien no tiene piernas, como una boa constrictora.

All Articles