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

适用于:Windows Server 2022、Windows Server 2019

在 Windows Server 2019 中,存储空间直通记录和存储虚拟机、服务器、驱动器、卷、网络适配器等的广泛性能历史记录。 性能历史记录在 PowerShell 中易于查询和处理,因此可以快速从 原始数据 转到 实际解答 ,例如:

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

Get-ClusterPerf cmdlet 是为脚本生成的。 它接受来自 cmdlet(如Get-VMGet-PhysicalDisk管道)的输入来处理关联,并且可以将其输出管道传递给实用工具 cmdlet,Sort-ObjectWhere-ObjectMeasure-Object快速撰写功能强大的查询。

本主题提供并介绍 6 个回答上述 6 个问题的示例脚本。 它们提供可用于查找峰值、查找平均值、绘制趋势线、运行离群检测等各种数据和时间范围的模式。 它们作为免费入门代码提供,供你复制、扩展和重复使用。

注意

为了简洁起见,示例脚本省略了可能需要高质量 PowerShell 代码的错误处理等操作。 它们主要用于灵感和教育,而不是生产用途。

示例 1:CPU,我看到你!

此示例使用 ClusterNode.Cpu.Usage 时间范围中的 LastWeek 系列来显示群集中每个服务器的最大 (“高水印”) 、最小和平均 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 (大于) 25、50 或 75 的值进行 -Gt 管道和计数。 最后一步是美 Format-Hours 化和 Format-Percent 帮助程序函数 - 当然是可选的。

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

示例 2:火灾、触发、延迟离群值

此示例使用 PhysicalDisk.Latency.Average 时间范围中的 LastHour 系列来查找统计离群值,定义为每小时平均延迟超过 +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 红色表示;如果没有,则为绿色。

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

示例 3:干扰邻居? 这是写的!

性能历史记录 也可以回答有关现在的问题。 新度量值实时可用,每 10 秒一次。 此示例使用 VHD.Iops.Total 时间范围 MostRecent 中的系列来识别最繁忙的 (有些人可能会说“最吵”) 虚拟机消耗最多存储 IOPS 的虚拟机,跨群集中的每个主机,并显示其活动的读/写明细。

屏幕快照

在下面的屏幕截图中,我们按存储活动查看前 10 个虚拟机:

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

工作原理

与此 cmdlet 不同 Get-PhysicalDiskGet-VM 它不具有群集感知性 – 它仅返回本地服务器上的 VM。 若要并行查询每个服务器,我们将调用包装在内 Invoke-Command (Get-ClusterNode).Name { ... }。 对于每个 VM,我们获取度量VHD.Iops.TotalVHD.Iops.Read值和VHD.Iops.Write度量值。 如果不指定 -TimeFrame 参数,我们将获取每个参数的 MostRecent 单个数据点。

提示

这些系列反映了此 VM 活动的所有 VHD/VHDX 文件的总和。 这是一个自动聚合性能历史记录的示例。 若要获取每个 VHD/VHDX 细分,可以将单个管道传递给 Get-ClusterPerf VM Get-VHD 而不是 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 gig 是新的 10 gig”

此示例使用 NetAdapter.Bandwidth.Total 时间范围中的 LastDay 系列来查找网络饱和的迹象,定义为 >理论最大带宽的 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 整数(如 100000000000)。 我们用于 Measure-Object 从最后一天获取平均值和峰值 (提醒:时间范围中的每个 LastDay 度量值表示 5 分钟) ,并乘以 8 位/字节以获取苹果到苹果的比较。

注意

某些供应商(如 Chelsio)在其 网络适配器 性能计数器中包含远程直接内存访问 (RDMA) 活动,因此包含在系列中 NetAdapter.Bandwidth.Total 。 其他人,如梅拉诺克斯,可能不是。 如果供应商不这样做,只需在此脚本的版本中添加 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 年。 此示例使用 Volume.Size.Available 时间范围中的 LastYear 系列来确定存储正在填充的速率,并估计何时会满。

屏幕快照

在下面的屏幕截图中,我们看到 备份 卷每天添加约 15 GB:

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

以这种速度,它将在另一个 42 天内达到其容量。

工作原理

时间 LastYear 范围每天有一个数据点。 虽然你只需要两个点来适应趋势线,但在实践中最好需要更多,比如 14 天。 我们用于Select-Object -Last 14为范围 [1, 14] 中的 x 设置 (x、y) 磅的数组。 使用这些点,我们将实现简单的 线性最小平方算法 来查找 $A$B 参数化最适合 y = ax + b 的直线。 欢迎再次上高中。

将卷 SizeRemaining 的属性除以斜率 (斜率 $A) 让我们粗略估计存储增长的当前速度,直到卷满为止。 Format-TrendFormat-DaysFormat-Bytes帮助程序函数美化输出。

重要

此估计值是线性的,仅基于最近的 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 在主机之间移动多少次,都无需将数据从不同的计算机合并在一起。 此示例使用 VM.Memory.Assigned 时间范围中的 LastMonth 系列来标识在过去 35 天内消耗最多内存的虚拟机。

屏幕快照

在下面的屏幕截图中,我们上个月看到前 10 个虚拟机(按内存使用量):

Screenshot of PowerShell

工作原理

在每台服务器上重复 Invoke-Command 上述 Get-VM 技巧。 我们用于 Measure-Object -Average 获取每个 VM 的每月平均值,然后 Sort-Object 获取 Select-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 基础结构时的复杂问题。

其他参考