Database
 sql >> Base de Dados >  >> RDS >> Database

Quem é o corredor ativo


Hoje em dia, dentro da comunidade SQL Server DBA, é extremamente provável que usemos, ou pelo menos tenhamos ouvido falar, do famoso procedimento armazenado sp_WhoIsActive desenvolvido por Adam Machanic.

Durante meu tempo como DBA, usei o SP para verificar imediatamente o que está acontecendo dentro de uma instância específica do SQL Server quando ela está recebendo todos os “apontamentos” de que um aplicativo específico está lento.

No entanto, há ocasiões em que esses problemas se tornam recorrentes e exigem uma maneira de capturar o que está acontecendo para encontrar um possível culpado. Há também cenários em que você tem várias instâncias servindo como back-end para aplicativos de terceiros. O procedimento armazenado pode funcionar potencialmente bem para encontrar nossos culpados.

Neste artigo, apresentarei uma ferramenta do PowerShell que pode ajudar qualquer DBA do SQL Server a coletar consultas detectadas por sp_WhoIsActive dentro de uma instância específica do SQL Server. Esse SP os corresponderia a uma determinada string de pesquisa e os armazenaria em um arquivo de saída para pós-análise.

Considerações iniciais


Aqui estão algumas suposições antes de mergulhar nos detalhes do roteiro:
  • O script recebe o nome da instância como parâmetro. Se nenhum for passado, localhost será assumido pelo script.
  • O script solicitará uma string de pesquisa específica para compará-la com os textos das consultas executadas na instância do SQL Server. Se houver uma correspondência com algum deles, ela será armazenada em um arquivo .txt que você poderá analisar posteriormente.
  • O arquivo de saída com todas as informações relacionadas à sua instância é gerado para o caminho exato em que o PowerShell está localizado e acionado. Certifique-se de ter o escrever permissões lá.
  • Se você executar o script do PowerShell várias vezes para a mesma instância, todos os arquivos de saída existentes serão substituídos. Apenas o mais recente será mantido. Portanto, se você precisar manter um arquivo muito específico, salve-o em outro lugar manualmente.
  • O pacote inclui um .sql arquivo com o código para implantar o Procedimento armazenado WhoIsActive para o banco de dados mestre da instância que você especificar. O script verifica se o procedimento armazenado já existe na instância e o cria se não existir.
    • Você pode optar por implantá-lo em outro banco de dados. Apenas garanta as modificações necessárias dentro do script.
    • Faça o download deste .sql arquivo de hospedagem segura.
  • O script tentará buscar as informações da instância do SQL Server a cada 10 segundos por padrão. Mas se você quiser usar um valor diferente, ajuste-o de acordo.
  • Certifique-se de que o usuário aplicado para se conectar à instância do SQL Server tenha permissões para criar e executar os procedimentos armazenados. Caso contrário, ele não cumprirá sua finalidade.

Usando o script do PowerShell


Aqui está o que você pode esperar do script:

Vá para o local onde você colocou o arquivo de script do PowerShell e execute-o assim:
PS C:\temp> .\WhoIsActive-Runner.ps1 SERVER\INSTANCE

Estou usando C:\temp como exemplo

A única coisa que o script perguntará é o tipo de login que você deseja usar para se conectar à instância.

Observação:se você usar o PowerShell ISE, os prompts serão parecidos com capturas de tela. Se você executá-lo diretamente do console do PowerShell, as opções serão solicitadas como texto na mesma janela .

Confiável – a conexão com a instância do SQL Server será feita com o mesmo usuário da execução do script do PowerShell. Você não precisa especificar nenhuma credencial, ele as assumirá com base no contexto.

Login do Windows – você deve fornecer um login do Windows para a autenticação correta.

Login SQL – você deve fornecer um login SQL para a autenticação correta.

Independentemente da opção escolhida, certifique-se de que ela tenha privilégios suficientes na instância para realizar verificações .

Se você escolher o tipo de login que exige a inserção de credenciais, o script o notificará em caso de falha:

Com as informações corretas especificadas, o script verificará se o SP existe no banco de dados mestre e continuará a criá-lo, caso não exista.

Certifique-se de que o arquivo .sql com o código T-SQL para criar o SP esteja localizado no mesmo caminho em que o script está localizado. O .sql o nome do arquivo deve ser sp_WhoIsActive.sql .

Se você quiser usar um nome de arquivo .sql diferente e um banco de dados de destino diferente, verifique as modificações necessárias dentro do script do PowerShell:

A próxima etapa será o prompt de string de pesquisa . Você precisa inseri-lo para coletar quaisquer correspondências retornadas por cada iteração de execução do procedimento armazenado dentro da instância do SQL Server.

Depois disso, você deve escolher quanto tempo deseja permitir a execução do script.

Para fins de demonstração, vou escolher a opção nº 1 (5 minutos). Vou deixar uma consulta fictícia em execução na minha instância. A consulta é WAITFOR DELAY '00:10′ . Vou especificar a string de pesquisa WAITFOR para que você possa ter uma noção do que o script fará por você.

