L’écriture de scripts avec PowerShell et l’historique des performances des espaces de stockage direct

S’applique à : Windows Server 2022, Windows Server 2019

Dans Windows Server 2019, les espaces de stockage direct enregistrent et stockent un historique des performances complet pour les machines virtuelles, les serveurs, les lecteurs, les volumes, les cartes réseau, etc. L’historique des performances est facile à interroger et à traiter dans PowerShell, ce qui vous permet de passer rapidement des données brutes aux réponses réelles aux questions telles que :

  1. Y a-t-il eu des pics de processeur la semaine dernière ?
  2. Un disque physique présente-t-il une latence anormale ?
  3. Quelles machines virtuelles consomment actuellement le plus d’E/S par seconde de stockage ?
  4. Ma bande passante réseau est-elle saturée ?
  5. Quand ce volume sera-t-il à court d’espace libre ?
  6. Au cours du mois dernier, quelles machines virtuelles ont utilisé le plus de mémoire ?

L’applet de commande Get-ClusterPerf est créée pour l’écriture de scripts. Il accepte les entrées des applets de commande telles que Get-VM ou Get-PhysicalDisk par le pipeline pour gérer l’association, et vous pouvez diriger sa sortie vers des applets de commande utilitaires telles que Sort-Object, Where-Objectet Measure-Object pour composer rapidement des requêtes puissantes.

Cette rubrique fournit et explique 6 exemples de scripts qui répondent aux 6 questions ci-dessus. Ils présentent des modèles que vous pouvez appliquer pour rechercher des pics, trouver des moyennes, tracer des lignes de tendance, détecter les valeurs hors norme et bien plus encore, sur une variété de données et de délais. Ils sont fournis en tant que code de démarrage gratuit pour vous permettre de copier, d’étendre et de réutiliser.

Notes

Par souci de concision, les exemples de scripts omettent des éléments tels que la gestion des erreurs que vous pouvez attendre du code PowerShell de haute qualité. Ils sont destinés principalement à l’inspiration et à l’éducation plutôt qu’à l’utilisation de la production.

Exemple 1 : PROCESSEUR, je te vois !

Cet exemple utilise la série ClusterNode.Cpu.Usage de la période LastWeek pour afficher l’utilisation maximale (« borne haute »), l’utilisation minimale et moyenne du processeur pour chaque serveur du cluster. Il effectue également une analyse de quartile simple pour montrer combien d’heures d’utilisation du processeur a dépassé 25 %, 50 % et 75 % au cours des 8 derniers jours.

Capture d'écran

Dans la capture d’écran ci-dessous, nous voyons que Server-02 a connu un pic inexpliqué la semaine dernière :

Screenshot that shows that Server-02 had an unexplained spike last week.

Fonctionnement

La sortie correcte des canaux Get-ClusterPerf dans l’applet de commande Measure-Object intégrée, nous spécifions simplement la propriété Value. Avec ses indicateurs -Maximum, -Minimumet -Average, Measure-Object nous donne les trois premières colonnes presque gratuitement. Pour effectuer l’analyse des quartiles, nous pouvons diriger vers Where-Object et compter le nombre de valeurs qui ont été -Gt (supérieures à) 25, 50 ou 75. La dernière étape consiste à embellir les fonctions Format-Hours et Format-Percent d’assistance, certainement facultatives.

Script

Voici le script :

Function Format-Hours {
    Param (
        $RawValue
    )
    # Weekly timeframe has frequency 15 minutes = 4 points per hour
    [Math]::Round($RawValue/4)
}

Function Format-Percent {
    Param (
        $RawValue
    )
    [String][Math]::Round($RawValue) + " " + "%"
}

$Output = Get-ClusterNode | ForEach-Object {
    $Data = $_ | Get-ClusterPerf -ClusterNodeSeriesName "ClusterNode.Cpu.Usage" -TimeFrame "LastWeek"

    $Measure = $Data | Measure-Object -Property Value -Minimum -Maximum -Average
    $Min = $Measure.Minimum
    $Max = $Measure.Maximum
    $Avg = $Measure.Average

    [PsCustomObject]@{
        "ClusterNode"    = $_.Name
        "MinCpuObserved" = Format-Percent $Min
        "MaxCpuObserved" = Format-Percent $Max
        "AvgCpuObserved" = Format-Percent $Avg
        "HrsOver25%"     = Format-Hours ($Data | Where-Object Value -Gt 25).Length
        "HrsOver50%"     = Format-Hours ($Data | Where-Object Value -Gt 50).Length
        "HrsOver75%"     = Format-Hours ($Data | Where-Object Value -Gt 75).Length
    }
}

