Como construir um acelerador de foguete para scripts PowerCLI 

Mais cedo ou mais tarde, qualquer administrador de sistema VMware consegue automatizar tarefas de rotina. Tudo começa na linha de comando e, em seguida, vem o PowerShell ou o VMware PowerCLI.

Suponha que você tenha dominado o PowerShell um pouco mais do que iniciar o ISE e usar cmdlets padrão de módulos que funcionam com algum tipo de mágica. Quando você começa a contar centenas de máquinas virtuais, verá que os scripts que ajudaram em pequenas escalas funcionam visivelmente mais devagar em grandes. 

Nesta situação, duas ferramentas ajudarão:

  • Espaços de execução do PowerShell - uma abordagem que permite paralelizar a execução de processos em threads separados; 
  • O Get-View é uma função básica do PowerCLI, um análogo do Get-WMIObject no Windows. Esse cmdlet não arrasta objetos relacionados a ele, mas recebe informações na forma de um objeto simples com tipos de dados simples. Em muitos casos, sai mais rápido.

A seguir, falarei brevemente sobre cada ferramenta e mostrarei exemplos de uso. Analisaremos scripts específicos e veremos quando um funciona melhor, quando o segundo. Vai!



Primeira Etapa: Runspace


Portanto, o Runspace foi projetado para processamento paralelo de tarefas fora do módulo principal. Obviamente, você pode iniciar outro processo que consumirá memória, processador etc. Se o script for executado em alguns minutos e gasta um gigabyte de memória, provavelmente você não precisará do Runspace. Mas, para scripts em dezenas de milhares de objetos, é necessário.
Você pode iniciar o desenvolvimento a partir daqui: 
Iniciando o uso dos espaços de execução do PowerShell: Parte 1

O que dá uso ao Runspace:

  • velocidade limitando a lista de comandos executáveis,
  • tarefas paralelas
  • segurança.

Aqui está um exemplo da Internet quando o Runspace ajuda:
« – , vSphere. vCenter , . , PowerShell.
, VMware vCenter .  
PowerShell runspaces, ESXi Runspace . PowerShell , , ».

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

No caso abaixo, a Runspace não está mais no negócio:
“Estou tentando escrever um script que coleta muitos dados da VM e, se necessário, grava novos dados. O problema é que existem muitas VMs e leva de 5 a 8 segundos para uma máquina. ” 

Fonte: PowerCLI multithreading com RunspacePool

O Get-View é necessário aqui, vamos prosseguir. 

Segunda etapa: Get-View


Para entender a utilidade do Get-View, lembre-se de como os cmdlets funcionam em geral. 

Os cmdlets são necessários para obter informações convenientemente sem a necessidade de estudar os livros de referência da API e reinventar a roda. O que antigamente era escrito em uma centena ou duas linhas de código, o PowerShell permite que você execute um comando. Por essa conveniência, pagamos velocidade. Dentro dos próprios cmdlets, não há mágica: o mesmo script, mas de nível inferior, escrito pelas mãos hábeis de um mestre da ensolarada Índia.

Agora, para comparação com o Get-View, use o cmdlet Get-VM: ele acessa a máquina virtual e retorna um objeto composto, ou seja, anexa outros objetos relacionados a ela: VMHost, Datastore etc.  

O Get-View em seu lugar não estraga nada extra no objeto retornado. Além disso, permite que você indique rigidamente exatamente quais informações precisamos, o que facilitará o objeto na saída. No Windows Server em geral, e no Hyper-V em particular, o cmdlet Get-WMIObject é um análogo direto - a idéia é exatamente a mesma.

O Get-View é inconveniente em operações de rotina em recursos de pontos. Mas quando se trata de milhares e dezenas de milhares de objetos, ele não tem preço.
Leia mais no Blog da VMware: Introdução ao Get-View

Agora vou mostrar tudo em um caso real. 

Escrevemos um script para descarregar uma VM


Uma vez, meu colega me pediu para otimizar seu script. A tarefa é uma rotina normal: encontre todas as VMs com um parâmetro cloud.uuid duplicado (sim, isso é possível ao clonar uma VM no vCloud Director). 

A solução óbvia que vem à mente:

  1. Obtenha uma lista de todas as VMs.
  2. De alguma forma, analise a lista.

A versão original era um script tão simples:

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

