Рано или поздно любой системный администратор VMware доходит до автоматизации рутинных задач. Начинается все с командной строки, потом идет PowerShell или VMware PowerCLI.

Допустим, вы освоили PowerShell чуть дальше запуска ISE и использования стандартных командлетов из модулей, которые работают за счет «какой-то магии». Когда вы начнете считать виртуальные машины сотнями, то обнаружите, что скрипты, которые выручали на малых масштабах, работают заметно медленнее на больших. 

В этой ситуации выручат 2 инструмента:

  • PowerShell Runspaces – подход, который позволяет распараллелить выполнение процессов в отдельных потоках; 
  • Get-View – базовая функция PowerCLI, аналог Get-WMIObject в Windows. Этот командлет не тянет за собой сопутствующие сущности объекты, а получает информацию в виде простого объекта с простыми типами данных. Во многих случаях выходит быстрее.

Дальше кратко расскажу про каждый инструмент и покажу примеры использования. Разберем конкретные скрипты и посмотрим, когда лучше работает один, когда второй. Поехали!



Первая ступень: Runspace


Итак, Runspace предназначен для параллельной обработки задач вне основного модуля. Конечно, можно запустить еще один процесс, который съест сколько-то памяти, процессора и т. д. Если ваш скрипт отрабатывает за пару минут и тратит гигабайт памяти, скорее всего, Runspace вам не потребуется. А вот для скриптов на десятки тысяч объектов он нужен.
Начать освоение можно отсюда: 
Beginning Use of PowerShell Runspaces: Part 1

Что дает использование Runspace:

  • скорость за счет ограничения списка выполняемых команд,
  • параллельное выполнение задач,
  • безопасность.

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

Источник: How to Show Virtual Machine I/O on an 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
}
# Далее РУКАМИ парсим полученный результат

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

Но замерим время:





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!

Замеряем время:



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

Замеряем время:



9 секунд для почти 10k объектов с фильтрацией по нужному условию. Отлично!

Вместо заключения

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

P.S.: Автор благодарит всех участников коммуны за помощь и поддержку при подготовке статьи. Даже тех, у кого лапки. И даже у кого лапок нет, как у удава.