Jak zbudować rakietowy wzmacniacz dla skryptów PowerCLI 

Wcześniej czy później każdy administrator systemu VMware zaczyna automatyzować rutynowe zadania. Wszystko zaczyna się od wiersza poleceń, potem pojawia się PowerShell lub VMware PowerCLI.

Załóżmy, że opanowałeś PowerShell nieco dalej niż uruchamianie ISE i używanie standardowych poleceń cmdlet z modułów, które działają dzięki „jakiejś magii”. Kiedy zaczniesz liczyć maszyny wirtualne w setkach, przekonasz się, że skrypty, które pomagają na małą skalę, działają zauważalnie wolniej na dużą skalę. 

W tej sytuacji pomocne będą 2 narzędzia:

  • Obszary wykonawcze programu PowerShell – podejście pozwalające na zrównoleglenie wykonywania procesów w oddzielnych wątkach; 
  • Pobierz widok – podstawowa funkcja PowerCLI, odpowiednik Get-WMIObject w Windows. To polecenie cmdlet nie pobiera obiektów towarzyszących jednostkom, ale odbiera informacje w postaci prostego obiektu z prostymi typami danych. W wielu przypadkach wychodzi szybciej.

Następnie krótko opowiem o każdym narzędziu i pokażę przykłady użycia. Przeanalizujmy konkretne skrypty i zobaczmy, kiedy jeden działa lepiej od drugiego. Iść!

Jak zbudować rakietowy wzmacniacz dla skryptów PowerCLI

Etap pierwszy: Runspace

Zatem Runspace jest przeznaczony do równoległego przetwarzania zadań poza modułem głównym. Oczywiście możesz uruchomić inny proces, który pochłonie trochę pamięci, procesora itp. Jeśli Twój skrypt uruchomi się za kilka minut i zajmie gigabajt pamięci, najprawdopodobniej nie będziesz potrzebował Runspace. Ale w przypadku skryptów dla dziesiątek tysięcy obiektów jest to potrzebne.

Możesz rozpocząć naukę tutaj: 
Rozpoczęcie korzystania z obszarów wykonawczych programu PowerShell: część 1

Co daje korzystanie z Runspace:

  • prędkość poprzez ograniczenie listy wykonywanych poleceń,
  • równoległa realizacja zadań,
  • bezpieczeństwo.

Oto przykład z Internetu, w którym Runspace pomaga:

„Konkurencja dotycząca pamięci masowej to jeden z najtrudniejszych wskaźników do śledzenia w vSphere. W vCenter nie możesz po prostu sprawdzić, która maszyna wirtualna zużywa więcej zasobów pamięci masowej. Na szczęście dzięki PowerShellowi możesz zebrać te dane w ciągu kilku minut.
Udostępnię skrypt, który pozwoli administratorom systemów VMware szybko przeszukać vCenter i otrzymać listę maszyn wirtualnych z danymi o ich średnim zużyciu.  
Skrypt wykorzystuje obszary wykonawcze programu PowerShell, aby umożliwić każdemu hostowi ESXi zbieranie informacji o zużyciu z własnych maszyn wirtualnych w oddzielnym obszarze wykonawczym i natychmiastowe raportowanie zakończenia. Dzięki temu PowerShell może natychmiast zamykać zadania, zamiast przeglądać hosty i czekać, aż każdy z nich zakończy swoje żądanie.

Źródło: Jak wyświetlić wejścia/wyjścia maszyny wirtualnej na pulpicie nawigacyjnym ESXi

W poniższym przypadku Runspace nie jest już przydatny:

„Próbuję napisać skrypt, który zbiera dużo danych z maszyny wirtualnej i w razie potrzeby zapisuje nowe dane. Problem polega na tym, że maszyn wirtualnych jest dość dużo, a na jednej maszynie spędza się 5–8 sekund”. 

Źródło: Wielowątkowość PowerCLI z RunspacePool

Tutaj będziesz potrzebować Get-View, przejdźmy do tego. 

Drugi etap: Get-View