$Output | Sort-Object ClusterNode | Format-Table

Exemple 2 : Incendie, incendie, anomalie de latence

Cet exemple utilise la série PhysicalDisk.Latency.Average de la période LastHour pour rechercher des valeurs statistiques hors norme, définies comme des lecteurs dont la latence moyenne horaire dépasse +3σ (trois écarts-types) au-dessus de la moyenne de la population.

Important

Par souci de concision, ce script n’implémente pas de protections contre une faible variance, ne gère pas les données manquantes partielles, ne se distingue pas par modèle ou microprogramme, etc. Faites preuve de bon sens et ne vous fiez pas uniquement à ce script pour déterminer s’il faut remplacer un disque dur. Il n’est présenté ici qu’à des fins éducatives.

Capture d'écran

Dans la capture d’écran ci-dessous, nous voyons qu’il n’y a pas de valeurs hors norme :

Screenshot that shows there are no outliers.

Fonctionnement

Tout d’abord, nous excluons les lecteurs inactifs ou presque inactifs en vérifiant que PhysicalDisk.Iops.Total est systématiquement -Gt 1. Pour chaque disque dur actif, nous dirigeons sa durée LastHour, composée de 360 mesures à intervalles de 10 secondes, pour Measure-Object -Average pour obtenir sa latence moyenne au cours de la dernière heure. Cela met en place notre population.

Nous implémentons la formule largement connue pour trouver la moyenne μ et l’écart-type σ de la population. Pour chaque disque dur actif, nous comparons sa latence moyenne à la moyenne de la population et nous divisons par l’écart-type. Nous conservons les valeurs brutes afin de pouvoir Sort-Object nos résultats, mais nous utilisons les fonctions d’assistance Format-Latency et Format-StandardDeviation pour embellir ce que nous allons afficher, certainement facultatif.

Si un lecteur est supérieur à +3σ, nous Write-Host en rouge ; sinon, en vert.

Script

Voici le script :

Function Format-Latency {
    Param (
        $RawValue
    )
    $i = 0 ; $Labels = ("s", "ms", "μs", "ns") # Petabits, just in case!
    Do { $RawValue *= 1000 ; $i++ } While ( $RawValue -Lt 1 )
    # Return
    [String][Math]::Round($RawValue, 2) + " " + $Labels[$i]
}

Function Format-StandardDeviation {
    Param (
        $RawValue
    )
    If ($RawValue -Gt 0) {
        $Sign = "+"
    }
    Else {
        $Sign = "-"
    }
    # Return
    $Sign + [String][Math]::Round([Math]::Abs($RawValue), 2) + "σ"
}

$HDD = Get-StorageSubSystem Cluster* | Get-PhysicalDisk | Where-Object MediaType -Eq HDD

$Output = $HDD | ForEach-Object {

    $Iops = $_ | Get-ClusterPerf -PhysicalDiskSeriesName "PhysicalDisk.Iops.Total" -TimeFrame "LastHour"
    $AvgIops = ($Iops | Measure-Object -Property Value -Average).Average

    If ($AvgIops -Gt 1) { # Exclude idle or nearly idle drives

        $Latency = $_ | Get-ClusterPerf -PhysicalDiskSeriesName "PhysicalDisk.Latency.Average" -TimeFrame "LastHour"
        $AvgLatency = ($Latency | Measure-Object -Property Value -Average).Average

        [PsCustomObject]@{
            "FriendlyName"  = $_.FriendlyName
            "SerialNumber"  = $_.SerialNumber
            "MediaType"     = $_.MediaType
            "AvgLatencyPopulation" = $null # Set below
            "AvgLatencyThisHDD"    = Format-Latency $AvgLatency
            "RawAvgLatencyThisHDD" = $AvgLatency
            "Deviation"            = $null # Set below
            "RawDeviation"         = $null # Set below
        }
    }
}

