Scripting con PowerShell y el historial de desempeño de Espacios de almacenamiento directo

Se aplica a: Windows Server 2022, Windows Server 2019

En Windows Server 2019, Espacios de almacenamiento directo registra y almacena un historial de desempeño muy amplio de máquinas virtuales, servidores, unidades, volúmenes, adaptadores de red, etc. Consultar y procesar el historial de desempeño en PowerShell es muy sencillo, por lo que se puede pasar rápidamente de unos datos sin procesar a respuestas reales a preguntas como:

  1. ¿Se alcanzaron picos de CPU la semana pasada?
  2. ¿Hay un disco físico que muestre una latencia anómala?
  3. ¿Qué máquinas virtuales consumen más IOPS de almacenamiento en este momento?
  4. ¿Está saturado el ancho de banda de red?
  5. ¿Cuándo se agotará el espacio de este volumen?
  6. En el último mes, ¿qué máquinas virtuales consumieron más memoria?

El cmdlet Get-ClusterPerf se compila para scripting. Acepta la entrada de cmdlets como Get-VM o Get-PhysicalDisk por la canalización para controlar la asociación y se puede canalizar su salida a cmdlets de utilidad como Sort-Object, Where-Objecty Measure-Object para crear rápidamente consultas eficaces.

En este tema se presentan y se explican seis scripts de muestra y se da respuesta a las seis preguntas anteriores. Presentan patrones que se pueden aplicar para buscar picos y promedios, trazar líneas de tendencia, ejecutar la detección de valores atípicos, etc. con distintos datos y períodos de tiempo. Se presentan como código de inicio gratuito que se pueden copiar, ampliar y reutilizar.

Nota

Por motivos de brevedad, en los ejemplos de scripts se omiten elementos como el control de errores que se podría esperar de código de PowerShell de calidad. Están pensados principalmente para inspirar y divulgar más que para usarse en producción.

Ejemplo 1: CPU, ¡nos vemos!

En este ejemplo se usa la serie ClusterNode.Cpu.Usage del período de tiempo LastWeek para mostrar el uso máximo («marca de agua superior»), mínimo y medio de la CPU de cada servidor del clúster. También hace un análisis de cuartiles simple para mostrar por cuántas horas el uso de CPU era superior al 25 %, el 50 % y el 75 % en los últimos ocho días.

Instantánea

En la captura de pantalla siguiente se puede ver que Server-02 alcanzó un pico inexplicado la semana pasada:

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

Cómo funciona

La salida de las canalizaciones Get-ClusterPerf en el cmdlet integrado Measure-Object, solo se especifica la propiedad Value. Con las marcas -Maximum, -Minimum y -Average, Measure-Object proporciona las tres primeras columnas casi de forma gratuita. Para realizar el análisis de cuartiles, se puede canalizar a Where-Object y contar cuántos valores eran -Gt (mayores que) 25, 50 o 75. El último paso es embellecer con las funciones auxiliares Format-Hours y Format-Percent, opcionales, por supuesto.

Script

Este es el 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

Ejemplo 2: algo se incendia, valor atípico de latencia

En este ejemplo se usa la serie PhysicalDisk.Latency.Average del período de tiempo LastHour para buscar valores atípicos estadísticos, definidos como unidades con una latencia media horaria superior a +3σ (tres desviaciones estándar) por encima del promedio de la población.

Importante

Por motivos de brevedad, este script no implementa medidas de seguridad ante una desviación baja, no controla datos ausentes parciales, no distingue en función del modelo ni del firmware, etc. Proceda con sensatez y no se base únicamente en este script para decidir reemplazar un disco duro. Se expone meramente con fines divulgativos.

Instantánea

En la captura de pantalla siguiente se observa que no hay valores atípicos:

Screenshot that shows there are no outliers.

Cómo funciona

En primer lugar, se excluyen las unidades inactivas o casi inactivas al comprobar que PhysicalDisk.Iops.Total es -Gt 1 de modo sistemático. Para cada HDD activo, se canaliza su período de tiempo LastHour, constituido por 360 medidas a intervalos de 10 segundos en Measure-Object -Average para obtener su latencia media en la última hora. De este modo se configura la población.

Se implementa la fórmula ampliamente conocida para encontrar la desviación media μ y estándar σ de la población. Para casa HDD activo, se compara la latencia media con el promedio de la población y se divide por la desviación estándar. Se mantienen los valores sin procesar, por lo que se puede Sort-Object los resultados, pero usar Format-Latency y las funciones auxiliares Format-StandardDeviation para embellecer lo que se mostrará de forma opcional.

Si una unidad es superior a +3σ, se Write-Host en rojo; de lo contrario, en verde.

Script

Este es el 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."
}

Ejemplo 3: ¿Vecino ruidoso? Así es.

El historial de rendimiento también puede responder a preguntas sobre lo que ocurre ahora mismo. Las nuevas medidas están disponibles en tiempo real, cada diez segundos. En este ejemplo se usa la serie VHD.Iops.Total del período de tiempo MostRecent para identificar las máquinas virtuales más activas (también se podrían llamar las «más ruidosas») que consumen la mayor cantidad de IOPS de almacenamiento, en todos los hosts del clúster y mostrar el desglose de lectura y escritura de su actividad.

Instantánea

En la captura de pantalla siguiente se ven las diez máquinas virtuales principales por actividad de almacenamiento:

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

Cómo funciona

