Como construir um foguete impulsionador para scripts PowerCLI 

Mais cedo ou mais tarde, qualquer administrador de sistema VMware automatiza tarefas rotineiras. Tudo começa com a linha de comando e depois vem o PowerShell ou VMware PowerCLI.

Digamos que você domine o PowerShell um pouco além de iniciar o ISE e usar cmdlets padrão de módulos que funcionam devido a “algum tipo de mágica”. Quando você começar a contar centenas de máquinas virtuais, descobrirá que os scripts que ajudam em pequena escala são visivelmente mais lentos em grande escala. 

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

  • Espaços de execução do PowerShell – uma abordagem que permite paralelizar a execução de processos em threads separados; 
  • Obter visualização – uma função básica do PowerCLI, um análogo do Get-WMIObject no Windows. Este cmdlet não extrai objetos que acompanham entidades, 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. Vamos analisar scripts específicos e ver quando um funciona melhor que o outro. Ir!

Como construir um foguete impulsionador para scripts PowerCLI

Primeira etapa: Runspace

Portanto, o Runspace foi projetado para processamento paralelo de tarefas fora do módulo principal. Claro, você pode iniciar outro processo que consumirá memória, processador, etc. Se o seu script for executado em alguns minutos e consumir um gigabyte de memória, provavelmente você não precisará do Runspace. Mas isso é necessário para scripts de dezenas de milhares de objetos.

Você pode começar a aprender aqui: 
Iniciando o uso de Runspaces do PowerShell: Parte 1

O que o uso do Runspace oferece:

  • velocidade limitando a lista de comandos executados,
  • execução paralela de tarefas,
  • segurança.

Aqui está um exemplo da Internet quando o Runspace ajuda:

“A contenção de armazenamento é uma das métricas mais difíceis de rastrear no vSphere. Dentro do vCenter, você não pode simplesmente ver qual VM está consumindo mais recursos de armazenamento. Felizmente, você pode coletar esses dados em minutos graças ao PowerShell.
Compartilharei um script que permitirá aos administradores de sistemas VMware pesquisar rapidamente no vCenter e receber uma lista de VMs com dados sobre seu consumo médio.  
O script usa runspaces do PowerShell para permitir que cada host ESXi colete informações de consumo de suas próprias VMs em um Runspace separado e relate imediatamente a conclusão. Isso permite que o PowerShell feche trabalhos imediatamente, em vez de iterar pelos hosts e esperar que cada um conclua sua solicitação.”

Fonte: Como mostrar E/S de máquina virtual em um painel ESXi

No caso abaixo, o Runspace não é mais útil:

“Estou tentando escrever um script que colete muitos dados de uma VM e grave novos dados quando necessário. O problema é que há muitas VMs e são gastos de 5 a 8 segundos em uma máquina.” 

Fonte: PowerCLI multithread com RunspacePool

Aqui você precisará do Get-View, vamos prosseguir. 

Segunda etapa: Get-View

Para entender por que o Get-View é útil, vale lembrar como os cmdlets funcionam em geral. 

Os cmdlets são necessários para obter informações de maneira conveniente, sem a necessidade de estudar livros de referência de API e reinventar a próxima roda. O que antigamente exigia cem ou duas linhas de código, o PowerShell permite que você faça com um comando. Pagamos por essa comodidade com agilidade. Não há mágica dentro dos próprios cmdlets: o mesmo script, mas em um nível inferior, escrito pelas mãos hábeis de um mestre da ensolarada Índia.

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

Get-View em seu lugar não adiciona nada desnecessário ao objeto retornado. Além disso, nos permite especificar estritamente quais informações precisamos, o que tornará o objeto de saída mais fácil. No Windows Server em geral e no Hyper-V em particular, o cmdlet Get-WMIObject é um análogo direto - a ideia é exatamente a mesma.

Get-View é inconveniente para operações de rotina em objetos pontuais. Mas quando se trata de milhares e dezenas de milhares de objetos, não tem preço.

Você pode ler mais no blog da VMware: Introdução ao Get-View

Agora vou mostrar tudo usando um caso real. 

Escrevendo um script para descarregar uma VM