Aby zrozumieć, dlaczego Get-View jest przydatny, warto pamiętać, jak ogólnie działają polecenia cmdlet. 

Polecenia cmdlet są potrzebne do wygodnego uzyskiwania informacji bez konieczności studiowania podręczników API i wymyślania na nowo koła. To, co w dawnych czasach wymagało stu lub dwóch linii kodu, PowerShell umożliwia wykonanie jednego polecenia. Za tę wygodę płacimy szybkością. W samych poleceniach cmdlet nie ma żadnej magii: ten sam skrypt, ale na niższym poziomie, napisany zręcznymi rękami mistrza ze słonecznych Indii.

Teraz dla porównania z Get-View weźmy polecenie cmdlet Get-VM: uzyskuje ono dostęp do maszyny wirtualnej i zwraca obiekt złożony, czyli dołącza do niej inne powiązane obiekty: VMHost, Datastore itp.  

Get-View w swoim miejscu nie dodaje do zwracanego obiektu niczego niepotrzebnego. Co więcej, pozwala nam ściśle określić, jakich informacji potrzebujemy, co ułatwi nam uzyskanie obiektu wyjściowego. W systemie Windows Server ogólnie, a w szczególności w Hyper-V, polecenie cmdlet Get-WMIObject jest bezpośrednim odpowiednikiem - idea jest dokładnie taka sama.

Get-View jest niewygodny w przypadku rutynowych operacji na obiektach punktowych. Ale jeśli chodzi o tysiące i dziesiątki tysięcy przedmiotów, nie ma to ceny.

Więcej możesz przeczytać na blogu VMware: Wprowadzenie do Get-View

Teraz pokażę Ci wszystko na prawdziwym przypadku. 

Napisanie skryptu w celu zwolnienia maszyny wirtualnej

Któregoś dnia kolega poprosił mnie o optymalizację jego scenariusza. Zadanie jest typową procedurą: znajdź wszystkie maszyny wirtualne ze zduplikowanym parametrem cloud.uuid (tak, jest to możliwe podczas klonowania maszyny wirtualnej w vCloud Director). 

Oczywiste rozwiązanie, które przychodzi mi na myśl, to:

  1. Uzyskaj listę wszystkich maszyn wirtualnych.
  2. Przeanalizuj jakoś listę.

Oryginalna wersja była taka prosta:

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

Wszystko jest niezwykle proste i przejrzyste. Można go napisać w kilka minut z przerwą na kawę. Przykręcamy filtr i gotowe.

Ale zmierzmy czas:

Jak zbudować rakietowy wzmacniacz dla skryptów PowerCLI

Jak zbudować rakietowy wzmacniacz dla skryptów PowerCLI

2 minuty 47 sek podczas przetwarzania prawie 10 tys. maszyn wirtualnych. Bonusem jest brak filtrów i konieczność ręcznego sortowania wyników. Oczywiście skrypt wymaga optymalizacji.

Runspace jako pierwsze przychodzą na ratunek, gdy trzeba jednocześnie uzyskać metryki hosta z vCenter lub przetwarzać dziesiątki tysięcy obiektów. Zobaczymy, co przyniesie takie podejście.

Włącz pierwszą prędkość: Obszary wykonawcze programu PowerShell

Pierwszą rzeczą, która przychodzi na myśl w przypadku tego skryptu, jest wykonanie pętli nie sekwencyjnie, ale w równoległych wątkach, zebranie wszystkich danych w jeden obiekt i przefiltrowanie ich. 

Ale jest problem: PowerCLI nie pozwoli nam otworzyć wielu niezależnych sesji do vCenter i wyrzuci śmieszny błąd:

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.

Aby rozwiązać ten problem, musisz najpierw przekazać informacje o sesji w strumieniu. Pamiętajmy, że PowerShell współpracuje z obiektami, które można przekazać jako parametr do funkcji lub do obiektu ScriptBlock. Przekażmy sesję w postaci takiego obiektu, pomijając $global:DefaultVIServers (Connect-VIServer z kluczem -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 $_
           }
       }
   }

