Scripting con PowerShell y el historial de desempeño de Espacios de almacenamiento directo
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:
- ¿Se alcanzaron picos de CPU la semana pasada?
- ¿Hay un disco físico que muestre una latencia anómala?
- ¿Qué máquinas virtuales consumen más IOPS de almacenamiento en este momento?
- ¿Está saturado el ancho de banda de red?
- ¿Cuándo se agotará el espacio de este volumen?
- 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-Object
y 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:
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:
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:
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:
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:
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:
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.