如何为 PowerCLI 脚本构建火箭助推器 

迟早,任何 VMware 系统管理员都会自动执行日常任务。 这一切都从命令行开始,然后是 PowerShell 或 VMware PowerCLI。

假设您对 PowerShell 的掌握程度比启动 ISE 和使用因“某种魔力”而起作用的模块中的标准 cmdlet 更进一步。 当您开始计算数百个虚拟机时,您会发现在小规模上提供帮助的脚本在大规模上运行速度明显较慢。 

在这种情况下,有两个工具可以提供帮助:

  • PowerShell 运行空间 – 一种允许您在单独的线程中并行执行进程的方法; 
  • 获取视图 – 基本的 PowerCLI 函数,类似于 Windows 中的 Get-WMIObject。 此 cmdlet 不会提取伴随实体的对象,而是接收具有简单数据类型的简单对象形式的信息。 在许多情况下,它的结果会更快。

接下来,我将简要介绍每个工具并展示使用示例。 让我们分析一下具体的脚本,看看一个脚本何时比另一个脚本效果更好。 去!

如何为 PowerCLI 脚本构建火箭助推器

第一阶段:运行空间

因此,Runspace 是为主模块之外的任务的并行处理而设计的。 当然,您可以启动另一个进程,该进程会消耗一些内存、处理器等。如果您的脚本运行几分钟并消耗一千兆字节的内存,那么您很可能不需要运行空间。 但对于数以万计的对象的脚本来说,这是需要的。

您可以从这里开始学习: 
开始使用 PowerShell 运行空间:第 1 部分

使用 Runspace 可以带来什么:

  • 通过限制执行命令的列表来提高速度,
  • 并行执行任务,
  • 安全。

以下是 Runspace 提供帮助时来自互联网的示例:

“存储争用是 vSphere 中最难跟踪的指标之一。 在 vCenter 内部,您不能仅仅查看哪个虚拟机消耗了更多存储资源。 幸运的是,借助 PowerShell,您可以在几分钟内收集这些数据。
我将分享一个脚本,该脚本允许 VMware 系统管理员快速搜索整个 vCenter,并接收包含平均消耗数据的虚拟机列表。  
该脚本使用 PowerShell 运行空间,允许每个 ESXi 主机从单独的运行空间中自己的虚拟机收集消耗信息,并立即报告完成情况。 这允许 PowerShell 立即关闭作业,而不是遍历主机并等待每个主机完成其请求。”

来源: 如何在 ESXi 仪表板上显示虚拟机 I/O

在以下情况下,Runspace 不再有用:

“我正在尝试编写一个脚本,从虚拟机收集大量数据并在必要时写入新数据。 问题是虚拟机相当多,一台机器上要花5-8秒。” 

来源: 带有 RunspacePool 的多线程 PowerCLI

这里你需要 Get-View,让我们继续吧。 

第二阶段:获取视图

要了解 Get-View 为何有用,有必要记住 cmdlet 的一般工作原理。 

需要cmdlet来方便地获取信息,而不需要研究API参考书和重新发明下一个轮子。 过去需要一百两行代码才能完成的工作,现在 PowerShell 可以让您用一条命令来完成。 我们为这种便利付出了速度的代价。 这些 cmdlet 本身并没有什么魔力:相同的脚本,但级别较低,由来自阳光明媚的印度的大师的巧手编写。

现在,为了与 Get-View 进行比较,我们以 Get-VM cmdlet 为例:它访问虚拟机并返回一个复合对象,也就是说,它附加了其他相关对象:VMHost、Datastore 等。  

Get-View 不会向返回的对象添加任何不必要的内容。 而且,它允许我们严格指定我们需要什么信息,这将使输出对象变得更容易。 在一般的 Windows Server 中,特别是在 Hyper-V 中,Get-WMIObject cmdlet 是一个直接的模拟 - 想法完全相同。

Get-View对于点对象的常规操作不方便。 但到了几千几万件的时候,就没有价格了。

您可以在 VMware 博客上阅读更多内容: 获取视图简介

现在我用一个真实的案例来告诉你一切。 

编写脚本来卸载虚拟机

有一天,我的同事让我优化他的脚本。 该任务是一个常见的例程:查找具有重复 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(使用 -NotDefault 键的 Connect-VIServer):

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

现在让我们通过运行空间池来实现多线程。  

算法如下:

  1. 我们得到所有虚拟机的列表。
  2. 在并行流中,我们获得cloud.uuid。
  3. 我们将流中的数据收集到一个对象中。
  4. 我们通过按 CloudUUID 字段的值对对象进行分组来过滤对象:唯一值的数量大于 1 的对象就是我们要查找的 VM。

结果,我们得到了脚本:


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 和将传递给流的参数即可将其用于其他类似情况。 利用它!

我们测量时间:

如何为 PowerCLI 脚本构建火箭助推器

55秒。 它更好了,但仍然可以更快。 

让我们转向第二个速度:GetView

让我们找出问题所在。
首先也是最重要的,Get-VM cmdlet 需要很长时间才能执行。
其次,Get-AdvancedOptions cmdlet 需要更长的时间才能完成。
我们先处理第二个问题。 

Get-AdvancedOptions 对于单个 VM 对象很方便,但在处理许多对象时非常笨拙。 我们可以从虚拟机对象本身(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 相同,但运行速度要快很多倍。 

现在获取VM。 它并不快,因为它处理复杂的对象。 一个逻辑问题出现了:为什么在这种情况下我们需要额外的信息和一个巨大的 PSObject,而我们只需要虚拟机的名称、其状态和一个棘手属性的值?  

此外,Get-AdvancedOptions 形式的障碍已从脚本中删除。 现在使用运行空间池似乎有点大材小用,因为在移交会话时不再需要跨蹲线程并行化缓慢的任务。 该工具很好,但不适合这种情况。 

让我们看看 ExtensionData 的输出:它只不过是一个 Get-View 对象。 

让我们调用 PowerShell 大师的古老技术:一行使用过滤器、排序和分组。 之前所有的恐怖都被优雅地折叠成一行并在一个会话中执行:


$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秒 对于近 10k 个对象,并按所需条件进行过滤。 伟大的!

取而代之的是结论

可接受的结果直接取决于工具的选择。 通常很难确定到底应该选择什么来实现这一目标。 列出的每种加速脚本的方法在其适用范围内都是很好的。 我希望本文能够帮助您完成了解基础架构中的流程自动化和优化基础知识的艰巨任务。

PS: 作者感谢所有社区成员在准备本文时提供的帮助和支持。 即使是那些有爪子的人。 甚至那些没有腿的动物,比如蟒蛇。

来源: habr.com

添加评论