Як побудувати ракетний прискорювач для скриптів PowerCLI 

Рано чи пізно будь-який системний адміністратор VMware досягає автоматизації рутинних завдань. Починається все з командного рядка, потім йде PowerShell чи VMware PowerCLI.

Припустимо, ви освоїли PowerShell трохи далі запуску ISE та використання стандартних командлетів із модулів, які працюють за рахунок «якоїсь магії». Коли ви почнете вважати віртуальні машини сотнями, то виявите, що скрипти, які рятували на малих масштабах, працюють помітно повільніше на великих. 

У цій ситуації виручать 2 інструменти:

  • PowerShell Runspaces – підхід, який дозволяє розпаралелити виконання процесів в окремих потоках; 
  • Get-View – базова функція PowerCLI, аналог Get-WMIObject у Windows. Цей командлет не тягне у себе супутні сутності об'єкти, а отримує інформацію як простого об'єкта з простими типами даних. У багатьох випадках виходить швидше.

Далі коротко розповім про кожен інструмент та покажу приклади використання. Розберемо конкретні скрипти та подивимося, коли краще працює один, коли другий. Поїхали!

Як побудувати ракетний прискорювач для скриптів PowerCLI

Перший ступінь: Runspace

Отже, Runspace призначений для паралельної обробки завдань поза основним модулем. Звичайно, можна запустити ще один процес, який з'їсть скільки пам'яті, процесора і т. д. Якщо ваш скрипт відпрацьовує за пару хвилин і витрачає гігабайт пам'яті, швидше за все, Runspace вам не потрібно. А ось для скриптів на десятки тисяч об'єктів він потрібний.

Почати освоєння можна звідси: 
Початок використання PowerShell Runspaces: Part 1

Що дає використання Runspace:

  • швидкість за рахунок обмеження списку виконуваних команд,
  • паралельне виконання завдань,
  • безпеку.

Ось приклад з інтернету, коли Runspace допомагає:

«Конкуренція за ресурси сховища – одна з метрик, які складно відстежувати у vSphere. Всередині vCenter не можна просто взяти та подивитися, яка ВМ споживає більше ресурсів сховища. На щастя, зібрати ці дані можна за хвилини завдяки PowerShell.
Поділюсь скриптом, який дозволить системним адміністраторам VMware швидко виконувати пошук по всьому vCenter і отримувати аркуш ВМ з даними щодо їхнього середнього споживання.  
Скрипт використовує PowerShell runspaces, щоб кожен хост ESXi збирав інформацію щодо споживання власних ВМ в окремому Runspace і відразу повідомляв про завершення. Це дозволяє PowerShell відразу закривати джоби, а не перебирати послідовно хости і не чекати, доки кожен завершить свій запит».

Джерело: How to Show Virtual Machine I/O на ESXi Dashboard

У випадку нижче Runspace вже не при справах:

«Намагаюся написати скрипт, який збирає багато даних із ВМ і за потреби записує нові дані. Проблема в тому, що ВМ досить багато і на одну машину витрачається по 5-8 секунд». 

Джерело: Multithreading PowerCLI with RunspacePool

Тут знадобиться Get-View, перейдемо до нього. 

Другий ступінь: Get-View

Щоб розібратися чим корисний Get-View, варто згадати, як працюють командлети взагалі. 

Командлети потрібні для зручного отримання інформації без необхідності вивчати довідники по API і винаходити черговий велосипед. Те, що за старих часів розписувалося в сотню-другу рядків коду, PowerShell дозволяє зробити однією командою. За цю зручність ми платимо швидкістю. Усередині самих командлетів ніякої магії немає: той самий скрипт, але нижчого рівня, написаний умілими руками майстра із сонячної Індії.

Тепер для порівняння з Get-View візьмемо командлет Get-VM: він звертається до віртуальної машини та повертає складовий об'єкт, тобто прикладає до нього інші супутні об'єкти: VMHost, Datastore і т.д.  

Get-View на його місці не прикручує в об'єкт, що повертається, нічого зайвого. Більше того, він дозволяє жорстко зазначити, яка саме інформація нам потрібна, що полегшить об'єкт на виході. У Windows Server загалом і Hyper-V, зокрема, прямим аналогом є командлет Get-WMIObject - ідея абсолютно та ж.

Get-View незручний у рутинних операціях над точковими об'єктами. Але коли мова заходить про тисячі та десятки тисяч об'єктів, йому немає ціни.

Почитати докладніше можна у блозі VMware: Introduction to Get-View

Нині все покажу на реальному кейсі. 

Пишемо скрипт для вивантаження ВМ

Якось мій колега попросив мене оптимізувати його скрипт. Завдання – звичайна рутина: знайти все ВМ з параметром cloud.uuid, що дублюється (так, таке можливо при клонуванні ВМ в vCloud Director). 