If ($Output.Length -Ge 3) { # Minimum population requirement

    # Find mean μ and standard deviation σ
    $μ = ($Output | Measure-Object -Property RawAvgLatencyThisHDD -Average).Average
    $d = $Output | ForEach-Object { ($_.RawAvgLatencyThisHDD - $μ) * ($_.RawAvgLatencyThisHDD - $μ) }
    $σ = [Math]::Sqrt(($d | Measure-Object -Sum).Sum / $Output.Length)

    $FoundOutlier = $False

    $Output | ForEach-Object {
        $Deviation = ($_.RawAvgLatencyThisHDD - $μ) / $σ
        $_.AvgLatencyPopulation = Format-Latency $μ
        $_.Deviation = Format-StandardDeviation $Deviation
        $_.RawDeviation = $Deviation
        # If distribution is Normal, expect >99% within 3σ
        If ($Deviation -Gt 3) {
            $FoundOutlier = $True
        }
    }

    If ($FoundOutlier) {
        Write-Host -BackgroundColor Black -ForegroundColor Red "Oh no! There's an HDD significantly slower than the others."
    }
    Else {
        Write-Host -BackgroundColor Black -ForegroundColor Green "Good news! No outlier found."
    }

    $Output | Sort-Object RawDeviation -Descending | Format-Table FriendlyName, SerialNumber, MediaType, AvgLatencyPopulation, AvgLatencyThisHDD, Deviation

}
Else {
    Write-Warning "There aren't enough active drives to look for outliers right now."
}

Exemple 3 : Voisin bruyant ? Exact !

L’historique des performances peut également répondre à des questions sur le moment présent. De nouvelles mesures sont disponibles en temps réel, toutes les 10 secondes. Cet exemple utilise la série VHD.Iops.Total de la période MostRecent pour identifier les machines virtuelles les plus occupées (ou « les plus bruyantes ») qui consomment le plus d’E/S par seconde de stockage, sur chaque hôte du cluster, et montrer la répartition en Lecture-écriture de leur activité.

Capture d'écran

Dans la capture d’écran ci-dessous, nous voyons le top 10 des machines virtuelles par activité de stockage :

Screenshot that shows the Top 10 virtual machines by storage activity.

Fonctionnement

Contrairement à Get-PhysicalDisk, l’applet de commande Get-VM ne prend pas en charge le cluster : elle retourne uniquement les machines virtuelles sur le serveur local. Pour interroger à partir de chaque serveur en parallèle, nous incluons dans un wrapper notre appel dans Invoke-Command (Get-ClusterNode).Name { ... }. Pour chaque machine virtuelle, nous obtenons les mesures VHD.Iops.Total, VHD.Iops.Readet VHD.Iops.Write. En ne spécifiant pas le paramètre -TimeFrame, nous obtenons le point de données MostRecent unique pour chacun d’eux.

Conseil

Ces séries reflètent l’ensemble de l’activité de cette machine virtuelle à tous ses fichiers VHD/VHDX. Il s’agit d’un exemple où l’historique des performances est automatiquement agrégé pour nous. Pour obtenir la répartition par VHD/VHDX, vous pouvez diriger un individu Get-VHD vers Get-ClusterPerf plutôt que vers la machine virtuelle.

Les résultats de chaque serveur sont regroupés sous la forme $Output, ce que nous pouvons Sort-Object, puis Select-Object -First 10. Notez que Invoke-Command décore les résultats avec une propriété PsComputerName indiquant d’où ils proviennent, que nous pouvons imprimer pour savoir où la machine virtuelle s’exécute.

Script

Voici le script :