Depois que o script concluir sua execução, você verá um arquivo .txt arquivo que contém o nome de sua instância e WhoIsActive como sufixo.

Aqui está uma amostra do que o script capturou e salvou nesse .txt Arquivo:

Código completo do script do PowerShell


Se você quiser experimentar este script, use o código abaixo:
param(
    $instance = "localhost"
)

if (!(Get-Module -ListAvailable -Name "SQLPS")) {
    Write-Host -BackgroundColor Red -ForegroundColor White "Module Invoke-Sqlcmd is not loaded"
    exit
}

#Function to execute queries (depending on if the user will be using specific credentials or not)
function Execute-Query([string]$query,[string]$database,[string]$instance,[int]$trusted,[string]$username,[string]$password){
    if($trusted -eq 1){
        try{ 
            Invoke-Sqlcmd -Query $query -Database $database -ServerInstance $instance -ErrorAction Stop -ConnectionTimeout 5 -QueryTimeout 0      
        }
        catch{
            Write-Host -BackgroundColor Red -ForegroundColor White $_
            exit
        }
    }
    else{
        try{
            Invoke-Sqlcmd -Query $query -Database $database -ServerInstance $instance -Username $username -Password $password -ErrorAction Stop -ConnectionTimeout 5 -QueryTimeout 0
        }
         catch{
            Write-Host -BackgroundColor Red -ForegroundColor White $_
            exit
        }
    }
}

function Get-Property([string]$property,[string]$instance){
    Write-Host -NoNewline "$($property) " 
    Write-Host @greenCheck
    Write-Host ""
    switch($loginChoice){
        0       {$output = Execute-Query "SELECT SERVERPROPERTY('$($property)')" "master" $instance 1 "" ""}
        default {$output = Execute-Query "SELECT SERVERPROPERTY('$($property)')" "master" $instance 0 $login $password}   
    }
    switch($property){ 
        "EngineEdition"{
            switch($output[0]){
                1 {"$($property): Personal or Desktop Engine" | Out-File -FilePath $filePath -Append}
                2 {"$($property): Standard" | Out-File -FilePath $filePath -Append}
                3 {"$($property): Enterprise" | Out-File -FilePath $filePath -Append}
                4 {"$($property): Express" | Out-File -FilePath $filePath -Append}
                5 {"$($property): SQL Database" | Out-File -FilePath $filePath -Append}
                6 {"$($property): Microsoft Azure Synapse Analytics" | Out-File -FilePath $filePath -Append}
                8 {"$($property): Azure SQL Managed Instance" | Out-File -FilePath $filePath -Append}
                9 {"$($property): Azure SQL Edge" | Out-File -FilePath $filePath -Append}
                11{"$($property): Azure Synapse serverless SQL pool" | Out-File -FilePath $filePath -Append}            
            }
        }
        "HadrManagerStatus"{
            switch($output[0]){
                0       {"$($property): Not started, pending communication." | Out-File -FilePath $filePath -Append}
                1       {"$($property): Started and running." | Out-File -FilePath $filePath -Append}
                2       {"$($property): Not started and failed." | Out-File -FilePath $filePath -Append}
                default {"$($property): Input is not valid, an error, or not applicable." | Out-File -FilePath $filePath -Append}            
            }
        }
        "IsIntegratedSecurityOnly"{
            switch($output[0]){
                1{"$($property): Integrated security (Windows Authentication)" | Out-File -FilePath $filePath -Append}
                0{"$($property): Not integrated security. (Both Windows Authentication and SQL Server Authentication.)" | Out-File -FilePath $filePath -Append}                
            }
        }
        default{                        
            if($output[0] -isnot [DBNull]){
                "$($property): $($output[0])" | Out-File -FilePath $filePath -Append
            }else{
                "$($property): N/A" | Out-File -FilePath $filePath -Append
            }
        }
    }
    
    return
}

$filePath = ".\$($instance.replace('\','_'))_WhoIsActive.txt"
Remove-Item $filePath -ErrorAction Ignore

$loginChoices = [System.Management.Automation.Host.ChoiceDescription[]] @("&Trusted", "&Windows Login", "&SQL Login")
$loginChoice = $host.UI.PromptForChoice('', 'Choose login type for instance', $loginChoices, 0)
switch($loginChoice)
{
    1 { 
        $login          = Read-Host -Prompt "Enter Windows Login"
        $securePassword = Read-Host -Prompt "Enter Password" -AsSecureString
        $password       = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword))
      }
    2 { 
        $login          = Read-Host -Prompt "Enter SQL Login"
        $securePassword = Read-Host -Prompt "Enter Password" -AsSecureString
        $password       = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword))
      }
}