Um dia meu colega me pediu para otimizar seu roteiro. A tarefa é uma rotina comum: encontrar 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. Analise a lista de alguma forma.

A versão original era este script 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. Pode ser escrito em alguns minutos com uma pausa para o café. Aparafuse a filtragem e pronto.

Mas vamos medir o tempo:

Como construir um foguete impulsionador para scripts PowerCLI

Como construir um foguete impulsionador para scripts PowerCLI

2 minutos e 47 segundos ao processar quase 10 mil VMs. Um bônus é a ausência de filtros e a necessidade de ordenar manualmente os resultados. Obviamente, o script requer otimização.

Os runspaces são os primeiros a ajudar quando você precisa obter simultaneamente métricas de host do vCenter ou processar dezenas de milhares de objetos. Vamos ver o que essa abordagem traz.

Ative a primeira velocidade: PowerShell Runspaces

A primeira coisa que vem à mente para este script é executar o loop não sequencialmente, mas em threads paralelos, coletar todos os dados em um objeto e filtrá-los. 

Mas há um problema: o PowerCLI não nos permitirá abrir muitas sessões independentes no vCenter e gerará 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 resolver isso, primeiro você deve passar as informações da sessão dentro do stream. Lembremos que o PowerShell trabalha com objetos que podem ser passados ​​​​como parâmetro para uma função ou para um ScriptBlock. Vamos passar a sessão na forma de tal objeto, ignorando $global:DefaultVIServers (Connect-VIServer com a chave -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 vamos implementar multithreading por meio de pools Runspace.  

O algoritmo é o seguinte:

  1. Obtemos uma lista de todas as VMs.
  2. Em fluxos paralelos, obtemos cloud.uuid.
  3. Coletamos dados de fluxos em um objeto.
  4. Filtramos o objeto agrupando-o pelo valor do campo CloudUUID: aqueles onde o número de valores únicos é maior que 1 são as VMs que procuramos.

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 passados ​​para o stream. Explore isso!

Medimos o tempo:

Como construir um foguete impulsionador para scripts PowerCLI

55 segundos. É melhor, mas ainda pode ser mais rápido. 

Vamos para a segunda velocidade: GetView

Vamos descobrir o que há de errado.
Em primeiro lugar, o cmdlet Get-VM leva muito tempo para ser executado.
Em segundo lugar, o cmdlet Get-AdvancedOptions leva ainda mais tempo para ser concluído.
Vamos lidar com o segundo primeiro. 

Get-AdvancedOptions é conveniente para objetos VM individuais, mas muito desajeitado ao trabalhar com muitos objetos. Podemos obter as mesmas informações do próprio objeto da máquina virtual (Get-VM). Está bem enterrado no objeto ExtensionData. Munidos de filtragem, agilizamos o processo de obtenção dos dados necessários.

Com um leve movimento da mão 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 saída é igual a Get-AdvancedOptions, mas funciona muito mais rápido. 

Agora para Get-VM. Não é rápido porque lida com objetos complexos. Surge uma questão lógica: por que precisamos de informações extras e de um PSObject monstruoso neste caso, quando precisamos apenas do nome da VM, seu estado e o valor de um atributo complicado?  

Além disso, o obstáculo na forma de Get-AdvancedOptions foi removido do script. Usar Runspace Pools agora parece um exagero, pois não há mais necessidade de paralelizar uma tarefa lenta em threads ocupados ao entregar uma sessão. A ferramenta é boa, mas não para este caso. 

Vejamos a saída de ExtensionData: nada mais é do que um objeto Get-View. 

Vamos recorrer à antiga técnica dos mestres do PowerShell: uma linha usando filtros, classificação e agrupamento. Todo o horror anterior é elegantemente resumido 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:

Como construir um foguete impulsionador para scripts PowerCLI

segundo 9 para quase 10 mil objetos com filtragem pela condição desejada. Ótimo!

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 listados para acelerar scripts é bom dentro de sua aplicabilidade. Espero que este artigo ajude você na difícil tarefa de entender os fundamentos da automação e otimização de processos em sua infraestrutura.

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

Fonte: habr.com

Adicionar um comentário