So erstellen Sie einen Raketenbeschleuniger für PowerCLI-Skripte 

Früher oder später kann jeder VMware-Systemadministrator Routineaufgaben automatisieren. Alles beginnt über die Befehlszeile, dann kommt PowerShell oder VMware PowerCLI.

Angenommen, Sie beherrschen PowerShell etwas weiter als das Starten von ISE und die Verwendung von Standard-Cmdlets aus Modulen, die mit einer Art Magie arbeiten. Wenn Sie anfangen, Hunderte von virtuellen Maschinen zu zählen, werden Sie feststellen, dass Skripte, die in kleinen Maßstäben geholfen haben, in großen deutlich langsamer arbeiten. 

In dieser Situation helfen 2 Tools:

  • PowerShell Runspaces - ein Ansatz, mit dem Sie die Ausführung von Prozessen in separaten Threads parallelisieren können. 
  • Get-View ist eine grundlegende PowerCLI-Funktion, ein Analogon zu Get-WMIObject unter Windows. Dieses Cmdlet zieht keine damit verbundenen Objekte, sondern empfängt Informationen in Form eines einfachen Objekts mit einfachen Datentypen. In vielen Fällen kommt es schneller heraus.

Als nächstes werde ich kurz auf jedes Tool eingehen und Anwendungsbeispiele zeigen. Wir werden bestimmte Skripte analysieren und sehen, wann eines am besten funktioniert, wann das zweite. Gehen!



Erste Stufe: Runspace


Daher ist Runspace für die parallele Verarbeitung von Aufgaben außerhalb des Hauptmoduls ausgelegt. Natürlich können Sie einen anderen Prozess starten, der Speicher, Prozessor usw. verbraucht. Wenn Ihr Skript in wenigen Minuten ausgeführt wird und ein Gigabyte Speicher verbraucht, benötigen Sie wahrscheinlich keinen Runspace. Aber für Skripte auf Zehntausenden von Objekten wird es benötigt.
Sie können die Entwicklung hier starten: 
Beginn der Verwendung von PowerShell-Runspaces: Teil 1

Was nutzt Runspace:

  • Geschwindigkeit durch Begrenzung der Liste der ausführbaren Befehle,
  • parallele Aufgaben
  • Sicherheit.

Hier ist ein Beispiel aus dem Internet, wenn Runspace hilft:
« – , vSphere. vCenter , . , PowerShell.
, VMware vCenter .  
PowerShell runspaces, ESXi Runspace . PowerShell , , ».

: How to Show Virtual Machine I/O on an ESXi Dashboard

Im folgenden Fall ist Runspace nicht mehr im Geschäft:
„Ich versuche, ein Skript zu schreiben, das viele Daten von der VM sammelt und bei Bedarf neue Daten schreibt. Das Problem ist, dass es viele VMs gibt und es für einen Computer 5-8 Sekunden dauert. “ 

Quelle: Multithreading PowerCLI mit RunspacePool

Get-View wird hier benötigt, fahren wir fort. 

Zweite Stufe: Get-View


Denken Sie daran, wie Cmdlets im Allgemeinen funktionieren, um die Nützlichkeit von Get-View zu verstehen. 

Cmdlets werden benötigt, um bequem Informationen zu erhalten, ohne API-Nachschlagewerke studieren und das Rad neu erfinden zu müssen. Was früher in hundert oder zwei Codezeilen geschrieben wurde, ermöglicht PowerShell, einen Befehl auszuführen. Für diese Bequemlichkeit zahlen wir Geschwindigkeit. In den Cmdlets selbst gibt es keine Magie: das gleiche Skript, aber auf einer niedrigeren Ebene, geschrieben von den erfahrenen Händen eines Meisters aus dem sonnigen Indien.

Nehmen Sie zum Vergleich mit Get-View das Cmdlet Get-VM: Es greift auf die virtuelle Maschine zu und gibt ein zusammengesetztes Objekt zurück, dh es werden andere verwandte Objekte daran angehängt: VMHost, Datenspeicher usw.  

Get-View an seiner Stelle schraubt nichts extra in das zurückgegebene Objekt. Darüber hinaus können Sie genau angeben, welche Informationen wir benötigen, was das Objekt an der Ausgabe erleichtert. In Windows Server im Allgemeinen und in Hyper-V im Besonderen ist das Cmdlet Get-WMIObject ein direktes Analogon - die Idee ist genau dieselbe.

Get-View ist bei Routineoperationen mit Punktfunktionen unpraktisch. Aber wenn es um Tausende und Zehntausende von Objekten geht, hat er keinen Preis.
Weitere Informationen finden Sie im VMware-Blog: Einführung in Get-View

Jetzt zeige ich alles an einem realen Fall. 

Wir schreiben ein Skript zum Entladen einer VM


Einmal bat mich mein Kollege, sein Skript zu optimieren. Die Aufgabe ist eine normale Routine: Suchen Sie alle VMs mit einem doppelten Parameter cloud.uuid (ja, dies ist möglich, wenn Sie eine VM in vCloud Director klonen). 

Die offensichtliche Lösung, die mir in den Sinn kommt:

  1. Holen Sie sich eine Liste aller VMs.
  2. Analysiere irgendwie die Liste.

Die Originalversion war so ein einfaches Skript:

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

Alles ist sehr einfach und klar. Es ist in ein paar Minuten mit einer Kaffeepause geschrieben. Schrauben Sie den Filter an und fertig.

Aber messen Sie die Zeit:





