By: Pablo Echeverria | Updated: 2017-11-07 | Comments (8) | Related: > PowerShell
Problem
We need to ensure the availability of the databases across several SQL Server instances. If there’s an issue, we must know before our customers are impacted. One of the things we must check is free space per drive, and there’s no easy way to monitor it as we need to check each instance one by one. Even automating this task can take several minutes to complete, in my case it was taking about 15 minutes to query 170 instances, mainly because the inventory contains outdated entries that no longer exist.
Solution
By implementing the script below, we can take advantage of the parallelism of tasks to gather the information, completing in 20 seconds for the 170 instances, without having to wait for the previous instance to continue. At the end of the process we get a list of unresponsive instances that must be checked manually or removed from the inventory.
PowerShell Check Disk Space Script in Parallel for all SQL Server Instances
First we need to configure our environment, specifying the server, database and query where we have our inventory. The minimum details needed are the ServerName and InstanceName, but having the Environment is also useful. Then, we specify the free space threshold for the report, in my case I set it up to report anything below 20%. Finally, the Maxthreads indicates I’ll have 20 threads gathering data across all of the instances.
I created a table called Inventory on my central server as follows:
CREATE TABLE Inventory ( [Environment] nvarchar(128) NOT NULL, [ServerName] nvarchar(128) NOT NULL, [InstanceName] nvarchar(128) NOT NULL )
Then I added some records to the table as follows. The InstanceName needs to include both the server and instance name.
- NoServer - this is a dummy server to show the error
- ServerA - this is the default instance
- ServerB - this uses a named instance called Test1
INSERT INTO Inventory values ('Bad','NoServer','NoServer') INSERT INTO Inventory values ('Production','ServerA','ServerA') INSERT INTO Inventory values ('Test','ServerB','ServerB\Test1')
Following is the PowerShell script. You will need to adjust the following variables:
- $server - this is the central SQL Server instance where you are storing the list of servers.
- $database - this is the database where you are storing the table that has the list of servers
- $query - this is the code that queries the table with the server names
- $threshold - this determines the threshold for % free to report back
- $MaxThreads - this is the number of parallel queries to run
$ErrorActionPreference = "Stop" #stop when an error is encountered # Declare variables $server = "ServerC" $database = "master" $query = @" SELECT [Environment], [ServerName], [InstanceName] FROM [Inventory] "@ $threshold = 20 #less than 20% is reported $SleepTimer = 1000 #after X milliseconds, check if the jobs have finished. 1000 is every second. $MaxResultTime = 300 #after X seconds, all jobs are killed. 300 is 5 minutes. $Maxthreads = 20 #number of parallel jobs # import modules Import-Module SqlPs -DisableNameChecking $error.clear() #clear error generated by last command # get list of instances $objects = @(Invoke-Sqlcmd -ServerInstance $server -Database $database -Query $query) #environment setup $output = @() $errors = "" $Jobs = @() $ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault() $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host) $RunspacePool.Open() $script = { Param ([string]$environment, [string]$serverName, [string]$instanceName, [int]$threshold) $ErrorActionPreference = "Stop" $query = " SET NOCOUNT ON --No 'rows returned' message IF @@VERSION LIKE 'Microsoft SQL Server 2000%' BEGIN SELECT DISTINCT LEFT([filename], CHARINDEX('\', [filename]) - 1) FROM [dbo].[sysaltfiles] END ELSE BEGIN SELECT DISTINCT LEFT([physical_name], CHARINDEX('\', [physical_name]) - 1) FROM [sys].[master_files] END" try { $disks = sqlcmd -S $instanceName -Q $query -h-1 -W -b -r1 2>&1 #no header, no whitespaces, break on error, errors to output, stderr to stdout if (!$disks -or $disks.length -eq 0) { return $instanceName + ": Unable to query server" | Out-String } #server may not exist anymore $diskList = "" #filter volumes based on drive letter foreach ($row in $disks) { if ($diskList -eq "") { $diskList = "DriveLetter='$($row)'" } else { $diskList += " OR DriveLetter='$($row)'" } } $result = Get-WmiObject Win32_Volume -ComputerName $serverName -filter "$diskList" | select ` @{Name="Environment"; Expression={$environment}}, ` @{Name="InstanceName"; Expression={$instanceName}}, ` DriveLetter, ` Label, ` @{Name="Capacity"; Expression={[math]::Round($_.Capacity/1GB, 2)}}, ` @{Name="FreeSpace"; Expression={[math]::Round($_.FreeSpace/1GB, 2)}}, ` @{Name="PercentFree"; Expression={[math]::Round(($_.FreeSpace * 100 / $_.Capacity), 2)}} "" #indicate there was no error $result | where {$_.PercentFree -lt $threshold} #return results lower than threshold } catch { return $instanceName + ": " + $LastExitCode + " " + $_ | foreach { $_.ToString() } | Out-String #Get-WmiObject error, maybe permissions } } function CreateThread() { param ([string]$environment, [string]$serverName, [string]$instanceName, [ref]$Jobs) $PowershellThread = [powershell]::Create().AddScript($script) #scriptToRun $PowershellThread.AddArgument($environment) | out-null $PowershellThread.AddArgument($serverName) | out-null $PowershellThread.AddArgument($instanceName) | out-null $PowershellThread.AddArgument($threshold) | out-null $PowershellThread.RunspacePool = $RunspacePool $Handle = $PowershellThread.BeginInvoke() $Job = "" | select Handle, Thread, object $Job.Handle = $Handle; $Job.Thread = $PowershellThread $Jobs.value += $Job } $ResultTimer = Get-Date #start time #start processing first task for each instance for ($i=0; $i -lt $objects.length; $i++) { CreateThread $objects[$i]."Environment" $objects[$i]."ServerName" $objects[$i]."InstanceName" ([ref]$Jobs) } while (@($Jobs | where {$_.Handle -ne $Null}).count -gt 0) { #update completed jobs, get errors and result, and dispose them foreach ($Job in @($Jobs | where {$_.Handle -ne $Null -and $_.Handle.IsCompleted -eq $True})) { $results = $Job.Thread.EndInvoke($Job.Handle) if ($results[0] -and $results[0] -ne "") { $errors += $results[0] } $output += $results[1] #end thread $Job.Thread.Dispose() $Job.Thread = $Null $Job.Handle = $Null } #show progress $inProgress = @($Jobs | where {$_.Handle.IsCompleted -eq $False}).count Write-Progress ` -Activity "Gathering data" ` -PercentComplete (($objects.length - $inProgress) * 100 / $objects.length) ` -Status "$inProgress pending" #exit on timeout $currentTime = Get-Date if (($currentTime - $ResultTimer).totalseconds -gt $MaxResultTime) { Write-Error "Child script appears to be frozen, try increasing MaxResultTime" break } #sleep Start-Sleep -Milliseconds $SleepTimer } #dispose thread pools $RunspacePool.Close() | Out-Null $RunspacePool.Dispose() | Out-Null $errors #return errors $output | Format-Table -Auto #return results
Work performed by each thread
There is a variable named $script, which performs the following:
- Query the instance to get the drives where any databases reside. This is because we or someone prior may have misconfigured a database. Note that Invoke-Sqlcmd is not thread safe, so we have to use SQLCMD with a special setup: remove “rows returned” message, do not return headers, and do not return white spaces. The query executed works for any version of SQL Server after SQL Server 2000. If we don’t get any results, we return an error to check the instance manually.
- For each drive returned from the query, we construct a filter to be passed to the next step, so we won’t query all the disks for a server, but only the ones we’re interested in. This is because multiple instances can reside on the same server, and it’s a waste of resources and time to query all of the drives if we’re not interested in them.
- Get the information about the disks, but express the values in GB rounded to 2 decimals.
- Return the information where the percent free is less than the threshold specified at the beginning of the script.
When this is run, the script shows the progress and the remaining instances:
At the end, the script reports any instance that could not be queried (due to a SQL or a Windows error) and the ones that have less than the threshold free space. We can see below that the first server had an error and the other two servers returned information.
Next Steps
- This script can be modified to send an HTML report on a scheduled basis, check out my other tip on Standardized table based SQL Server monitoring email with PowerShell for all the details.
- Check out these additional resources:
About the author
This author pledges the content of this article is based on professional experience and not AI generated.
View all my tips
Article Last Updated: 2017-11-07