Cum să construiți un amplificator de rachetă pentru scripturile PowerCLI 

Mai devreme sau mai târziu, orice administrator de sistem VMware vine să automatizeze sarcinile de rutină. Totul începe cu linia de comandă, apoi vine PowerShell sau VMware PowerCLI.

Să presupunem că ați stăpânit PowerShell puțin mai departe decât lansarea ISE și utilizarea cmdlet-urilor standard din module care funcționează datorită „un fel de magie”. Când începeți să numărați mașinile virtuale în sute, veți descoperi că scripturile care ajută la scară mică rulează considerabil mai încet la scară largă. 

În această situație, 2 instrumente vă vor ajuta:

  • Spații de rulare PowerShell – o abordare care vă permite să paralelizați execuția proceselor în fire separate; 
  • Get-View – o funcție de bază PowerCLI, un analog al Get-WMIObject în Windows. Acest cmdlet nu trage obiecte care însoțesc entitățile, ci primește informații sub forma unui obiect simplu cu tipuri de date simple. În multe cazuri, iese mai repede.

În continuare, voi vorbi pe scurt despre fiecare instrument și voi arăta exemple de utilizare. Să analizăm anumite scripturi și să vedem când unul funcționează mai bine decât celălalt. Merge!

Cum să construiți un amplificator de rachetă pentru scripturile PowerCLI

Prima etapă: Runspace

Deci, Runspace este proiectat pentru procesarea paralelă a sarcinilor în afara modulului principal. Desigur, puteți lansa un alt proces care va consuma puțină memorie, procesor etc. Dacă scriptul rulează în câteva minute și consumă un gigaoctet de memorie, cel mai probabil nu veți avea nevoie de Runspace. Dar pentru scripturi pentru zeci de mii de obiecte este nevoie.

Puteți începe să învățați aici: 
Începutul utilizării PowerShell Runspaces: Partea 1

Ce oferă utilizarea Runspace:

  • viteza prin limitarea listei de comenzi executate,
  • executarea paralelă a sarcinilor,
  • Siguranță.

Iată un exemplu de pe Internet când Runspace ajută:

„Conflictul de stocare este una dintre cele mai greu de urmărit în vSphere. În vCenter, nu puteți să vedeți care VM consumă mai multe resurse de stocare. Din fericire, puteți colecta aceste date în câteva minute datorită PowerShell.
Voi împărtăși un script care va permite administratorilor de sistem VMware să caute rapid în vCenter și să primească o listă de VM cu date despre consumul lor mediu.  
Scriptul folosește spații de rulare PowerShell pentru a permite fiecărei gazde ESXi să colecteze informații de consum de la propriile VM într-un spațiu de rulare separat și să raporteze imediat finalizarea. Acest lucru îi permite lui PowerShell să închidă joburile imediat, mai degrabă decât să repete prin gazde și să aștepte ca fiecare să își finalizeze cererea.”

Sursa: Cum să afișați I/O mașinii virtuale pe un tablou de bord ESXi

În cazul de mai jos, Runspace nu mai este util:

„Încerc să scriu un script care colectează multe date de la o VM și scrie date noi atunci când este necesar. Problema este că există destul de multe VM și 5-8 secunde sunt petrecute pe o singură mașină.” 

Sursa: Multithreading PowerCLI cu RunspacePool

Aici veți avea nevoie de Get-View, să trecem la el. 

A doua etapă: Get-View

Pentru a înțelege de ce este util Get-View, merită să ne amintim cum funcționează cmdleturile în general. 

Cmdlet-urile sunt necesare pentru a obține informații în mod convenabil, fără a fi nevoie de a studia cărțile de referință API și de a reinventa următoarea roată. Ceea ce pe vremuri lua o sută sau două linii de cod, PowerShell vă permite să faceți cu o singură comandă. Plătim pentru acest confort cu rapiditate. Nu există magie în interiorul cmdlet-urilor în sine: același scenariu, dar la un nivel inferior, scris de mâinile iscusite ale unui maestru din India însorită.

Acum, pentru comparație cu Get-View, să luăm cmdletul Get-VM: accesează mașina virtuală și returnează un obiect compozit, adică îi atașează alte obiecte înrudite: VMHost, Datastore etc.  

Get-View în locul său nu adaugă nimic inutil obiectului returnat. Mai mult, ne permite să specificăm cu strictețe ce informații avem nevoie, ceea ce va face obiectul de ieșire mai ușor. În Windows Server în general și în Hyper-V în special, cmdletul Get-WMIObject este un analog direct - ideea este exact aceeași.

Get-View este incomod pentru operațiuni de rutină asupra obiectelor punctuale. Dar când vine vorba de mii și zeci de mii de obiecte, nu are preț.

Puteți citi mai multe pe blogul VMware: Introducere în Get-View

Acum vă voi arăta totul folosind un caz real. 

Scrierea unui script pentru a descărca o VM

Într-o zi, colegul meu mi-a cerut să-i optimizez scenariul. Sarcina este o rutină obișnuită: găsiți toate VM-urile cu un parametru cloud.uuid duplicat (da, acest lucru este posibil când clonați o VM în vCloud Director). 

Soluția evidentă care îmi vine în minte este:

  1. Obțineți o listă cu toate VM-urile.
  2. Analizați cumva lista.

