Compartilhar via


Scripts com o PowerShell e o histórico de desempenho dos Espaços de Armazenamento Diretos

Aplica-se a: Windows Server 2022, Windows Server 2019

No Windows Server 2019, os Espaços de Armazenamento Diretos registram e armazenam um extenso histórico de desempenho para máquinas virtuais, servidores, unidades, volumes, adaptadores de rede e muito mais. O histórico de desempenho é fácil de consultar e processar no PowerShell para que você possa ir rapidamente de dados brutos para respostas reais para perguntas como:

  1. Houve algum pico de uso de CPU na semana passada?
  2. Algum disco físico está apresentando latência anormal?
  3. Quais VMs estão consumindo mais IOPS de armazenamento no momento?
  4. Minha largura de banda de rede está saturada?
  5. Quando esse volume ficará sem espaço livre?
  6. No mês passado, quais VMs usaram mais memória?

O cmdlet Get-ClusterPerf é criado para scripts. Ele aceita entrada de cmdlets como Get-VM ou Get-PhysicalDisk pelo pipeline para lidar com a associação, e você pode redirecionar sua saída para cmdlets utilitários como Sort-Object, Where-Object e Measure-Object para compor rapidamente consultas avançadas.

Este tópico fornece e explica seis scripts de exemplo que respondem às seis perguntas acima. Eles apresentam padrões que você pode aplicar para localizar picos, encontrar médias, plotar linhas de tendência, detectar exceções e muito mais, em uma variedade de dados e períodos. Eles são fornecidos como código inicial gratuito para você copiar, estender e reutilizar.

Observação

Para serem breves, os scripts de exemplo omitem coisas como tratamento de erros, que você pode esperar de códigos do PowerShell de alta qualidade. Eles se destinam principalmente à inspiração e à educação, e não ao uso em produção.

Exemplo 1: CPU, vejo você!

Este exemplo usa a série ClusterNode.Cpu.Usage do período LastWeek para mostrar o uso máximo ("marca d'água alta"), o uso mínimo e o uso médio da CPU para cada servidor no cluster. Ele também faz uma análise de quartil simples para mostrar quantas horas o uso da CPU foi superior a 25%, 50% e 75% nos últimos 8 dias.

Captura de tela

Na captura de tela abaixo, vemos que Server-02 teve um pico inexplicável na semana passada:

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

Como ele funciona

A saída de Get-ClusterPerf é direcionada para o cmdlet interno Measure-Object, nós apenas especificamos a propriedade Value. Com os sinalizadores -Maximum, -Minimum e -Average, Measure-Object nos dá as três primeiras colunas quase de graça. Para fazer a análise de quartil, podemos direcionar para Where-Object e contar quantos valores foram -Gt (maiores que) 25, 50 ou 75. A última etapa é embelezar com as funções auxiliares Format-Hours e Format-Percent, certamente opcionais.

Script

Este é o 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

Exemplo 2: Fogo, fogo, exceção de latência

Este exemplo usa a série PhysicalDisk.Latency.Average do período LastHour para procurar exceções estatísticas, definidas como unidades com latência média por hora superior a +3σ (três desvios padrão) acima da média populacional.

Importante

Para resumir, esse script não implementa proteções contra baixa variação, não manipula dados parciais ausentes, não distingue por modelo ou firmware etc. Exerça um bom julgamento e não dependa apenas do script para determinar se um disco rígido deve ser substituído. Ele é apresentado aqui apenas para fins educacionais.

Captura de tela

Na captura de tela abaixo, vemos que não há exceções:

Screenshot that shows there are no outliers.

Como ele funciona

Primeiro, excluímos unidades ociosas ou quase ociosas verificando se PhysicalDisk.Iops.Total é consistentemente -Gt 1. Para cada HDD ativo, direcionamos o período LastHour, composto por 360 medidas em intervalos de 10 segundos, para Measure-Object -Average para obter a latência média na última hora. Isso configura nossa população.

Implementamos a fórmula amplamente conhecida para encontrar a média μ e o desvio padrão σ da população. Para cada HDD ativo, comparamos a latência média com a média populacional e dividimos pelo desvio padrão. Mantemos os valores brutos, para que possamos Sort-Object os resultados, mas usamos as funções auxiliares Format-Latency e Format-StandardDeviation para embelezar o que mostraremos – certamente opcional.

Se qualquer unidade for maior que +3σ, nós Write-Host vermelho; caso contrário, em verde.

Script

Este é o 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."
}

Exemplo 3: Vizinho barulhento? É uma gravação!

O histórico de desempenho também pode responder a perguntas sobre agora. Novas medidas ficam disponíveis em tempo real, a cada 10 segundos. Este exemplo usa a série VHD.Iops.Total do período MostRecent para identificar as máquinas virtuais mais movimentadas (alguns podem dizer "mais barulhentas") que consomem mais IOPS de armazenamento, em todos os hosts do cluster, e mostrar o detalhamento de leitura/gravação de suas atividades.

Captura de tela

Na captura de tela abaixo, vemos as 10 principais máquinas virtuais por atividade de armazenamento:

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

Como ele funciona

Diferente de Get-PhysicalDisk, o cmdlet Get-VM não tem reconhecimento de cluster – ele retorna apenas VMs no servidor local. Para consultar de todos os servidores em paralelo, encapsulamos a chamada em Invoke-Command (Get-ClusterNode).Name { ... }. Para cada VM, obtemos as medidas VHD.Iops.Total, VHD.Iops.Read e VHD.Iops.Write. Ao não especificar o parâmetro -TimeFrame, obtemos o ponto de dados único MostRecent para cada um.