Tudo é extremamente simples e claro. Está escrito em alguns minutos com uma pausa para o café. Enrosque o filtro e pronto.

Mas meça o tempo:





2 minutos e 47 segundos ao processar quase 10k VM. Um bônus é a falta de filtros e a necessidade de classificar manualmente o resultado. Obviamente, o script está pedindo otimização.

Ransepses são os primeiros a serem resgatados quando você precisa obter métricas de host com o vCenter de uma só vez ou precisa processar dezenas de milhares de objetos. Vamos ver o que essa abordagem dará.

Ativamos a primeira velocidade: PowerShell Runspaces

A primeira coisa que vem à mente para esse script é executar o loop não em série, mas em threads paralelos, colete todos os dados em um objeto e os filtre. 

Mas há um problema: o PowerCLI não nos permitirá abrir muitas sessões independentes para o vCenter e lançará um erro engraçado:

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 resolvê-lo, você deve primeiro passar as informações da sessão para o fluxo. Lembramos que o PowerShell trabalha com objetos que podem ser passados ​​como parâmetro para pelo menos uma função, pelo menos para ScriptBlock. Vamos passar a sessão como um objeto ignorando $ global: DefaultVIServers (Connect-VIServer com a 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 $_
           }
       }
   }

Agora, implementamos multithreading por meio de pools de Runspace.  

O algoritmo é o seguinte:

  1. Obtenha uma lista de todas as VMs.
  2. Em threads paralelos, obtemos cloud.uuid.
  3. Coletamos dados de fluxos em um objeto.
  4. Nós filtramos o objeto agrupando pelo valor do campo CloudUUID: aqueles em que o número de valores exclusivos é maior que 1 e existem as VMs desejadas.

Como resultado, obtemos o 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
}

A beleza desse script é que ele pode ser usado em outros casos semelhantes, simplesmente substituindo o ScriptBlock e os parâmetros que serão transferidos para o fluxo. Explorar!

Medimos o tempo:



55 segundos. Já é melhor, mas ainda mais rápido. 

Passamos para a segunda velocidade: GetView

Descobrimos o que está errado.
Primeiro e óbvio: o cmdlet Get-VM leva muito tempo para ser concluído.
Segundo: o cmdlet Get-AdvancedOptions executa ainda mais.
Primeiro, vamos lidar com o segundo. 

Get-AdvancedOptions é conveniente em objetos individuais da VM, mas muito lento ao trabalhar com muitos objetos. Podemos obter as mesmas informações do próprio objeto da máquina virtual (Get-VM). É só que está bem enterrado no objeto ExtensionData. Armado com a filtragem, aceleramos o processo de obtenção dos dados necessários.

Com um movimento do pulso, isto é:


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 transforma nisso:


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

A conclusão é a mesma que Get-AdvancedOptions, mas funciona muitas vezes mais rápido. 

Agora para Get-VM. Não é executado rapidamente, pois lida com objetos complexos. Uma questão lógica surge: por que precisamos de informações extras e um PSObject monstruoso nesse caso, quando precisamos apenas do nome da VM, seu estado e valor do atributo complicado?  

Além disso, o freio diante de Get-AdvancedOptions deixou o script. O uso de Pools de Runspace agora parece um exagero, pois não há mais necessidade de paralelizar uma tarefa lenta em fluxos com agachamentos ao transferir uma sessão. A ferramenta é boa, mas não para este caso. 

Observamos a saída de ExtensionData: não é nada além de um objeto Get-View. 

Vamos chamar a técnica antiga dos mestres do PowerShell: uma linha usando filtros, classificação e agrupamento. Todo o horror anterior se quebra elegantemente em uma linha e é executado em uma sessão:


$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 o tempo:



9 segundos para quase 10 mil objetos com filtragem de acordo com a condição desejada. Bem!

Em vez de uma conclusão Um

resultado aceitável depende diretamente da escolha da ferramenta. Muitas vezes é difícil dizer com certeza o que exatamente deve ser escolhido para alcançá-lo. Cada um dos métodos de aceleração de script listados é bom dentro dos limites de sua aplicabilidade. Espero que este artigo o ajude na difícil tarefa de entender os conceitos básicos da automação de processos e sua otimização em sua infraestrutura.

PS: O autor agradece a todos os membros da comuna por sua ajuda e apoio na preparação do artigo. Mesmo aqueles com patas. E mesmo quem não tem pernas, como uma jibóia.

All Articles