使用 PowerShell 和存储空间直通性能历史记录编写脚本

适用于:Windows Server 2022、Windows Server 2019

在 Windows Server 2019 中,存储空间直通记录并存储虚拟机、服务器、驱动器、卷、网络适配器等的大量性能历史记录。 在 PowerShell 中可以轻松查询和处理性能历史记录,因此你可以快速从原始数据中得到如下所述问题的实际答案

  1. 上周是否出现了任何 CPU 高峰?
  2. 是否有任何物理磁盘出现异常的延迟?
  3. 哪些 VM 目前消耗的存储 IOPS 最多?
  4. 我的网络带宽是否饱和?
  5. 此卷何时会耗尽可用空间?
  6. 在过去一个月,哪些 VM 使用的内存最多?

Get-ClusterPerf cmdlet 旨在用于编写脚本。 它通过管道接受来自 Get-VMGet-PhysicalDisk 等 cmdlet 的输入以处理关联,你可以将其输出传送到 Sort-ObjectWhere-ObjectMeasure-Object 等实用工具 cmdlet,以快速编写强大的查询。

本主题将提供并解释 6 个示例脚本来回答上述 6 个问题。 这些脚本提供了一些模式,你可以应用这些模式以基于各种数据和时间范围查找峰值、查找平均值、绘制趋势线、运行离群值检测,等等。 它们作为免费的入门代码提供,允许你复制、扩充和重用。

注意

为简洁起见,示例脚本中省略了你可能预期高质量 PowerShell 代码提供的一些功能,例如错误处理。 它们主要用于激发灵感和用户教育,而不是用于生产目的。

示例 1:CPU,我看到你了!

此示例使用 LastWeek 时间范围内的 ClusterNode.Cpu.Usage 系列来显示群集中每台服务器的最大(“高水位线”)、最小和平均 CPU 使用率。 它还执行简单的四分位数分析,以显示过去 8 天内 CPU 使用率超过 25%、50% 和 75% 的小时数。

屏幕快照

在以下屏幕截图中,我们看到 Server-02 在上周出现了无法解释的高峰

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

工作原理

Get-ClusterPerf 的输出可以顺利地传送到内置的 Measure-Object cmdlet,我们只需指定 Value 属性即可。 指定 -Maximum-Minimum-Average 标志后,Measure-Object 毫不费力地为我们提供了前三列。 若要执行四分位数分析,我们可以将参数传送给 Where-Object 并统计 -Gt(大于)25、50 或 75 的值有多少个。 最后一步是使用 Format-HoursFormat-Percent 帮助器函数进行美化 – 当然这是可选操作。

脚本

脚本如下:

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

示例 2:不好啦,有延迟离群值

此示例使用 LastHour 时间范围内的 PhysicalDisk.Latency.Average 系列来查找统计离群值,这些离群值定义为每小时平均延迟比群体平均值高于 3σ(三个标准偏差)以上的驱动器。

重要

为简洁起见,此脚本不针对较低方差实施保护措施,不处理部分缺失的数据,不区分型号或固件等。请运用良好的判断,而不要依赖此脚本来确定是否更换硬盘。 此脚本仅用于教育目的。

屏幕快照

在以下屏幕截图中,我们没有看到离群值:

Screenshot that shows there are no outliers.

工作原理

首先,我们通过检查 PhysicalDisk.Iops.Total 是否持续为 -Gt 1 来排除空闲或接近空闲的驱动器。 对于每个处于活动状态的 HDD,我们将其 LastHour 时间范围(由 10 秒间隔的 360 个测量值组成)通过传送给 Measure-Object -Average,以获得其在过去一小时的平均延迟。 这就建立了我们的群体。

我们使用广为人知的公式来查找群体的平均值 μ 和标准偏差 σ。 对于每个处于活动状态的 HDD,我们将其平均延迟与群体平均值进行比较,然后除以标准偏差。 我们保留原始值,以便可以对结果运行 Sort-Object,但使用 Format-LatencyFormat-StandardDeviation 帮助器函数来美化显示的内容 – 当然这是可选操作。

如果任一驱动器的延迟超过 3σ 以上,则以红色将它标识为 Write-Host;否则标识为绿色。

脚本

脚本如下:

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."
}

示例 3:近邻干扰? 那是写入!

性能历史记录也可以近实时回答问题。 每隔 10 秒就会实时提供新的测量值。 此示例使用 MostRecent 时间范围内的 VHD.Iops.Total 系列来识别群集中每个主机上消耗最多存储 IOPS 的最繁忙(有时称为“最嘈杂”)虚拟机,并显示它们的读/写活动明细。

屏幕快照

以下屏幕截图显示了按存储活动列出的排名前 10 的虚拟机:

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

工作原理

