PowerShell Performance Counter

Performancecounter gibt es in Windows schon seit Windows NT 3.x und sie sind das Gegenstück zu SNMP auf Unix.

Performancecounter lesen

Seit PowerShell 2 gibt es nun auch ein "Get-Counter"-Commandlet, welches einen direkten lesenden Zugriff auf die Performance Counter erlaubt:

PS> (Get-Counter "\prozessor(_total)\prozessorzeit (%)").countersamples[0]

Path                       InstanceName                             CookedValue
----                       ------------                             -----------
\\nawnbfc\prozessor(_to... _total                              4,24059311090853

Natürlich kann man auch über das .NET-Framework auf Performancecounter zugreifen, was anscheinend auch schneller aber vor allem einfacher ist, als der WMI-Zugriff. Hier ein Muster auf einem englischen Server:

[PS] $perf = new-object System.Diagnostics.PerformanceCounter
[PS] $perf.countername = "% Processor Time"
[PS] $perf.Categoryname = "Processor"
[PS] $perf.Instancename = "_Total"
 
oder
 
[PS] $perf = new-object System.Diagnostics.PerformanceCounter "Processor","% Processor Time", "_Total"

[PS] $perf
 
CategoryName     : Processor
CounterHelp      : % Processor Time is the percentage of elapsed time that the
                   processor spends to execute a non-Idle thread. It is calcula
                   ted by measuring the duration of the idle thread is active i
                   n the sample interval, and subtracting that time from interv
                   al duration.  (Each processor has an idle thread that consum
                   es cycles when no other threads are ready to run). This coun
                   ter is the primäry indicator of processor activity, and disp
                   lays the average percentage of busy time observed during the
                    sample interval. It is calculated by monitoring the time th
                   at the service is inactive, and subtracting that value from
                   100%.
CounterName      : % Processor Time
CounterType      : Timer100NsInverse
InstanceLifetime : Global
InstanceName     : _Total
ReadOnly         : True
MachineName      : .
RawValue         : 36131046250000
Site             :
Container        :

Das ist ja noch einfach, so lange es genau ein Counter ist und die Namen als auch Instanzen gleich sind. Nun gibt es aber Performancecounter, die mehrere Instanzen haben, z.B.: die Exchange 2007 CCR Replikation. Da muss man sich dann selbst durch die Instanzen arbeiten.

[PS] $perfcat = new-object System.Diagnostics.PerformanceCounterCategory("MSExchange Replication")
[PS] $perfcat.GetInstanceNames() 
      | %{$perfcat.GetCounters($_)} 
      | ft Cat*,countername,Instancename,rawvalue
 
CategoryName        CounterName         InstanceName             RawValue
------------        -----------         ------------             --------
MSExchange Repli... CopyNotification... _total                     83317
MSExchange Repli... CopyGenerationNu... _total                     83317
MSExchange Repli... InspectorGenerat... _total                     83317
MSExchange Repli... ReplayNotificati... _total                     83317
MSExchange Repli... ReplayGeneration... _total                     83317
MSExchange Repli... ReplayQueueLength   _total                         0
MSExchange Repli... Suspended           _total                         0
MSExchange Repli... TrUNCatedGenerat... _total                         0
MSExchange Repli... CopyNotification... s1 sg1                     83317
MSExchange Repli... CopyGenerationNu... s1 sg1                     83317
MSExchange Repli... InspectorGenerat... s1 sg1                     83317
MSExchange Repli... ReplayNotificati... s1 sg1                     83317
MSExchange Repli... ReplayGeneration... s1 sg1                     83317
MSExchange Repli... ReplayQueueLength   s1 sg1                         0
MSExchange Repli... ReplayBatchSize     s1 sg1                         0
MSExchange Repli... CopyQueueLength     s1 sg1                         0

Aber auch das geht mit Get-Counter, man muss nur etwas geschickter die Abfrage stellen. Hier z.B. für die Anzahl der Handles aller Prozesse

(get-counter "\Process(*)\handle count" ).countersamples

Path                                    InstanceName                CookedValue
----                                    ------------                -----------
\\w2k8r2e2010\process(idle)\handle c... idle                                  0
\\w2k8r2e2010\process(system)\handle... system                              717
\\w2k8r2e2010\process(smss)\handle c... smss                                 32
\\w2k8r2e2010\process(csrss#2)\handl... csrss                                72
\\w2k8r2e2010\process(csrss#1)\handl... csrss                               293

Eine Filterung ist dann mit "Where" möglich oder sie suchen genau einen bestimmten Wert. Vielleicht hilft es auch einfach einmal alle Counter auf einem PC aufzulisten:

#Alle Counter listen 
Get-Counter "\*\*"

# Alle Counter einer Gruppe mit allen Instanzen lauflisten
Get-Counter "\MSExchange Replication(*)\*"

# Alle Counter einer bekannten Instanz auslesen
Get-Counter "\MSExchange Replication(_total)\*

# einen einzelnen Counter
Get-Counter "\MSExchange Replication(_total)\log copy kb/sec"

Achtung: Der String des Counters ist "Case sensibel"

Performance Counter und Serversprache

Die Lokalisierung von Strings findet sich bei Windows leider auch bei den Performance Counter wieder. Daher muss die Abfrage des String sich immer auch an der Landessprache orientieren. Aber es gibt natürlich eine Möglichkeit, von dem englischen Namen auf den lokalen Namen zu schließen.

.. If the name of the objects and counters is known only as an English string in the application, there are some additional steps that you must perform on a system that uses a localized language, such as French or German.
Quelle: Using PDH APIs correctly in a localized language https://support.microsoft.com/en-us/help/287159/using-pdh-apis-correctly-in-a-localized-language 

Die Liste der Performance Counter in englischer Sprache findet sich in einem Registierungsschlüssel:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009\Counter

Diese MultiLine String-Feld enthält alle Instanzen mit einer ID und dem String

Wenn ich also den englischen Namen kenne, kann ich mir so die "Nummer" des Counters holen und dann mit der Nummer und der "Current Language" mit den lokalen Text dazu ermitteln um dann letztlich den Counter zu lesen. Ich finde etwas schade, dass man nicht direkt einfach immer die englischen Beschreibungen nutzen kann. Aber so muss  man eben einen kleinen Umweg machen.

function Get-LocalPerfCounterName {

   param (
      [Parameter(Mandatory=$true)]
      $Name
    )

   $key009 = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009'
   $counters009 = (Get-ItemProperty -Path $key009 -Name Counter).Counter.tolower()
   $Index = $counters009.IndexOf($name.tolower())
   if ($index -eq -1) {
      $null   # not found
   }
   else {
      $keylocal = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\CurrentLanguage'
      $counterslocal = (Get-ItemProperty -Path $keylocal -Name Counter).Counter
      $counterslocal[$index]
   }
}

Über die "ToLower"-Methode mache ich die Suche unabhängig von Groß/Kleinschreibung (Case). Bedenken sie aber, dass ein Performance Counter ja immer aus einem Counternamem und der Unterklasse besteht. Sie müssen die  Funktion daher doppelt aufrufen. hier am Beispiel einer CPU

# Get CPU Load with local Perfmon names

# Normal localized Version
(get-counter "\prozessor(_total)\prozessorzeit (%)").countersamples.cookedValue

# Get CPU Load using english names as source
$counter="Processor"
$Subcounter = "% Processor Time"
$instance = "_Total"

(get-counter "\$(get-LocalPerfCounterName $counter)($($instance))\$(get-LocalPerfCounterName $subcounter)").countersamples.cookedValue

Die eigentliche "Instanz" ist aber nicht lokalisiert. Wer also einen Exchange Information Store ausliest, der findet als Instanzname dann den Namen der Datenbank. Bei generischen Countern ist ein "_total" in der Regel passend oder die Nummer der Instanz

Performancecounter anlegen

Was viele Personen vermutlich seltener verwenden ist die Funktion, eigene Counter anzulegen.

Damit das PowerShell-Skript Performancecounter anlegen kann, muss es als Administrator gestartet werden

# Create performance Counter
#
# Achtung: als ADMIN starten, wenn man den Counter anlegen muss

$categoryName = "MSXFAQCounters"
 
if ([System.Diagnostics.PerformanceCounterCategory]::Exists($categoryName)) {
    write-host "Counter exists"
}
else {
    write-host "Create Counters"
    $counterData = new-object System.Diagnostics.CounterCreationDataCollection
     
    $counter = new-object  System.Diagnostics.CounterCreationData
    $counter.CounterType = [System.Diagnostics.PerformanceCounterType]::AverageCount64
    $counter.CounterName = "64bit Counter Name"
    $counterData.Add($counter)

    # Counter real anlegen
    [System.Diagnostics.PerformanceCounterCategory]::Create( `
           $categoryName, $categoryName, `
           [System.Diagnostics.PerformanceCounterCategoryType]::SingleInstance, $counterData)
}

Natürlich gibt es noch viele andere Countertypen. In Perfmon sind diese Counter dann einfach einzusehen

Offen bleibt dann nur noch die Frage, wie man aus PowerShell auch Werte schreiben kann

Performancecounter schreiben

Auch das ist per PowerShell relativ einfach möglich.

$categoryName = "MSXFAQCounters"
# Counter holen
$Percounter64bit = New-Object System.Diagnostics.PerformanceCounter($categoryName,"64bit Counter Name",$false)

# Aktueller Wert
$Percounter64bit.rawValue

# Wert ändern
$Percounter64bit.rawValue = [int]8

#Neuer Wert
$Percounter64bit.rawValue

 

Offen bleibt dann nur noch die Frage, wie man aus PowerShell auch Werte schreiben kann

 

Performancecounter löschen

Wenn Sie den Counter nicht mehr benötigen, dann sollten Sie diesen natürlich auch wieder korrekt entfernen

# Remove performance Counter
#
# Achtung: als ADMIN starten, wenn man den Counter loeschen will
$categoryName = "MSXFAQCounters"
 
if ([System.Diagnostics.PerformanceCounterCategory]::Exists($categoryName)) {
    [System.Diagnostics.PerformanceCounterCategory]::Delete($categoryName)
}

Hinweis:
Mit diesen wenigen Zeilen können jeden Counter ohne weitere Rückfrage entfernen !

Weitere Links

Auch hierzu finden sich im Internet umfangreiche Beschreibungen