Teraz zaimplementujmy wielowątkowość poprzez pule przestrzeni roboczej.  

Algorytm wygląda następująco:

  1. Otrzymujemy listę wszystkich maszyn wirtualnych.
  2. W strumieniach równoległych dostajemy cloud.uuid.
  3. Zbieramy dane ze strumieni w jeden obiekt.
  4. Filtrujemy obiekt grupując go według wartości pola CloudUUID: te, w których liczba unikalnych wartości jest większa niż 1, to maszyny wirtualne, których szukamy.

W rezultacie otrzymujemy skrypt:


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
}

Piękno tego skryptu polega na tym, że można go użyć w innych podobnych przypadkach, po prostu zastępując blok ScriptBlock i parametry, które zostaną przekazane do strumienia. Wykorzystaj to!

Mierzymy czas:

Jak zbudować rakietowy wzmacniacz dla skryptów PowerCLI

55 sekund. Jest lepiej, ale wciąż może być szybciej. 

Przejdźmy do drugiej prędkości: GetView

Dowiedzmy się, co jest nie tak.
Przede wszystkim wykonanie polecenia cmdlet Get-VM zajmuje dużo czasu.
Po drugie, wykonanie polecenia cmdlet Get-AdvancedOptions zajmuje jeszcze więcej czasu.
Zajmijmy się najpierw tym drugim. 

Get-AdvancedOptions jest wygodny dla pojedynczych obiektów VM, ale bardzo niezdarny podczas pracy z wieloma obiektami. Te same informacje możemy uzyskać z samego obiektu maszyny wirtualnej (Get-VM). Jest po prostu dobrze ukryty w obiekcie ExtensionData. Uzbrojeni w filtrowanie przyspieszamy proces pozyskiwania niezbędnych danych.

Lekkim ruchem ręki wygląda to tak:


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

Zamienia się w to:


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

Dane wyjściowe są takie same jak w przypadku Get-AdvancedOptions, ale działają wielokrotnie szybciej. 

Teraz do Get-VM. Nie jest szybki, ponieważ zajmuje się złożonymi obiektami. Powstaje logiczne pytanie: po co nam w tym przypadku dodatkowe informacje i potworny PSObject, skoro potrzebujemy tylko nazwy maszyny wirtualnej, jej stanu i wartości trudnego atrybutu?  

Dodatkowo ze skryptu usunięto przeszkodę w postaci Get-AdvancedOptions. Korzystanie z Runspace Pools wydaje się teraz przesadą, ponieważ nie ma już potrzeby równoległego wykonywania powolnego zadania między wątkami przysiadowymi podczas przekazywania sesji. Narzędzie jest dobre, ale nie w tym przypadku. 

Przyjrzyjmy się wynikom ExtensionData: to nic innego jak obiekt Get-View. 

Nazwijmy starożytną technikę mistrzów PowerShella: jedna linia wykorzystująca filtry, sortowanie i grupowanie. Cały poprzedni horror został elegancko zwinięty w jedną linię i wykonany w jednej sesji:


$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

Mierzymy czas:

Jak zbudować rakietowy wzmacniacz dla skryptów PowerCLI

9 sekund dla prawie 10 tys. obiektów z filtrowaniem według pożądanego warunku. Świetnie!

Zamiast zawierania

Akceptowalny wynik zależy bezpośrednio od wyboru narzędzia. Często trudno powiedzieć z całą pewnością, co dokładnie wybrać, aby to osiągnąć. Każda z wymienionych metod przyspieszania skryptów jest dobra w granicach swojej stosowalności. Mam nadzieję, że ten artykuł pomoże Ci w trudnym zadaniu zrozumienia podstaw automatyzacji i optymalizacji procesów w Twojej infrastrukturze.

PS: Autor dziękuje wszystkim członkom społeczności za pomoc i wsparcie w przygotowaniu artykułu. Nawet te z łapami. A nawet ci, którzy nie mają nóg, jak boa dusiciel.

Źródło: www.habr.com

Dodaj komentarz