$Output = Invoke-Command (Get-ClusterNode).Name {
    Function Format-Iops {
        Param (
            $RawValue
        )
        $i = 0 ; $Labels = (" ", "K", "M", "B", "T") # Thousands, millions, billions, trillions...
        Do { if($RawValue -Gt 1000){$RawValue /= 1000 ; $i++ } } While ( $RawValue -Gt 1000 )
        # Return
        [String][Math]::Round($RawValue) + " " + $Labels[$i]
    }

    Get-VM | ForEach-Object {
        $IopsTotal = $_ | Get-ClusterPerf -VMSeriesName "VHD.Iops.Total"
        $IopsRead  = $_ | Get-ClusterPerf -VMSeriesName "VHD.Iops.Read"
        $IopsWrite = $_ | Get-ClusterPerf -VMSeriesName "VHD.Iops.Write"
        [PsCustomObject]@{
            "VM" = $_.Name
            "IopsTotal" = Format-Iops $IopsTotal.Value
            "IopsRead"  = Format-Iops $IopsRead.Value
            "IopsWrite" = Format-Iops $IopsWrite.Value
            "RawIopsTotal" = $IopsTotal.Value # For sorting...
        }
    }
}

$Output | Sort-Object RawIopsTotal -Descending | Select-Object -First 10 | Format-Table PsComputerName, VM, IopsTotal, IopsRead, IopsWrite

Exemple 4 : les 25 gigas ont détrôné les 10 gigas

Cet exemple utilise la série NetAdapter.Bandwidth.Total de la période LastDay pour rechercher les signes de saturation du réseau, défini comme >90 % de la bande passante maximale théorique. Pour chaque carte réseau du cluster, il compare l’utilisation la plus élevée de la bande passante observée au cours du dernier jour à la vitesse de liaison indiquée.

Capture d'écran

Dans la capture d’écran ci-dessous, nous voyons qu’un Fabrikam NX-4 Pro #2 a atteint un pic le dernier jour :

Screenshot that shows that Fabrikam NX-4 Pro #2 peaked in the last day.

Fonctionnement

Nous répétons notre astuce Invoke-Command de ci-dessus à Get-NetAdapter sur chaque serveur et nous dirigeons vers Get-ClusterPerf. En cours de route, nous récupérons deux propriétés pertinentes : sa chaîne LinkSpeed comme « 10 Gbit/s » et son entier brut Speed comme 10000000000. Nous utilisons Measure-Object pour obtenir la moyenne et le pic du dernier jour (rappel : chaque mesure dans la période LastDay représente 5 minutes) et multiplier par 8 bits par octet pour obtenir une comparaison « point à point ».

Notes

Certains fournisseurs, comme Chelsio, incluent l’activité d’accès direct à la mémoire à distance (RDMA) dans leurs compteurs de performances de carte réseau, de sorte qu’elle est incluse dans la série NetAdapter.Bandwidth.Total. D’autres, comme Mellanox, peuvent ne pas le faire. Si ce n’est pas le cas de votre fournisseur, ajoutez simplement la série NetAdapter.Bandwidth.RDMA.Total dans votre version de ce script.

Script

Voici le script :

$Output = Invoke-Command (Get-ClusterNode).Name {

    Function Format-BitsPerSec {
        Param (
            $RawValue
        )
        $i = 0 ; $Labels = ("bps", "kbps", "Mbps", "Gbps", "Tbps", "Pbps") # Petabits, just in case!
        Do { $RawValue /= 1000 ; $i++ } While ( $RawValue -Gt 1000 )
        # Return
        [String][Math]::Round($RawValue) + " " + $Labels[$i]
    }

    Get-NetAdapter | ForEach-Object {

        $Inbound = $_ | Get-ClusterPerf -NetAdapterSeriesName "NetAdapter.Bandwidth.Inbound" -TimeFrame "LastDay"
        $Outbound = $_ | Get-ClusterPerf -NetAdapterSeriesName "NetAdapter.Bandwidth.Outbound" -TimeFrame "LastDay"

        If ($Inbound -Or $Outbound) {

            $InterfaceDescription = $_.InterfaceDescription
            $LinkSpeed = $_.LinkSpeed

            $MeasureInbound = $Inbound | Measure-Object -Property Value -Maximum
            $MaxInbound = $MeasureInbound.Maximum * 8 # Multiply to bits/sec

            $MeasureOutbound = $Outbound | Measure-Object -Property Value -Maximum
            $MaxOutbound = $MeasureOutbound.Maximum * 8 # Multiply to bits/sec

            $Saturated = $False

            # Speed property is Int, e.g. 10000000000
            If (($MaxInbound -Gt (0.90 * $_.Speed)) -Or ($MaxOutbound -Gt (0.90 * $_.Speed))) {
                $Saturated = $True
                Write-Warning "In the last day, adapter '$InterfaceDescription' on server '$Env:ComputerName' exceeded 90% of its '$LinkSpeed' theoretical maximum bandwidth. In general, network saturation leads to higher latency and diminished reliability. Not good!"
            }

            [PsCustomObject]@{
                "NetAdapter"  = $InterfaceDescription
                "LinkSpeed"   = $LinkSpeed
                "MaxInbound"  = Format-BitsPerSec $MaxInbound
                "MaxOutbound" = Format-BitsPerSec $MaxOutbound
                "Saturated"   = $Saturated
            }
        }
    }
}

