Partilhar via


Scripting com PowerShell e histórico de desempenho do Storage Spaces Direct

No Windows Server 2019, Storage Spaces Direct registra e armazena 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 passar rapidamente de dados brutos para respostas reais a perguntas como:

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

O Get-ClusterPerf cmdlet foi 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 canalizar sua saída para cmdlets de utilitários como Sort-Object, Where-Objecte Measure-Object para compor rapidamente consultas poderosas.

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

Observação

Por uma questão de brevidade, os scripts de exemplo omitem coisas como o tratamento de erros que você pode esperar de um código PowerShell de alta qualidade. Destinam-se principalmente à inspiração e educação e não ao uso da produção.

Amostra 1: CPU, eu vejo você!

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

Captura de ecrã

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

Captura de tela que mostra que o Server-02 teve um pico inexplicável na semana passada.

Como funciona

A saída de Get-ClusterPerf encaixa-se bem no cmdlet interno Measure-Object, bastando especificar a propriedade Value. Com as suas -Maximum, -Minimum, e -Average bandeiras, Measure-Object dá-nos as três primeiras colunas quase gratuitamente. Para fazer a análise de quartis, podemos usar Where-Object para filtrar e contar quantos valores eram -Gt (maiores que) 25, 50 ou 75. O último passo é embelezar com as funções auxiliares Format-Hours e Format-Percent – certamente opcionais.

Guião

Aqui está o roteiro:

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

Amostra 2: Incêndio, incêndio, latência fora do normal

Esta amostra utiliza a série PhysicalDisk.Latency.Average correspondente ao período de tempo LastHour para procurar valores estatísticos anómalos, definidos como discos com uma latência média horária acima de +3σ (três desvios-padrão) acima da média da população.

Importante

Por uma questão de brevidade, este script não implementa salvaguardas contra baixa variância, não lida com dados parcialmente ausentes, não distingue por modelo ou firmware, etc. Por favor, exerça bom senso e não confie apenas neste script para determinar se deve substituir um disco rígido. É apresentado aqui apenas para fins educacionais.

Captura de ecrã

Na imagem abaixo, vemos que não há valores atípicos:

Captura de ecrã que mostra que não há valores anormais.

Como funciona

Primeiro, excluímos unidades ociosas ou quase ociosas verificando se PhysicalDisk.Iops.Total está consistentemente -Gt 1. Para cada HDD ativo, enviamos o seu LastHour período de tempo, composto por 360 medições realizadas a cada 10 segundos, para Measure-Object -Average com o objetivo de obter a latência média na hora anterior. Isso configura a 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 sua latência média com a média da população e dividimos pelo desvio padrão. Mantemos os valores brutos, para que possamos Sort-Object nossos resultados, mas usamos Format-Latency e Format-StandardDeviation funções auxiliares para embelezar o que iremos mostrar – certamente opcional.

Caso um disco exceda +3σ, indicamos em vermelho; caso contrário, em verde.

Guião

Aqui está o roteiro:

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

Amostra 3: Vizinho barulhento? Isso é escrever!

O histórico de desempenho também pode responder a perguntas sobre o momento. Novas medições estão disponíveis em tempo real, a cada 10 segundos. Este exemplo utiliza a série VHD.Iops.Total do intervalo de MostRecent para identificar as máquinas virtuais mais activas (alguns podem dizer "mais barulhentas") que consomem mais IOPS de armazenamento, em todos os hosts do cluster, e apresentar a divisão de leitura/gravação da sua atividade.

Captura de ecrã

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

Captura de tela que mostra as 10 principais máquinas virtuais por atividade de armazenamento.

Como funciona

Ao contrário de Get-PhysicalDisk, o cmdlet Get-VM não tem consciência de clusters – ele apenas retorna VMs no servidor local. Para consultar a partir de cada servidor em paralelo, envolvemos nossa chamada em Invoke-Command (Get-ClusterNode).Name { ... }. Para cada VM, obtemos as medições VHD.Iops.Total, VHD.Iops.Read e VHD.Iops.Write. Ao não especificar o -TimeFrame parâmetro, obtemos o MostRecent único ponto de dados para cada um.

Sugestão

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

Os resultados de cada servidor se reúnem como $Output, que podemos Sort-Object e depois Select-Object -First 10. Observe que o Invoke-Command decora os resultados com uma propriedade PsComputerName que indica de onde eles vieram, e que podemos imprimir para determinar onde a VM está a ser executada.

Guião

Aqui está o roteiro:

$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

Amostra 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, definida como >90% de 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 ecrã

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

Captura de ecrã que mostra que o Fabrikam NX-4 Pro #2 atingiu o pico no último dia.

Como funciona

Repetimos o nosso truque mencionado acima em cada servidor e canalizamos para Invoke-Command. Ao longo do caminho, capturamos duas propriedades relevantes: a sua LinkSpeed string tal como "10 Gbps" e o seu inteiro bruto Speed tal como 10000000000. Usamos Measure-Object para obter a média e o pico do último dia (lembrete: cada medição no período de tempo LastDay representa 5 minutos) e multiplicamos por 8 bits por byte para obter uma comparação direta.

Observação

Alguns fornecedores, como o Chelsio, incluem a atividade RDMA (acesso remoto direto à memória) em seus contadores de desempenho do adaptador de rede , por isso está incluída na NetAdapter.Bandwidth.Total série. Outros, como Mellanox, não podem. Se o seu fornecedor não o fizer, basta adicionar a série NetAdapter.Bandwidth.RDMA.Total na sua versão deste script.

Guião

Aqui está o roteiro:

$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

Amostra 5: Torne o armazenamento moderno novamente!

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

Captura de ecrã

Na imagem abaixo, vemos que o volume de backup está adicionando cerca de 15 GB por dia:

Captura de tela que mostra que o volume de backup está adicionando cerca de 15 GB por dia.

A este ritmo, atingirá a sua capacidade em mais 42 dias.

Como funciona

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

Dividindo a propriedade do SizeRemaining 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

Esta estimativa é linear e baseia-se apenas nas 14 medições diárias mais recentes. Existem técnicas mais sofisticadas e precisas. Por favor, exerça bom senso e não confie apenas neste script para determinar se deve investir na expansão do seu armazenamento. É apresentado aqui apenas para fins educacionais.

Guião

Aqui está o roteiro:


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

Amostra 6: Consumo excessivo de memória, você pode correr, mas não podes esconder-te

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

Captura de ecrã

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

Captura de ecrã do PowerShell

Como funciona

Repetimos o nosso Invoke-Command truque, introduzido acima, em Get-VM todos os servidores. Usamos Measure-Object -Average para obter a média mensal para cada VM, seguido Sort-Object por Select-Object -First 10 para obter a nossa tabela de classificação. (Ou talvez seja a nossa lista dos mais procurados ?)

Guião

Aqui está o roteiro:

$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! Espero que estas amostras o inspirem e o ajudem a começar. Com o histórico de desempenho do Storage Spaces Direct e o poderoso cmdlet amigável Get-ClusterPerf para scripts, você tem o poder de perguntar – e responder! – perguntas complexas enquanto você gerencia e monitora sua infraestrutura do Windows Server 2019.

Referências Adicionais