sql >> Base de Datos >  >> RDS >> Database

Corredor WhoIsActive

Hoy en día, dentro de la comunidad de DBA de SQL Server, es muy probable que usemos, o al menos hayamos oído hablar, del famoso procedimiento almacenado sp_WhoIsActive desarrollado por Adam Machanic.

Durante mi tiempo como DBA, usé el SP para verificar de inmediato lo que sucede dentro de una instancia particular de SQL Server cuando recibe todos los "señalamientos" de que una aplicación en particular se está ejecutando lentamente.

Sin embargo, hay ocasiones en las que estos problemas se vuelven recurrentes y requieren una forma de capturar lo que está sucediendo para encontrar un posible culpable. También hay escenarios en los que tiene varias instancias que funcionan como back-end para aplicaciones de terceros. El procedimiento almacenado podría funcionar potencialmente bien para encontrar a nuestros culpables.

En este artículo, presentaré una herramienta de PowerShell que puede ayudar a cualquier DBA de SQL Server a recopilar consultas detectadas por sp_WhoIsActive dentro de una instancia particular de SQL Server. Ese SP los relacionaría con una determinada cadena de búsqueda y los almacenaría en un archivo de salida para el análisis posterior.

Consideraciones iniciales

Aquí hay algunas suposiciones antes de sumergirse en los detalles del guión:

  • El script recibe el nombre de la instancia como parámetro. Si no se pasa ninguno, localhost será asumido por el script.
  • El script le pedirá una cadena de búsqueda particular para compararla con los textos de las consultas ejecutadas en la instancia de SQL Server. Si hay una coincidencia con alguno de ellos, se almacenará en un archivo .txt que podrá analizar más adelante.
  • El archivo de salida con toda la información relacionada con su instancia se genera para la ruta exacta donde se encuentra y activa PowerShell. Asegúrese de tener el escribir permisos allí.
  • Si ejecuta la secuencia de comandos de PowerShell varias veces para la misma instancia, se sobrescribirán todos los archivos de salida existentes anteriormente. Sólo se conservará el más reciente. Por lo tanto, si necesita conservar un archivo muy específico, guárdelo en otro lugar manualmente.
  • El paquete incluye un .sql archivo con el código para implementar el Procedimiento almacenado WhoIsActive a la base de datos maestra de la instancia que especifique. El script verifica si el procedimiento almacenado ya existe en la instancia y lo crea si no es así.
    • Puede optar por implementarlo en otra base de datos. Solo asegúrese de las modificaciones necesarias dentro del script.
    • Descargue este .sql archivo de alojamiento seguro.
  • La secuencia de comandos intentará obtener la información de la instancia de SQL Server cada 10 segundos de forma predeterminada. Pero si desea utilizar un valor diferente, ajústelo en consecuencia.
  • Asegúrese de que el usuario solicitado para conectarse a la instancia de SQL Server tenga permisos para crear y ejecutar los procedimientos almacenados. De lo contrario, no logrará su propósito.

Uso de la secuencia de comandos de PowerShell

Esto es lo que puede esperar del guión:

Vaya a la ubicación donde colocó el archivo de script de PowerShell y ejecútelo de esta manera:

PS C:\temp> .\WhoIsActive-Runner.ps1 SERVER\INSTANCE

Estoy usando C:\temp como ejemplo

Lo único que le preguntará el script es el tipo de inicio de sesión que desea utilizar para conectarse a la instancia.

Nota:si usa PowerShell ISE, las indicaciones se verán como capturas de pantalla. Si lo ejecuta directamente desde la consola de PowerShell, las opciones aparecerán como texto dentro de la misma ventana .

De confianza – la conexión a la instancia de SQL Server se realizará con el mismo usuario que para la ejecución del script de PowerShell. No tiene que especificar ninguna credencial, las asumirá según el contexto.

Inicio de sesión de Windows – debe proporcionar un inicio de sesión de Windows para la autenticación correcta.

Inicio de sesión SQL – debe proporcionar un inicio de sesión SQL para la autenticación correcta.

Independientemente de la opción que elija, asegúrese de que tenga suficientes privilegios en la instancia para realizar comprobaciones .

Si elige el tipo de inicio de sesión que requiere que ingrese las credenciales, el script le notificará en caso de falla:

Con la información correcta especificada, la secuencia de comandos verificará si el SP existe en la base de datos maestra y procederá a crearlo si no es así.

Asegúrese de que el archivo .sql con el código T-SQL para crear el SP se encuentre en la misma ruta donde se encuentra el script. El .sql el nombre del archivo debe ser sp_WhoIsActive.sql .

Si desea utilizar un nombre de archivo .sql diferente y una base de datos de destino diferente, asegúrese de realizar las modificaciones necesarias dentro del script de PowerShell:

El siguiente paso será el indicador de cadena de búsqueda . Debe ingresarlo para recopilar las coincidencias devueltas por cada iteración de ejecución del procedimiento almacenado dentro de la instancia de SQL Server.

Después de eso, debe elegir cuánto tiempo desea permitir para la ejecución del script.

Para fines de demostración, elegiré la opción n. ° 1 (5 minutos). Dejaré una consulta ficticia ejecutándose en mi instancia. La consulta es WAITFOR DELAY ’00:10′ . Voy a especificar la cadena de búsqueda WAITFOR para que pueda tener una idea de lo que el script hará por usted.

Después de que el script complete su ejecución, verá un .txt archivo que contiene el nombre de su instancia y WhoIsActive como sufijo.

Aquí hay una muestra de lo que el script capturó y guardó en ese .txt archivo:

Código Completo del Script de PowerShell

Si desea probar este script, utilice el siguiente código:

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)"

Conclusión

Tengamos en cuenta que WhoIsActive no capturará las consultas que DB Engine ejecuta muy rápido. Sin embargo, el espíritu de esta herramienta es detectar aquellas consultas problemáticas que son lentas y podrían beneficiarse de una ronda (o rondas) de optimización.

Podría argumentar que un seguimiento de Profiler o una sesión de evento extendido podrían lograr lo mismo. Sin embargo, me parece muy conveniente que simplemente pueda abrir varias ventanas de PowerShell y ejecutar cada una en diferentes instancias al mismo tiempo. Es algo que podría resultar un poco tedioso para múltiples instancias.

Al usar esto como un trampolín, puede ir un poco más allá y configurar un mecanismo de alerta para recibir notificaciones sobre cualquier ocurrencia detectada por el script para cualquier consulta que se haya estado ejecutando durante más de X minutos.