Get-PhysicalDisk 不同,Get-VM cmdlet 无法感知群集 – 它只返回本地服务器上的 VM。 为了从每个服务器并行查询,我们将调用包装在 Invoke-Command (Get-ClusterNode).Name { ... } 中。 对于每个 VM,我们将获得 VHD.Iops.TotalVHD.Iops.ReadVHD.Iops.Write 测量值。 如果不指定 -TimeFrame 参数的话,我们将获得每个 VM 的 MostRecent 单一数据点。

提示

这些系列反映了此 VM 对其所有 VHD/VHDX 文件执行的活动的总和。 在此示例中,系统为我们自动聚合了性能历史记录。 若要获取每个 VHD/VHDX 的明细,可以将单个 Get-VHD 传送到 Get-ClusterPerf 而不是 VM。

来自每个服务器的结果作为 $Output 一起返回,我们可以对其运行 Sort-Object,然后运行 Select-Object -First 10。 请注意,Invoke-Command 使用 PsComputerName 属性(指示结果的来源位置)修饰结果,我们可以输出这种结果以了解 VM 在哪个位置运行。

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

示例 4:他们说,“25 Gbps 的带宽现在变成了 10 Gbps”

此示例使用 LastDay 时间范围内的 NetAdapter.Bandwidth.Total 系列来查找网络饱和迹象,这种迹象定义为超过理论最大带宽的 90%。 对于群集中的每个网络适配器,它会将过去一天内观察到的最高带宽使用率与此适配器声称的链接速度进行比较。

屏幕快照

在以下屏幕截图中,我们看到 Fabrikam NX-4 Pro #2 在过去一天出现峰值

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

工作原理

我们重复上述 Invoke-Command 技巧,对每台服务器运行 Get-NetAdapter 并将结果传送给 Get-ClusterPerf。 在此过程中,我们抓取了两个相关属性:LinkSpeed 字符串(例如“10 Gbps”,以及原始 Speed 整数(例如 10000000000)。 我们使用 Measure-Object 获取过去一天的平均值和峰值(提醒:LastDay 时间范围内的每个度量代表 5 分钟)并乘以每字节 8 位,以进行对等比较。

注意

某些供应商(例如 Chelsio)在其网络适配器性能计数器中包含远程直接内存访问 (RDMA) 活动,因此该活动包含在 NetAdapter.Bandwidth.Total 系列中。 其他供应商(例如 Mellanox)可能不会这样。 如果你的供应商不是这样,你只需在此脚本的版本中添加 NetAdapter.Bandwidth.RDMA.Total 系列即可。

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

示例 5:让存储再次瘦身!

为了查看宏观趋势,性能历史记录最长会保留 1 年。 此示例使用 LastYear 时间范围内的 Volume.Size.Available 系列来判断存储空间的填充速度并估计它何时会填满。

屏幕快照

在以下屏幕截图中,我们看到“备份”卷上占用的空间每日增加大约 15 GB

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

按照此速度,再过 42 天,它的容量就会填满。

工作原理

LastYear 时间范围每日有一个数据点。 虽然严格地说,只需绘制两个点就能拟合一条趋势线,但在实践中,最好要求绘制更多的点,例如绘制 14 天的点。 我们使用 Select-Object -Last 14 设置 (x, y) 点的数组,x 在 [1, 14] 范围内。 有了这些点,就可以实现简单的线性最小二乘算法来查找参数化最佳拟合线 y = ax + b 的 $A$B。 欢迎回到高中复读。

将卷的 SizeRemaining 属性除以趋势(斜率 $A),可以根据当前存储占用量的增长速度,粗略地估计该卷在几天后将被填满。 Format-BytesFormat-TrendFormat-Days 帮助器函数美化输出。

重要

此估计值是线性的,仅基于最近的 14 次每日测量。 还有其他更高深、更准确的方法。 请运用良好的判断,而不要依赖此脚本来确定是否投资扩展你的存储。 此脚本仅用于教育目的。

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

示例 6:内存大户,躲得过初一,躲不过十五

由于性能历史记录是为整个群集统一收集和存储的,因此无论 VM 在主机之间移动多少次,你都不需要将不同计算机中的数据拼接在一起。 此示例使用 LastMonth 时间范围内的 VM.Memory.Assigned 系列来识别过去 35 天内消耗最多内存的虚拟机。

屏幕快照

以下屏幕截图显示了上个月内存使用量排名前 10 的虚拟机:

Screenshot of PowerShell

工作原理

重复上面介绍的 Invoke-Command 技巧在每台服务器上运行 Get-VM。 我们使用 Measure-Object -Average 获取每个 VM 的每月平均值,然后依次使用 Sort-ObjectSelect-Object -First 10 获取榜单。 (或许这就是我们的“头号通缉犯”名单?)

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

就这么简单! 希望这些示例能够激发你的灵感并帮助你入门。 借助存储空间直通性能历史记录和强大、脚本友好的 Get-ClusterPerf cmdlet,你可以提问和回答问题! – 管理和监视 Windows Server 2019 基础结构时遇到的复杂问题。

其他参考