$Output | Sort-Object PsComputerName, InterfaceDescription | Format-Table PsComputerName, NetAdapter, LinkSpeed, MaxInbound, MaxOutbound, Saturated

Exemple 5 : Le stockage revient en force !

Pour examiner les tendances macro, l’historique des performances est conservé pendant 1 an maximum. Cet exemple utilise les séries Volume.Size.Availablede la période LastYear pour déterminer le taux de remplissage du stockage et estimer quand il sera plein.

Capture d'écran

Dans la capture d’écran ci-dessous, nous voyons que le volume de sauvegarde ajoute environ 15 Go par jour :

Screenshot that shows that the Backup volume is adding about 15 GB per day.

À ce rythme, il atteindra sa capacité dans encore 42 jours.

Fonctionnement

La période LastYear a un point de données par jour. Bien que vous n’ayez strictement besoin que de deux points pour vous adapter à une ligne de tendance, dans la pratique, il est préférable d’en exiger plus, comme 14 jours. Nous utilisons Select-Object -Last 14 pour configurer un tableau de points (x, y), pour x dans la plage [1, 14]. Avec ces points, nous implémentons l’algorithme linéaire simple des moindres carrés pour rechercher $A et $B qui paramètre la ligne de meilleur ajustement y = ax + b. Faisons un petit retour au lycée.

La division de la propriété SizeRemaining du volume par la tendance (la pente $A) nous permet d’estimer grossièrement le nombre de jours, au rythme actuel de croissance du stockage, jusqu’à ce que le volume soit plein. Les fonctions d’assistance Format-Bytes, Format-Trendet Format-Days embellissent la sortie.

Important

Cette estimation est linéaire et basée uniquement sur les 14 mesures quotidiennes les plus récentes. Il existe des techniques plus sophistiquées et plus précises. Faites preuve de bon sens et ne vous fiez pas uniquement à ce script pour déterminer s’il faut investir dans l’expansion de votre stockage. Il n’est présenté ici qu’à des fins éducatives.

Script

Voici le script :


Function Format-Bytes {
    Param (
        $RawValue
    )
    $i = 0 ; $Labels = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
    Do { $RawValue /= 1024 ; $i++ } While ( $RawValue -Gt 1024 )
    # Return
    [String][Math]::Round($RawValue) + " " + $Labels[$i]
}

Function Format-Trend {
    Param (
        $RawValue
    )
    If ($RawValue -Eq 0) {
        "0"
    }
    Else {
        If ($RawValue -Gt 0) {
            $Sign = "+"
        }
        Else {
            $Sign = "-"
        }
        # Return
        $Sign + $(Format-Bytes ([Math]::Abs($RawValue))) + "/day"
    }
}

Function Format-Days {
    Param (
        $RawValue
    )
    [Math]::Round($RawValue)
}

$CSV = Get-Volume | Where-Object FileSystem -Like "*CSV*"