Dica

Essas séries refletem a soma da atividade dessa VM a todos os seus arquivos VHD/VHDX. Este é um exemplo em que o histórico de desempenho está sendo agregado automaticamente para nós. Para obter o detalhamento por VHD/VHDX, você pode direcionar um Get-VHD individual a Get-ClusterPerf em vez da VM.

Os resultados de cada servidor se reúnem como $Output, o que podemos Sort-Object e, em seguida, Select-Object -First 10. Observe que Invoke-Command decora resultados com uma propriedade PsComputerName que indica de onde eles vieram, que podemos imprimir para saber onde a VM está em execução.

Script

Este é o 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

Exemplo 4: Como dizem, "25 gig é o novo 10 gig"

Este exemplo usa a série NetAdapter.Bandwidth.Total do período LastDay para procurar sinais de saturação de rede, definidos como >90% da largura de banda máxima teórica. Para cada adaptador de rede no cluster, ele compara o maior uso de largura de banda observado no último dia com sua velocidade de link declarada.

Captura de tela

Na captura de tela abaixo, vemos que Fabrikam NX-4 Pro #2 atingiu o pico no último dia:

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

Como ele funciona

Repetimos nosso truque Invoke-Command acima para Get-NetAdapter em cada servidor e direcionamos para Get-ClusterPerf. Ao longo do caminho, pegamos duas propriedades relevantes: sua cadeia de caracteres LinkSpeed como "10 Gbps" e seu inteiro bruto Speed como 10000000000. Usamos Measure-Object para obter a média e o pico do último dia (lembrete: cada medida no período LastDay representa 5 minutos) e multiplicar por 8 bits por byte para obter uma comparação entre maçãs e maçãs.

Observação

Alguns fornecedores, como a Chelsio, incluem atividade RDMA (acesso à memória direta remota) em seus contadores de desempenho do Adaptador de Rede, portanto, isso está incluído na série NetAdapter.Bandwidth.Total. Outros, como a Mellanox, podem não incluir. Se o seu fornecedor não fizer incluir, basta adicionar a série NetAdapter.Bandwidth.RDMA.Total à sua versão desse script.

Script

Este é o 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

Exemplo 5: Fazer do armazenamento uma tendência novamente!

Para examinar tendências de macro, o histórico de desempenho é mantido por até 1 ano. Este exemplo usa a série Volume.Size.Available do período LastYear para determinar a taxa em que o armazenamento está sendo preenchido e estimar quando ele estará cheio.

Captura de tela

Na captura de tela abaixo, vemos que o volume de Backup está adicionando cerca de 15 GB por dia:

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

Nesse ritmo, ele atingirá sua capacidade em mais 42 dias.

Como ele funciona

O período LastYear tem um ponto de dados por dia. Embora você só precise estritamente de dois pontos para traçar uma linha de tendência, na prática é melhor exigir mais, como 14 dias. Usamos Select-Object -Last 14 para configurar uma matriz de pontos (x, y), para x no intervalo [1, 14]. Com esses pontos, implementamos o algoritmo de quadrados mínimos lineares simples para localizar $A e $B, que parametrizam a linha de melhor ajuste y = ax + b. Bem-vindo ao ensino médio de novo.

Dividir a propriedade SizeRemaining do volume pela tendência (a inclinação $A) nos permite estimar grosseiramente quantos dias, na taxa atual de crescimento do armazenamento, até que o volume esteja cheio. As funções auxiliares Format-Bytes, Format-Trend e Format-Days embelezam a saída.

Importante

Essa estimativa é linear e baseada apenas nas 14 medidas diárias mais recentes. Existem técnicas mais sofisticadas e precisas. Exerça bom julgamento e não dependa apenas desse script para determinar se deseja investir na expansão do armazenamento. Ele é apresentado aqui apenas para fins educacionais.

Script

Este é o 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

Exemplo 6: Hog de memória, você pode correr, mas não pode se esconder

Como o histórico de desempenho é coletado e armazenado centralmente para todo o cluster, você nunca precisa unir dados de computadores diferentes, independentemente de quantas vezes as VMs se movem entre hosts. Este exemplo usa a série VM.Memory.Assigned do período LastMonth para identificar as máquinas virtuais que consumiram mais memória nos últimos 35 dias.

Captura de tela

Na captura de tela abaixo, vemos as 10 principais máquinas virtuais por uso de memória no último mês:

Screenshot of PowerShell

Como ele funciona

Repetimos nosso truque Invoke-Command, introduzido acima, em Get-VM em todos os servidores. Usamos Measure-Object -Average para obter a média mensal para cada VM, em seguida Sort-Object, seguido por Select-Object -First 10 para obter nosso placar de líderes. (Ou talvez seja nossa lista dos Mais procurados?)

Script

Este é o 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

É isso! Esperamos que esses exemplos o inspirem e o ajudem a começar. Com o histórico de desempenho dos Espaços de Armazenamento Diretos e o poderoso cmdlet amigável para scripts Get-ClusterPerf, você tem o poder de fazer – e responder! – perguntas complexas à medida que gerencia e monitora sua infraestrutura do Windows Server 2019.

Referências adicionais