2 Minuten 47 Sekunden, wenn Sie fast 10.000 VM verarbeiten. Ein Bonus ist das Fehlen von Filtern und die Notwendigkeit, das Ergebnis manuell zu sortieren. Offensichtlich fordert das Skript eine Optimierung.

Ransepses sind die ersten, die Abhilfe schaffen, wenn Sie Host-Metriken gleichzeitig mit vCenter abrufen oder Zehntausende von Objekten verarbeiten müssen. Mal sehen, was dieser Ansatz geben wird.

Wir aktivieren die erste Geschwindigkeit: PowerShell Runspaces

Das erste, was Ihnen bei diesem Skript einfällt, ist: Um die Schleife nicht in Reihe, sondern in parallelen Threads auszuführen, sammeln Sie alle Daten in einem Objekt und filtern Sie sie. 

Es gibt jedoch ein Problem: Mit PowerCLI können wir nicht viele unabhängige Sitzungen für vCenter öffnen und es wird ein lustiger Fehler ausgegeben:

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.

Um dies zu lösen, müssen Sie zuerst Sitzungsinformationen an den Stream übergeben. Wir erinnern uns, dass PowerShell mit Objekten arbeitet, die als Parameter an mindestens eine Funktion, zumindest an ScriptBlock, übergeben werden können. Übergeben wir die Sitzung als solches Objekt unter Umgehung von $ global: DefaultVIServers (Connect-VIServer mit dem Schlüssel -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 $_
           }
       }
   }

Jetzt implementieren wir Multithreading durch Runspace-Pools.  

Der Algorithmus ist wie folgt:

  1. Holen Sie sich eine Liste aller VMs.
  2. In parallelen Threads erhalten wir cloud.uuid.
  3. Wir sammeln Daten aus Streams in einem Objekt.
  4. Wir filtern das Objekt durch Gruppierung nach dem Wert des CloudUUID-Felds: diejenigen, bei denen die Anzahl der eindeutigen Werte mehr als 1 beträgt und die gewünschten VMs vorhanden sind.

Als Ergebnis erhalten wir das Skript:


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
}

Das Schöne an diesem Skript ist, dass es in anderen ähnlichen Fällen verwendet werden kann, indem einfach der ScriptBlock und die Parameter ersetzt werden, die an den Stream übergeben werden. Nutze es aus!

Wir messen die Zeit:



55 Sekunden. Schon besser, aber immer noch schneller. 

Wir gehen zur zweiten Geschwindigkeit über: GetView

Wir finden heraus, was falsch ist.
Erstens und offensichtlich: Die Fertigstellung des Cmdlets Get-VM dauert lange.
Zweitens: Das Cmdlet Get-AdvancedOptions wird noch länger ausgeführt.
Lassen Sie uns zuerst den zweiten behandeln. 

Get-AdvancedOptions ist praktisch für einzelne VM-Objekte, jedoch sehr langsam, wenn mit vielen Objekten gearbeitet wird. Wir können die gleichen Informationen vom Objekt der virtuellen Maschine selbst (Get-VM) erhalten. Es ist nur so, dass es gut im ExtensionData-Objekt vergraben ist. Mit Filterung beschleunigen wir den Prozess des Abrufs der erforderlichen Daten.

Mit einem Handgriff ist dies:


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

Wird daraus:


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

Die Schlussfolgerung ist dieselbe wie bei Get-AdvancedOptions, funktioniert jedoch um ein Vielfaches schneller. 

Nun zu Get-VM. Es wird nicht schnell ausgeführt, da es sich um komplexe Objekte handelt. Es stellt sich eine logische Frage: Warum benötigen wir in diesem Fall zusätzliche Informationen und ein monströses PSO-Objekt, wenn wir nur den Namen der VM, ihren Status und den Wert des kniffligen Attributs benötigen?  

Darüber hinaus hat die Bremse angesichts von Get-AdvancedOptions das Skript verlassen. Die Verwendung von Runspace-Pools scheint jetzt übertrieben, da beim Übertragen einer Sitzung keine langsame Aufgabe mehr in Streams mit Squats parallelisiert werden muss. Das Tool ist gut, aber nicht für diesen Fall. 

Wir betrachten die Ausgabe von ExtensionData: Es ist nichts anderes als ein Get-View-Objekt. 

Nennen wir die alte Technik der PowerShell-Master: eine Zeile mit Filtern, Sortierungen und Gruppierungen. Alle vorherigen Horrorvorgänge fallen elegant in einer Zeile zusammen und werden in einer Sitzung ausgeführt:


$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

Wir messen die Zeit:



9 Sekunden für fast 10.000 Objekte mit Filterung nach den gewünschten Bedingungen. Fein!

Anstelle einer Schlussfolgerung Ein

akzeptables Ergebnis hängt direkt von der Wahl des Werkzeugs ab. Es ist oft schwierig, genau zu sagen, was genau gewählt werden sollte, um dies zu erreichen. Jede der aufgelisteten Skriptbeschleunigungsmethoden ist im Rahmen ihrer Anwendbarkeit gut. Ich hoffe, dieser Artikel hilft Ihnen bei der schwierigen Aufgabe, die Grundlagen der Prozessautomatisierung und deren Optimierung in Ihrer Infrastruktur zu verstehen.

PS: Der Autor dankt allen Mitgliedern der Gemeinde für ihre Hilfe und Unterstützung bei der Vorbereitung des Artikels. Sogar die mit Pfoten. Und selbst wer keine Beine hat, wie eine Boa Constrictor.

All Articles