A diferencia de Get-PhysicalDisk, el cmdlet Get-VM no es compatible con clústeres; solo devuelve máquinas virtuales en el servidor local. Para hacer consultas desde cada servidor en paralelo, se encapsula la llamada en Invoke-Command (Get-ClusterNode).Name { ... }. Para cada máquina virtual, se obtienen las medidas VHD.Iops.Total, VHD.Iops.Read y VHD.Iops.Write. Al no especificar el parámetro -TimeFrame, se obtiene el único punto de datos MostRecent para cada uno.

Sugerencia

En estas series se refleja la suma de la actividad de esta máquina virtual en todos los archivos VHD/VHDX. En este ejemplo el historial de rendimiento se agrega automáticamente. Para obtener el desglose por VHD/VHDX, se puede canalizar un Get-VHD individual a Get-ClusterPerf en lugar de a la máquina virtual.

Los resultados de cada servidor se unen como $Output, que se puede Sort-Object y, a continuación, Select-Object -First 10. Conviene tener en cuenta que Invoke-Command representa los resultados con una propiedad PsComputerName que indica su procedencia, desde donde se puede imprimir para saber dónde se está ejecutando la máquina virtual.

Script

Este es el 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

Ejemplo 4: Como dicen, «ahora 25-gig es como 10-gig»

En este ejemplo se usa la serie NetAdapter.Bandwidth.Total del período de tiempo LastDay para buscar signos de saturación de red, definidos como el >90 % del ancho de banda máximo teórico. Para cada adaptador de red del clúster, se compara el uso de ancho de banda más alto observado en el último día con la velocidad de vínculo indicada.

Instantánea

En la captura de pantalla siguiente, se observa que un Fabrikam NX-4 Pro #2 alcanzó un pico el último día:

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

Cómo funciona

Se repite el truco de Invoke-Command mencionado anteriormente para Get-NetAdapter en cada servidor y se canaliza en Get-ClusterPerf. Sobre la marcha se captan dos propiedades relevantes: su cadena LinkSpeed como «10 Gbps» y su entero sin procesar Speed como 10000000000. Se usa Measure-Object para obtener el promedio y el pico del último día (aviso: cada medida del período de tiempo LastDay representa 5 minutos) y multiplicar por 8 bits por byte para establecer una comparación válida.

Nota

Algunos proveedores, como Chelsio, incorporan la actividad de acceso directo a memoria remota (RDMA) en sus contadores de rendimiento del adaptador de red, por lo que se incluye en la serie NetAdapter.Bandwidth.Total. Es posible que otros, como Mellanox, no lo hagan. Si el proveedor no lo hace, simplemente agregue la serie NetAdapter.Bandwidth.RDMA.Total a su versión de este script.

Script

Este es el 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

Ejemplo 5: vuelve la tendencia del almacenamiento.

Para ver las tendencias de macros, el historial de rendimiento se conserva durante un máximo de un año. En este ejemplo se usa la serie Volume.Size.Available del período de tiempo LastYear para determinar a qué velocidad se está llenando el almacenamiento y calcular cuándo se completará.

Instantánea

En la captura de pantalla siguiente se indica que el volumen de copia de seguridad agrega aproximadamente 15 GB al día:

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

A este ritmo, se alcanzará su capacidad dentro de 42 días.

Cómo funciona

El período de tiempo LastYear tiene un punto de datos al día. Aunque estrictamente solo se necesitan dos puntos para ajustarse a una línea de tendencia, en la práctica es mejor requerir más, por ejemplo 14 días. Se usa Select-Object -Last 14 para configurar una matriz de puntos (x, y) para x en el intervalo [1, 14]. Con estos puntos, se implementa el algoritmo de mínimos cuadrados lineales simple para buscar $A y $B que parametrice la línea de mejor ajuste y = ax + b. Bienvenido otra vez a secundaria.

Dividir la propiedad de volumen SizeRemaining por la tendencia (la pendiente $A) nos permite calcular toscamente cuántos días, a la velocidad actual de crecimiento del almacenamiento, tardará el volumen en llenarse. Las funciones auxiliares Format-Bytes, Format-Trend y Format-Days embellecen la salida.

Importante

Esta estimación es lineal y solo se basa en las 14 mediciones diarias más recientes. Existen técnicas más sofisticadas y precisas. Use el sentido común y no confíe únicamente en este script para determinar si debe invertir en ampliar el almacenamiento. Se expone meramente con fines divulgativos.

Script

Este es el 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

Ejemplo 6: la memoria, se puede ejecutar, pero no se puede ocultar

Dado que el historial de rendimiento se recopila y almacena de forma centralizada para todo el clúster, nunca se necesita unir datos de diferentes máquinas, independientemente del número de veces que las máquinas virtuales se muevan entre hosts. En este ejemplo se usa la serie VM.Memory.Assigned del período de tiempo LastMonth para identificar las máquinas virtuales que consumieron más memoria en los últimos 35 días.

Instantánea

En la captura de pantalla siguiente se ven las 10 máquinas virtuales principales por uso de memoria el mes pasado:

Screenshot of PowerShell

Cómo funciona

Se repite nuestro truco Invoke-Command anterior para Get-VM en cada servidor. Se usa Measure-Object -Average para obtener la media mensual de cada máquina virtual y, después, Sort-Object seguido de Select-Object -First 10 para obtener la tabla de clasificación. (¿O se trata de nuestra lista de más buscados?)

Script

Este es el 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

Eso es todo. Esperemos que estos ejemplos le inspiren y le ayuden a empezar. Con el historial de rendimiento de Espacios de almacenamiento directo y el cmdlet Get-ClusterPerf eficaz y descriptivo de scripting, tiene la capacidad tanto de preguntar como de responder. - preguntas complicadas a medida que administra y supervisa la infraestructura de Windows Server 2019.

Referencias adicionales