$Output = $CSV | ForEach-Object {

    $N = 14 # Require 14 days of history

    $Data = $_ | Get-ClusterPerf -VolumeSeriesName "Volume.Size.Available" -TimeFrame "LastYear" | Sort-Object Time | Select-Object -Last $N

    If ($Data.Length -Ge $N) {

        # Last N days as (x, y) points
        $PointsXY = @()
        1..$N | ForEach-Object {
            $PointsXY += [PsCustomObject]@{ "X" = $_ ; "Y" = $Data[$_-1].Value }
        }

        # Linear (y = ax + b) least squares algorithm
        $MeanX = ($PointsXY | Measure-Object -Property X -Average).Average
        $MeanY = ($PointsXY | Measure-Object -Property Y -Average).Average
        $XX = $PointsXY | ForEach-Object { $_.X * $_.X }
        $XY = $PointsXY | ForEach-Object { $_.X * $_.Y }
        $SSXX = ($XX | Measure-Object -Sum).Sum - $N * $MeanX * $MeanX
        $SSXY = ($XY | Measure-Object -Sum).Sum - $N * $MeanX * $MeanY
        $A = ($SSXY / $SSXX)
        $B = ($MeanY - $A * $MeanX)
        $RawTrend = -$A # Flip to get daily increase in Used (vs decrease in Remaining)
        $Trend = Format-Trend $RawTrend

        If ($RawTrend -Gt 0) {
            $DaysToFull = Format-Days ($_.SizeRemaining / $RawTrend)
        }
        Else {
            $DaysToFull = "-"
        }
    }
    Else {
        $Trend = "InsufficientHistory"
        $DaysToFull = "-"
    }

    [PsCustomObject]@{
        "Volume"     = $_.FileSystemLabel
        "Size"       = Format-Bytes ($_.Size)
        "Used"       = Format-Bytes ($_.Size - $_.SizeRemaining)
        "Trend"      = $Trend
        "DaysToFull" = $DaysToFull
    }
}

$Output | Format-Table

Exemple 6 : Les gros consommateurs de mémoire, l’heure de l’exécution a sonné !

Étant donné que l’historique des performances est collecté et stocké de manière centralisée pour l’ensemble du cluster, vous n’avez jamais besoin d’assembler des données provenant de différentes machines, quel que soit le nombre de fois où des machines virtuelles se déplacent entre des hôtes. Cet exemple utilise la série VM.Memory.Assigned de la période LastMonth pour identifier les machines virtuelles qui consomment le plus de mémoire au cours des 35 derniers jours.

Capture d'écran

Dans la capture d’écran ci-dessous, nous voyons le top 10 des machines virtuelles par utilisation de la mémoire le mois dernier :

Screenshot of PowerShell

Fonctionnement

Nous répétons notre astuce Invoke-Command, présentée ci-dessus, à Get-VM sur chaque serveur. Nous utilisons Measure-Object -Average pour obtenir la moyenne mensuelle pour chaque machine virtuelle, puis Sort-Object et Select-Object -First 10 pour obtenir notre classement. (Ou peut-être qu’il s’agit de notre liste des plus recherchés ?)

Script

Voici le script :

$Output = Invoke-Command (Get-ClusterNode).Name {
    Function Format-Bytes {
        Param (
            $RawValue
        )
        $i = 0 ; $Labels = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
        Do { if( $RawValue -Gt 1024 ){ $RawValue /= 1024 ; $i++ } } While ( $RawValue -Gt 1024 )
        # Return
        [String][Math]::Round($RawValue) + " " + $Labels[$i]
    }

    Get-VM | ForEach-Object {
        $Data = $_ | Get-ClusterPerf -VMSeriesName "VM.Memory.Assigned" -TimeFrame "LastMonth"
        If ($Data) {
            $AvgMemoryUsage = ($Data | Measure-Object -Property Value -Average).Average
            [PsCustomObject]@{
                "VM" = $_.Name
                "AvgMemoryUsage" = Format-Bytes $AvgMemoryUsage.Value
                "RawAvgMemoryUsage" = $AvgMemoryUsage.Value # For sorting...
            }
        }
    }
}

$Output | Sort-Object RawAvgMemoryUsage -Descending | Select-Object -First 10 | Format-Table PsComputerName, VM, AvgMemoryUsage

Et voilà ! Espérons que ces exemples vous inspirent et vous aideront à démarrer. Avec l’historique des performances des espaces de stockage direct et l’applet de commande Get-ClusterPerf puissante et adaptée à l’écriture de scripts, vous pouvez poser des questions complexes, et y répondre ! à mesure que vous gérez et surveillez votre infrastructure Windows Server 2019.

Références supplémentaires