Friday, March 23, 2012 A PowerShell Alternative to SDelete

Problem Background

Many storage appliances support thin provisioning by not storing large blocks of zeros on disk, thus saving expensive physical disks for actual data. One of the challenges of this feature is that Windows (or most file systems really) does not zero out space that was used by files that have been deleted. This means that over time as files are written an deleted, the amount of physical disk that is saved by thin provisioning grows smaller.

One popular solution (really the only one we’ve been able to find) is to use the SysInternals utility SDelete’s -z switch to write zeros to all the free space, thus allowing the storage appliance to use all that empty space for something else. The challenge with sdelete is that it only supports volumes mounted as drive letters, not volumes that are mounted to folders. Another concern with SDelete is that it appears to fill up the entire disk, if only for that brief second between finishing writing the file out and deleting the file. We were concerned with using this tool at work on a workload like SQL that might not take kindly to a disk filling up.

Our storage administrator tried to solve this problem using the standard PowerShell techniques of redirecting null strings to files and found the performance to be somewhat lacking. I don’t want to get into a detailed analysis of PowerShell IO characteristics (mostly because I haven’t spent much time researching how PowerShell does IO), but instead just post that the solution is to turn to the System.IO .Net namespace and perform the IO operations the “hard way”.

The Script

Here is the script I came up with using a FileStream to write out the zero file. We informally compared a couple of runs between this script and sdelete and saw roughly the same performance.


  1 <#
  3  Writes a large file full of zeroes to a volume in order to allow a storage
  4  appliance to reclaim unused space.
  7  Creates a file called ThinSAN.tmp on the specified volume that fills the
  8  volume up to leave only the percent free value (default is 5%) with zeroes.
  9  This allows a storage appliance that is thin provisioned to mark that drive
 10  space as unused and reclaim the space on the physical disks.
 13  The folder to create the zeroed out file in.  This can be a drive root (c:\)
 14  or a mounted folder (m:\mounteddisk).  This must be the root of the mounted
 15  volume, it cannot be an arbitrary folder within a volume.
 17 .PARAMETER PercentFree
 18  A float representing the percentage of total volume space to leave free.  The
 19  default is .05 (5%)
 22  PS> Write-ZeroesToFreeSpace -Root "c:\"
 24  This will create a file of all zeroes called c:\ThinSAN.tmp that will fill the
 25  c drive up to 95% of its capacity.
 28  PS> Write-ZeroesToFreeSpace -Root "c:\MountPoints\Volume1" -PercentFree .1
 30  This will create a file of all zeroes called
 31  c:\MountPoints\Volume1\ThinSAN.tmp that will fill up the volume that is
 32  mounted to c:\MountPoints\Volume1 to 90% of its capacity.
 35  PS> Get-WmiObject Win32_Volume -filter "drivetype=3" | Write-ZeroesToFreeSpace
 37  This will get a list of all local disks (type=3) and fill each one up to 95%
 38  of their capacity with zeroes.
 40 .NOTES
 41  You must be running as a user that has permissions to write to the root of the
 42  volume you are running this script against. This requires elevated privileges
 43  using the default Windows permissions on the C drive.
 44 #>
 45 param(
 46   [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
 47   [ValidateNotNullOrEmpty()]
 48   [Alias("Name")]
 49   $Root,
 50   [Parameter(Mandatory=$false)]
 51   [ValidateRange(0,1)]
 52   $PercentFree =.05
 53 )
 54 process{
 55   #Convert the $Root value to a valid WMI filter string
 56   $FixedRoot = ($Root.Trim("\") -replace "\\","\\") + "\\"
 57   $FileName = "ThinSAN.tmp"
 58   $FilePath = Join-Path $Root $FileName
 60   #Check and make sure the file doesn't already exist so we don't clobber someone's data
 61   if( (Test-Path $FilePath) ) {
 62     Write-Error -Message "The file $FilePath already exists, please delete the file and try again"
 63   } else {
 64     #Get a reference to the volume so we can calculate the desired file size later
 65     $Volume = gwmi win32_volume -filter "name='$FixedRoot'"
 66     if($Volume) {
 67       #I have not tested for the optimum IO size ($ArraySize), 64kb is what sdelete.exe uses
 68       $ArraySize = 64kb
 69       #Calculate the amount of space to leave on the disk
 70       $SpaceToLeave = $Volume.Capacity * $PercentFree
 71       #Calculate the file size needed to leave the desired amount of space
 72       $FileSize = $Volume.FreeSpace - $SpacetoLeave
 73       #Create an array of zeroes to write to disk
 74       $ZeroArray = new-object byte[]($ArraySize)
 76       #Open a file stream to our file 
 77       $Stream = [io.File]::OpenWrite($FilePath)
 78       #Start a try/finally block so we don't leak file handles if any exceptions occur
 79       try {
 80         #Keep track of how much data we've written to the file
 81         $CurFileSize = 0
 82         while($CurFileSize -lt $FileSize) {
 83           #Write the entire zero array buffer out to the file stream
 84           $Stream.Write($ZeroArray,0, $ZeroArray.Length)
 85           #Increment our file size by the amount of data written to disk
 86           $CurFileSize += $ZeroArray.Length
 87         }
 88       } finally {
 89         #always close our file stream, even if an exception occurred
 90         if($Stream) {
 91           $Stream.Close()
 92         }
 93         #always delete the file if we created it, even if an exception occurred
 94         if( (Test-Path $FilePath) ) {
 95           del $FilePath
 96         }
 97       }
 98     } else {
 99       Write-Error "Unable to locate a volume mounted at $Root"
100     }
101   }
102 }

comments powered by Disqus