Очевидний варіант рішення, який спадає на думку:

  1. Отримати список усіх ВМ.
  2. Якимось чином розпарсувати список.

Вихідним варіантом був такий нехитрий скрипт:

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

Все дуже просто і зрозуміло. Пишеться за пару хвилин із перервою на каву. Прикрутити фільтрацію, і справа зроблена.

Але заміряємо час:

Як побудувати ракетний прискорювач для скриптів PowerCLI

Як побудувати ракетний прискорювач для скриптів PowerCLI

2 хвилини 47 секунд при обробці майже 10k ВМ. Бонусом – відсутність фільтрів та необхідність вручну сортувати результат. Очевидно, що скрипт вимагає оптимізації.

Ранспейси першими приходять на допомогу, коли потрібно одночасно отримати метрики хостів із vCenter або потрібно обробити десятки тисяч об'єктів. Подивимося, що дасть цей підхід.

Включаємо першу швидкість: PowerShell Runspaces

Перше, що спадає на думку для цього скрипта: виконати цикл не послідовно, а в паралельних потоках, зібрати всі дані в один об'єкт і відфільтрувати. 

Але є проблема: PowerCLI не дозволить нам відкривати безліч незалежних сесій до vCenter і викине веселу помилку:

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.

Щоб її вирішити, треба спочатку передати всередину потоку інформацію про сесію. Згадуємо, що PowerShell працює з об'єктами, які можна передавати як параметр хоч у функцію, хоч у ScriptBlock. Передаємо сесію у вигляді такого об'єкта в обхід $global:DefaultVIServers (Connect-VIServer з ключем -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 $_
           }
       }
   }

Тепер реалізуємо мультипоточність через Runspace Pools.  

Алгоритм наступний:

  1. Отримуємо список усіх ВМ.
  2. У паралельних потоках одержуємо cloud.uuid.
  3. Дані із потоків збираємо в один об'єкт.
  4. Фільтруємо об'єкт через угруповання за значенням поля CloudUUID: ті, де кількість унікальних значень більша за 1, і є шукані ВМ.

У результаті отримуємо скрипт:


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
}

Принадність цього скрипта в тому, що його можна використовувати і в інших схожих випадках, просто замінивши ScriptBlock та параметри, які будуть передані в потік. Exploit it!

Вимірюємо час:

Як побудувати ракетний прискорювач для скриптів PowerCLI

55 секунд. Вже краще, але все одно якнайшвидше. 

Переходимо на другу швидкість: GetView

З'ясовуємо, що негаразд.
Перше та очевидне: командлет Get-VM виконується довго.
Друге: командлет Get-AdvancedOptions виконується ще довше.
Спочатку розберемося з другим. 

Get-AdvancedOptions зручний на окремих об'єктах ВМ, але дуже неповороткий під час роботи з безліччю об'єктів. Ту ж інформацію ми можемо отримати з самого об'єкта віртуальної машини (Get-VM). Просто вона добре закопана в об'єкті ExtensionData. Озброївшись фільтрацією, прискорюємо процес отримання потрібних даних.

Легким рухом руки це:


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

Перетворюється на це:


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

Висновок той же, що і у Get-AdvancedOptions, але працює в рази швидше. 

Тепер до Get-VM. Він виконується не швидко, оскільки має справу зі складними об'єктами. Постає логічне питання: а навіщо нам в даному випадку зайва інформація і монструозний PSObject, коли нам потрібне ім'я ВМ, її стан і значення хитрого атрибуту?  

До того ж, зі скрипту пішло гальмо в особі Get-AdvancedOptions. Застосування Runspace Pools тепер виглядає надмірністю, тому що більше немає необхідності в розпаралелювання повільного завдання в потоках з присіданнями під час передачі сесії. Інструмент добрий, але не для цього кейсу. 

Дивимося на висновок ExtensionData: це не що інше, як об'єкт Get-View. 

Закличемо древню техніку майстрів PowerShell: one line із застосуванням фільтрів, сортувань та угруповання. Весь попередній жах елегантно хлопається в один рядок і виконується в одній сесії:


$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

Вимірюємо час:

Як побудувати ракетний прискорювач для скриптів PowerCLI

9 секунд для майже 10 к об'єктів з фільтруванням за потрібною умовою. Чудово!

Замість висновку

Прийнятний результат залежить від вибору інструмента. Найчастіше важко сказати напевно, що саме слід вибрати для його досягнення. Кожен із перерахованих методів прискорення скриптів хороший у межах своєї застосовності. Сподіваюся, ця стаття допоможе вам у нелегкій справі розуміння основ автоматизації процесів та їх оптимізації у вашій інфраструктурі.

PS: Автор дякує всім учасникам комуни за допомогу та підтримку при підготовці статті. Навіть тих, хто має лапки. І навіть у кого лапок нема, як у удава.

Джерело: habr.com

Додати коментар або відгук