Versiunea originală a fost acest script simplu:

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
}
# Далее РУКАМИ парсим полученный результат

Totul este extrem de simplu și clar. Se poate scrie în câteva minute cu o pauză de cafea. Înșurubați filtrarea și gata.

Dar să măsurăm timpul:

Cum să construiți un amplificator de rachetă pentru scripturile PowerCLI

Cum să construiți un amplificator de rachetă pentru scripturile PowerCLI

2 minute 47 secunde atunci când procesează aproape 10k VM-uri. Un bonus este absența filtrelor și necesitatea de a sorta manual rezultatele. Evident, scriptul necesită optimizare.

Runspace-urile sunt primele care vin în ajutor atunci când trebuie să obțineți simultan valori gazdă de la vCenter sau trebuie să procesați zeci de mii de obiecte. Să vedem ce aduce această abordare.

Porniți prima viteză: PowerShell Runspaces

Primul lucru care vă vine în minte pentru acest script este să executați bucla nu secvențial, ci în fire paralele, colectați toate datele într-un singur obiect și filtrați-l. 

Dar există o problemă: PowerCLI nu ne va permite să deschidem multe sesiuni independente la vCenter și va arunca o eroare amuzantă:

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.

Pentru a rezolva acest lucru, trebuie mai întâi să transmiteți informații despre sesiune în flux. Să ne amintim că PowerShell funcționează cu obiecte care pot fi transmise ca parametru fie unei funcții, fie unui ScriptBlock. Să trecem sesiunea sub forma unui astfel de obiect, ocolind $global:DefaultVIServers (Connect-VIServer cu cheia -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 $_
           }
       }
   }

Acum să implementăm multithreading prin Runspace Pools.  

Algoritmul este după cum urmează:

  1. Primim o listă cu toate VM-urile.
  2. În fluxuri paralele obținem cloud.uuid.
  3. Colectăm date din fluxuri într-un singur obiect.
  4. Filtrăm obiectul grupându-l după valoarea câmpului CloudUUID: cele în care numărul de valori unice este mai mare decât 1 sunt VM-urile pe care le căutăm.

Ca rezultat, obținem scriptul:


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
}

Frumusețea acestui script este că poate fi folosit în alte cazuri similare prin simpla înlocuire a ScriptBlock și a parametrilor care vor fi trecuți în flux. Exploatează-l!

Măsurăm timpul:

Cum să construiți un amplificator de rachetă pentru scripturile PowerCLI

55 secunde. Este mai bine, dar poate fi tot mai rapid. 

Să trecem la a doua viteză: GetView

Să aflăm ce este în neregulă.
În primul rând, cmdletul Get-VM durează mult timp pentru a se executa.
În al doilea rând, cmdletul Get-AdvancedOptions durează și mai mult.
Să ne ocupăm mai întâi de al doilea. 

Get-AdvancedOptions este convenabil pentru obiecte VM individuale, dar foarte neîndemânatic atunci când lucrați cu multe obiecte. Putem obține aceleași informații de la obiectul mașină virtuală în sine (Get-VM). Este doar îngropat bine în obiectul ExtensionData. Înarmați cu filtrare, grăbim procesul de obținere a datelor necesare.

Cu o mișcare ușoară a mâinii, aceasta este:


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 transformă în asta:


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

Ieșirea este aceeași cu Get-AdvancedOptions, dar funcționează de multe ori mai rapid. 

Acum să Get-VM. Nu este rapid pentru că se ocupă de obiecte complexe. Apare o întrebare logică: de ce avem nevoie de informații suplimentare și de un PSObject monstruos în acest caz, când avem nevoie doar de numele VM, starea acestuia și valoarea unui atribut complicat?  

În plus, obstacolul sub forma Get-AdvancedOptions a fost eliminat din script. Utilizarea Runspace Pools acum pare exagerată, deoarece nu mai este nevoie să paralelizezi o sarcină lentă între firele de execuție la predarea unei sesiuni. Instrumentul este bun, dar nu pentru acest caz. 

Să ne uităm la rezultatul ExtensionData: nu este altceva decât un obiect Get-View. 

Să apelăm la tehnica străveche a maeștrilor PowerShell: o linie folosind filtre, sortare și grupare. Toată groaza anterioară este prăbușită elegant într-o singură linie și executată într-o singură sesiune:


$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

Măsurăm timpul:

Cum să construiți un amplificator de rachetă pentru scripturile PowerCLI

9 secunde pentru aproape 10k obiecte cu filtrare după condiția dorită. Grozav!

În loc de concluzie

Un rezultat acceptabil depinde direct de alegerea instrumentului. Adesea este dificil de spus cu siguranță ce anume ar trebui să fie ales pentru a realiza acest lucru. Fiecare dintre metodele enumerate pentru accelerarea scripturilor este bună în limitele aplicabilității sale. Sper că acest articol vă va ajuta în sarcina dificilă de a înțelege elementele de bază ale automatizării și optimizării proceselor în infrastructura dumneavoastră.

PS: Autorul mulțumește tuturor membrilor comunității pentru ajutorul și sprijinul acordat în pregătirea articolului. Chiar și cei cu labe. Și chiar și cei care nu au picioare, ca un boa constrictor.

Sursa: www.habr.com

Adauga un comentariu