#Attempt to connect to the SQL Server instance using the information provided by the user
try{
    switch($loginChoice){
        0{
            $spExists = Execute-Query "SELECT COUNT(*) FROM sys.objects WHERE type = 'P' AND name = 'sp_WhoIsActive'" "master" $instance 1 "" ""
            if($spExists[0] -eq 0){
                Write-Host "The Stored Procedure doesn't exist in the master database."
                Write-Host "Attempting its creation..."
                try{
                    Invoke-Sqlcmd -ServerInstance $instance -Database "master" -InputFile .\sp_WhoIsActive.sql
                    Write-Host -BackgroundColor Green -ForegroundColor White "Success!"
                }
                catch{
                    Write-Host -BackgroundColor Red -ForegroundColor White $_
                    exit
                }
            }
        }
        default{
            $spExists = Execute-Query "SELECT COUNT(*) FROM sys.objects WHERE type = 'P' AND name = 'sp_WhoIsActive'" "master" $instance 0 $login $password
            if($spExists[0] -eq 0){
                Write-Host "The Stored Procedure doesn't exist in the master database."
                Write-Host "Attempting its creation..."
                try{
                    Invoke-Sqlcmd -ServerInstance $instance -Database "master" -Username $login -Password $password -InputFile .\sp_WhoIsActive.sql
                    Write-Host -BackgroundColor Green -ForegroundColor White "Success!"
                }
                catch{
                    Write-Host -BackgroundColor Red -ForegroundColor White $_
                    exit
                }
            }
        }   
    }     
}
catch{
    Write-Host -BackgroundColor Red -ForegroundColor White $_
    exit
}

#If the connection succeeds, then proceed with the retrieval of the configuration for the instance
Write-Host " _______  _______                           _______ _________ _______  _______  _______ __________________          _______ "
Write-Host "(  ____ \(  ____ )       |\     /||\     /|(  ___  )\__   __/(  ____ \(  ___  )(  ____ \\__   __/\__   __/|\     /|(  ____ \"
Write-Host "| (    \/| (    )|       | )   ( || )   ( || (   ) |   ) (   | (    \/| (   ) || (    \/   ) (      ) (   | )   ( || (    \/"
Write-Host "| (_____ | (____)| _____ | | _ | || (___) || |   | |   | |   | (_____ | (___) || |         | |      | |   | |   | || (__    "
Write-Host "(_____  )|  _____)(_____)| |( )| ||  ___  || |   | |   | |   (_____  )|  ___  || |         | |      | |   ( (   ) )|  __)   "
Write-Host "      ) || (             | || || || (   ) || |   | |   | |         ) || (   ) || |         | |      | |    \ \_/ / | (      "
Write-Host "/\____) || )             | () () || )   ( || (___) |___) (___/\____) || )   ( || (____/\   | |   ___) (___  \   /  | (____/\"
Write-Host "\_______)|/              (_______)|/     \|(_______)\_______/\_______)|/     \|(_______/   )_(   \_______/   \_/   (_______/"                                                                                                                            
Write-Host ""
$searchString = Read-Host "Enter string to lookup"  
$timerChoices = [System.Management.Automation.Host.ChoiceDescription[]] @("&1)5m", "&2)10m", "&3)15m","&4)30m","&5)Indefinitely")
$timerChoice  = $host.UI.PromptForChoice('', 'How long should the script run?', $timerChoices, 0)

Write-Host -NoNewline "Script will run "
switch($timerChoice){
    0{
        Write-Host "for 5 minutes."
        $limit = 5
    }
    1{
        Write-Host "for 10 minutes."
        $limit = 10
    }
    2{
        Write-Host "for 15 minutes."
        $limit = 15
    }
    3{
        Write-Host "for 30 minutes."
        $limit = 30
    }
    4{
        Write-Host "indefinitely (press ctrl-c to exit)."
        $limit = 2000000
    }
}
Write-Host "Start TimeStamp: $(Get-Date)"

$StopWatch = [system.diagnostics.stopwatch]::StartNew()

while($StopWatch.Elapsed.TotalMinutes -lt $limit){
    $results = Execute-Query "EXEC sp_WhoIsActive" "master" $instance 1 "" ""
    Get-Date | Out-File -FilePath $filePath -Append
    "####################################################################" | Out-File -FilePath $filePath -Append
    foreach($result in $results){
        if($result.sql_text -match $searchString){
            $result | Out-File -FilePath $filePath -Append
        }
        "####################################################################" | Out-File -FilePath $filePath -Append
    }
    Start-Sleep -s 10
}
Get-Date | Out-File -FilePath $filePath -Append
"####################################################################" | Out-File -FilePath $filePath -Append
Write-Host "End TimeStamp  : $(Get-Date)"

Conclusão


Vamos ter em mente que o WhoIsActive não captura consultas que são executadas muito rapidamente pelo DB Engine. No entanto, o espírito desta ferramenta é detectar aquelas consultas problemáticas que são lentas e podem se beneficiar de uma rodada (ou rodadas) de otimização.

Você pode argumentar que um rastreamento do Profiler ou uma sessão de Evento Estendido poderia realizar a mesma coisa. No entanto, acho muito conveniente que você possa simplesmente abrir várias janelas do PowerShell e executar cada uma em diferentes instâncias ao mesmo tempo. É algo que pode se tornar um pouco tedioso para várias instâncias.

Usando isso como um trampolim, você pode ir um pouco além e configurar um mecanismo de alerta para ser notificado sobre qualquer ocorrência detectada pelo script para qualquer consulta que esteja em execução por mais de X minutos.