Database
 sql >> Base de données >  >> RDS >> Database

Coureur WhoIsActive

De nos jours, au sein de la communauté SQL Server DBA, il est fort probable que nous utilisions, ou du moins que nous ayons entendu parler, de la fameuse procédure stockée sp_WhoIsActive développé par Adam Machanic.

Pendant mon temps en tant que DBA, j'ai utilisé le SP pour vérifier immédiatement ce qui se passe à l'intérieur d'une instance SQL Server particulière lorsqu'elle reçoit tous les "points du doigt" qu'une application particulière s'exécute lentement.

Cependant, il y a des occasions où de tels problèmes deviennent récurrents et nécessitent un moyen de capturer ce qui se passe pour trouver un coupable potentiel. Il existe également des scénarios dans lesquels plusieurs instances servent de backend pour des applications tierces. La procédure stockée pourrait bien fonctionner pour trouver nos coupables.

Dans cet article, je présenterai un outil PowerShell qui peut aider n'importe quel DBA SQL Server à collecter les requêtes détectées par sp_WhoIsActive à l'intérieur d'une instance particulière de SQL Server. Ce SP les associerait à une certaine chaîne de recherche et les stockerait dans un fichier de sortie pour une post-analyse.

Considérations initiales

Voici quelques hypothèses avant de plonger dans les détails du script :

  • Le script reçoit le nom de l'instance en paramètre. Si aucun n'est passé, localhost sera pris en charge par le script.
  • Le script vous demandera une chaîne de recherche particulière pour la comparer aux textes des requêtes exécutées dans l'instance SQL Server. S'il y a une correspondance avec l'un d'eux, il sera stocké dans un fichier .txt que vous pourrez analyser plus tard.
  • Le fichier de sortie contenant toutes les informations relatives à votre instance est généré pour le chemin exact où se trouve et se déclenche le PowerShell. Assurez-vous que vous avez le écrire autorisations ici.
  • Si vous exécutez le script PowerShell plusieurs fois pour la même instance, tous les fichiers de sortie existants seront écrasés. Seul le plus récent sera conservé. Par conséquent, si vous devez conserver un fichier très spécifique, enregistrez-le manuellement ailleurs.
  • Le lot comprend un .sql fichier avec le code pour déployer la procédure stockée WhoIsActive à la base de données master de l'instance que vous spécifiez. Le script vérifie si la procédure stockée existe déjà dans l'instance et la crée si ce n'est pas le cas.
    • Vous pouvez choisir de le déployer sur une autre base de données. Assurez-vous simplement des modifications nécessaires dans le script.
    • Téléchargez ce .sql fichier à partir d'un hébergement sécurisé.
  • Le script tentera de récupérer les informations de l'instance SQL Server toutes les 10 secondes par défaut. Mais si vous souhaitez utiliser une valeur différente, ajustez-la en conséquence.
  • Assurez-vous que l'utilisateur demandé à se connecter à l'instance SQL Server dispose des autorisations nécessaires pour créer et exécuter les procédures stockées. Sinon, il n'atteindra pas son objectif.

Utilisation du script PowerShell

Voici ce que vous pouvez attendre du script :

Accédez à l'emplacement où vous avez placé le fichier de script PowerShell et exécutez-le comme ceci :

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

J'utilise C:\temp comme exemple

La seule chose que le script vous demandera est le type de connexion que vous souhaitez utiliser pour vous connecter à l'instance.

Remarque :Si vous utilisez PowerShell ISE, les invites ressembleront à des captures d'écran. Si vous l'exécutez directement à partir de la console PowerShell, les options seront demandées sous forme de texte dans la même fenêtre .

Fiable – la connexion à l'instance SQL Server se fera avec le même utilisateur que pour l'exécution du script PowerShell. Vous n'avez pas à spécifier d'informations d'identification, il les supposera en fonction du contexte.

Connexion Windows – vous devez fournir un identifiant Windows pour une authentification correcte.

Connexion SQL – vous devez fournir un identifiant SQL pour une authentification correcte.

Peu importe l'option que vous choisissez, assurez-vous qu'elle dispose de suffisamment de privilèges dans l'instance pour effectuer des vérifications .

Si vous choisissez le type de connexion qui nécessite la saisie d'identifiants, le script vous avertira en cas d'échec :

Avec les informations correctes spécifiées, le script vérifiera si le SP existe dans la base de données principale et procédera à sa création si ce n'est pas le cas.

Assurez-vous que le fichier .sql avec le code T-SQL pour créer le SP se trouve au même chemin où se trouve le script. Le .sql le nom du fichier doit être sp_WhoIsActive.sql .

Si vous souhaitez utiliser un autre nom de fichier .sql et une autre base de données cible, assurez-vous des modifications nécessaires dans le script PowerShell :

La prochaine étape sera l'invite de chaîne de recherche . Vous devez le saisir pour collecter toutes les correspondances renvoyées par chaque itération d'exécution de la procédure stockée dans l'instance SQL Server.

Après cela, vous devez choisir le temps que vous souhaitez accorder à l'exécution du script.

À des fins de démonstration, je vais choisir l'option n ° 1 (5 minutes). Je vais laisser une requête factice en cours d'exécution dans mon instance. La requête est WAITFOR DELAY '00:10′ . Je vais spécifier la chaîne de recherche WAITFOR afin que vous puissiez avoir une idée de ce que le script fera pour vous.

Une fois que le script a terminé son exécution, vous verrez un .txt fichier qui contient le nom de votre instance et WhoIsActive comme suffixe.

Voici un exemple de ce que le script a capturé et enregistré dans ce .txt fichier :

Code complet du script PowerShell

Si vous voulez essayer ce script, veuillez utiliser le code ci-dessous :

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

Conclusion

Gardons à l'esprit que WhoIsActive ne capturera pas les requêtes exécutées très rapidement par le moteur de base de données. Cependant, l'esprit de cet outil est de détecter les requêtes problématiques qui sont lentes et qui pourraient bénéficier d'un tour (ou de tours) d'optimisation.

Vous pourriez dire qu'une trace de profileur ou une session d'événements étendus pourrait accomplir la même chose. Cependant, je trouve très pratique que vous puissiez simplement lancer plusieurs fenêtres PowerShell et les exécuter sur différentes instances en même temps. C'est quelque chose qui pourrait s'avérer un peu fastidieux pour plusieurs instances.

En utilisant cela comme un tremplin, vous pouvez aller un peu plus loin et configurer un mécanisme d'alerte pour être averti de toute occurrence détectée par le script pour toute requête exécutée depuis plus de X minutes.