How to Configure Storage Spaces with Tiering on Windows Server 2012 R2

Storage Spaces, introduced in Windows Server 2012 and enhanced in Windows Server 2012 R2, is a software-defined storage technology that allows administrators to pool physical drives into virtual storage with resiliency, thin provisioning, and automated tiering. Storage Tiering — the key new feature in 2012 R2 — automatically moves frequently accessed (“hot”) data to faster SSD storage and less frequently accessed (“cold”) data to slower HDD storage, providing the performance of SSD at the cost of HDD for the bulk of your data. This guide covers deploying a tiered Storage Spaces configuration with both SSD and HDD tiers.

Prerequisites

– Windows Server 2012 R2 (Storage Tiering requires 2012 R2; basic Storage Spaces works on 2012)
– At minimum: 2 SSD drives and 2 HDD drives (4 drives total for a mirrored tiered pool)
– Drives must not contain data and must not be system/boot drives
– Drives should be the same manufacturer and capacity within each tier for best results
– The Storage Spaces cmdlets require the Storage module (built-in)
– Administrator rights on the server

Step 1: Inventory Available Disks

# List all available physical disks
Get-PhysicalDisk | Select-Object FriendlyName, MediaType, Size, HealthStatus, 
    OperationalStatus, BusType | Format-Table -AutoSize

# Identify SSD vs HDD
$ssds = Get-PhysicalDisk | Where-Object { $_.MediaType -eq "SSD" }
$hdds = Get-PhysicalDisk | Where-Object { $_.MediaType -eq "HDD" }

Write-Host "Available SSDs: $($ssds.Count)"
$ssds | Select-Object FriendlyName, @{n="SizeGB";e={[math]::Round($_.Size/1GB,0)}} | Format-Table

Write-Host "Available HDDs: $($hdds.Count)"
$hdds | Select-Object FriendlyName, @{n="SizeGB";e={[math]::Round($_.Size/1GB,0)}} | Format-Table

# Check which disks are already in use
Get-PhysicalDisk | Where-Object { $_.CanPool -eq $false } | 
    Select-Object FriendlyName, CannotPoolReason | Format-Table

# Identify disks eligible for pooling
$poolableDisks = Get-PhysicalDisk | Where-Object { $_.CanPool -eq $true }
Write-Host "Poolable disks: $($poolableDisks.Count)"

Step 2: Create a Storage Pool

# Create a new storage pool from all poolable disks
# Include both SSDs and HDDs — the pool contains all physical media
$allPoolable = Get-PhysicalDisk | Where-Object { $_.CanPool -eq $true }

$pool = New-StoragePool `
    -FriendlyName "TieredPool01" `
    -StorageSubSystemFriendlyName (Get-StorageSubSystem -FriendlyName "*Space*").FriendlyName `
    -PhysicalDisks $allPoolable

# Verify pool creation
Get-StoragePool -FriendlyName "TieredPool01" | Format-List FriendlyName, HealthStatus, 
    OperationalStatus, @{n="TotalSizeGB";e={[math]::Round($_.Size/1GB,2)}},
    @{n="AllocatedSizeGB";e={[math]::Round($_.AllocatedSize/1GB,2)}}

# Check physical disks in the pool
Get-StoragePool -FriendlyName "TieredPool01" | 
    Get-PhysicalDisk | 
    Select-Object FriendlyName, MediaType, @{n="SizeGB";e={[math]::Round($_.Size/1GB,0)}} |
    Sort-Object MediaType | Format-Table -AutoSize

Step 3: Create Storage Tiers

Storage Tiers are logical representations of the SSD and HDD groups within the pool. You define tiers separately, then reference them when creating virtual disks:

# Create the SSD (Performance) tier
$ssdTier = New-StorageTier `
    -StoragePoolFriendlyName "TieredPool01" `
    -FriendlyName "SSDTier" `
    -MediaType SSD `
    -Description "Performance tier - SSD media for hot data"

# Create the HDD (Capacity) tier
$hddTier = New-StorageTier `
    -StoragePoolFriendlyName "TieredPool01" `
    -FriendlyName "HDDTier" `
    -MediaType HDD `
    -Description "Capacity tier - HDD media for cold data"

# Verify tiers
Get-StorageTier | Select-Object FriendlyName, MediaType, 
    @{n="SizeGB";e={[math]::Round($_.Size/1GB,0)}} | Format-Table -AutoSize

Step 4: Create a Tiered Virtual Disk

Create a virtual disk that uses both storage tiers. You specify the size from each tier and the resiliency type (mirror is recommended for performance; parity for capacity):

# Create a tiered virtual disk with mirroring on both tiers
# Two-way mirror requires minimum 2 SSDs and 2 HDDs

$ssdTier = Get-StorageTier -FriendlyName "SSDTier"
$hddTier = Get-StorageTier -FriendlyName "HDDTier"

$vdisk = New-VirtualDisk `
    -StoragePoolFriendlyName "TieredPool01" `
    -FriendlyName "TieredVolume01" `
    -StorageTiers @($ssdTier, $hddTier) `
    -StorageTierSizes @(100GB, 500GB) `  # 100 GB SSD tier, 500 GB HDD tier
    -ResiliencySettingName Mirror `
    -WriteCacheSize 1GB `               # Write-back cache on SSD tier
    -AutoWriteCacheSize $false `
    -ProvisioningType Fixed

# Verify the virtual disk
Get-VirtualDisk -FriendlyName "TieredVolume01" | Format-List FriendlyName, HealthStatus, 
    OperationalStatus, ResiliencySettingName, Size, FootprintOnPool

# View tier allocation
Get-VirtualDisk -FriendlyName "TieredVolume01" | 
    Get-StorageTierSupportedSize | Format-List *

Step 5: Initialize and Format the Virtual Disk

# Get the disk object
$disk = Get-VirtualDisk -FriendlyName "TieredVolume01" | Get-Disk

# Initialize the disk
Initialize-Disk -Number $disk.Number -PartitionStyle GPT

# Create a partition using the full disk
$partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize `
    -DriveLetter T

# Format with NTFS (large allocation unit for sequential workloads)
Format-Volume -DriveLetter T -FileSystem NTFS `
    -NewFileSystemLabel "TieredVolume" `
    -AllocationUnitSize 65536 `  # 64KB cluster size for SQL Server/VM workloads
    -Confirm:$false

# Verify
Get-Volume -DriveLetter T | Format-List DriveLetter, FileSystem, HealthStatus, 
    @{n="SizeGB";e={[math]::Round($_.Size/1GB,2)}},
    @{n="FreeGB";e={[math]::Round($_.SizeRemaining/1GB,2)}}

Step 6: Configure Storage Tier Optimization Schedule

Storage Tiering works by analyzing I/O patterns and moving hot data to SSD and cold data to HDD. This optimization runs as a scheduled task by default, but you can customize it:

# View the existing tier optimization task
Get-ScheduledTask -TaskName "Storage Tiers Management" | 
    Select-Object TaskName, State, LastRunTime, NextRunTime | Format-Table

# Run optimization immediately
Optimize-StoragePool -FriendlyName "TieredPool01" -TierAware

# Or optimize a specific volume
Optimize-Volume -DriveLetter T -TierOptimize -Verbose

# Customize the optimization schedule (default: Sunday 1 AM)
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday, Thursday -At "01:00AM"
Set-ScheduledTask -TaskName "Storage Tiers Management" -Trigger $trigger

# Monitor tier heat (which files are "hot")
Get-StorageTierSupportedSize -FriendlyName "TieredVolume01"

# View tier allocation per virtual disk
Get-VirtualDisk -FriendlyName "TieredVolume01" |
    Get-StorageTier | 
    Select-Object FriendlyName, @{n="AllocatedGB";e={[math]::Round($_.AllocatedSize/1GB,2)}},
        @{n="TotalGB";e={[math]::Round($_.Size/1GB,0)}} |
    Format-Table -AutoSize

Step 7: Pin Files to Specific Tiers

For workloads with predictable I/O patterns, you can pin specific files or folders to a tier rather than relying on the heat algorithm:

# Pin a file to the SSD tier (always keep on SSD regardless of access frequency)
Set-FileStorageTier -FilePath "T:SQLDataTempDB.mdf" -DesiredStorageTierFriendlyName "SSDTier"
Set-FileStorageTier -FilePath "T:SQLDataTempLog.ldf" -DesiredStorageTierFriendlyName "SSDTier"

# Pin large backup files to the HDD tier (they are written once, rarely read)
Set-FileStorageTier -FilePath "T:BackupsFullBackup.bak" -DesiredStorageTierFriendlyName "HDDTier"

# View current tier assignments
Get-FileStorageTier -VolumeDriveLetter T | 
    Select-Object FilePath, DesiredStorageTierFriendlyName, State | 
    Format-Table -AutoSize

# Remove a pin (let auto-tiering decide)
Clear-FileStorageTier -FilePath "T:SQLDataTempDB.mdf"

Step 8: Monitor and Report Storage Pool Health

function Get-StoragePoolHealth {
    param([string]$PoolName = "TieredPool01")

    $pool = Get-StoragePool -FriendlyName $PoolName
    Write-Host "=== Storage Pool: $PoolName ===" -ForegroundColor Cyan
    Write-Host "Health: $($pool.HealthStatus)"
    Write-Host "Operational: $($pool.OperationalStatus)"
    Write-Host "Total Size: $([math]::Round($pool.Size/1TB,2)) TB"
    Write-Host "Used: $([math]::Round($pool.AllocatedSize/1GB,0)) GB"

    Write-Host "`n--- Tiers ---"
    Get-StorageTier | Where-Object {$_.StoragePoolFriendlyName -eq $PoolName} |
        Select-Object FriendlyName, MediaType,
            @{n="SizeGB";e={[math]::Round($_.Size/1GB,0)}},
            @{n="AllocatedGB";e={[math]::Round($_.AllocatedSize/1GB,0)}} | Format-Table

    Write-Host "--- Virtual Disks ---"
    Get-StoragePool -FriendlyName $PoolName | Get-VirtualDisk |
        Select-Object FriendlyName, HealthStatus, OperationalStatus, ResiliencySettingName,
            @{n="SizeGB";e={[math]::Round($_.Size/1GB,0)}} | Format-Table

    Write-Host "--- Physical Disks ---"
    Get-StoragePool -FriendlyName $PoolName | Get-PhysicalDisk |
        Select-Object FriendlyName, MediaType, HealthStatus, OperationalStatus,
            @{n="SizeGB";e={[math]::Round($_.Size/1GB,0)}} | Format-Table

    # Check for any degraded or warning conditions
    $unhealthy = Get-StoragePool -FriendlyName $PoolName | Get-PhysicalDisk |
        Where-Object { $_.HealthStatus -ne "Healthy" }
    if ($unhealthy) {
        Write-Warning "$($unhealthy.Count) physical disk(s) in unhealthy state!"
        $unhealthy | Select-Object FriendlyName, HealthStatus, OperationalStatus | Format-Table
    }
}

Get-StoragePoolHealth -PoolName "TieredPool01"

Verification

# Final verification
Get-StoragePool -FriendlyName "TieredPool01" | 
    Select-Object FriendlyName, HealthStatus, OperationalStatus | Format-List

Get-VirtualDisk -FriendlyName "TieredVolume01" | 
    Select-Object FriendlyName, HealthStatus, ResiliencySettingName | Format-List

Get-Volume -DriveLetter T | 
    Select-Object DriveLetter, FileSystem, HealthStatus,
        @{n="SizeGB";e={[math]::Round($_.Size/1GB,2)}},
        @{n="FreeGB";e={[math]::Round($_.SizeRemaining/1GB,2)}} | Format-List

# Disk I/O performance test on the tiered volume
$testFile = "T:tiering_test.tmp"
$sw = [System.Diagnostics.Stopwatch]::StartNew()
[System.IO.File]::WriteAllBytes($testFile, (New-Object byte[] 104857600))  # 100 MB write
$sw.Stop()
Write-Host "100 MB write time: $($sw.ElapsedMilliseconds) ms ($([math]::Round(100/$sw.Elapsed.TotalSeconds,1)) MB/s)"
Remove-Item $testFile

Summary

Storage Spaces with Tiering on Windows Server 2012 R2 delivers automated storage optimization that maximizes both performance and capacity. By creating a pool containing both SSD and HDD drives, defining tier objects for each media type, creating mirrored virtual disks with specific capacity allocations per tier, and letting the heat-based optimizer manage data placement, you achieve SSD-level performance for hot data while maintaining economical HDD storage for cold data — with no manual data movement. The ability to pin files to specific tiers adds precise control for predictable workloads like SQL Server TempDB or VM boot disks, making Storage Spaces Tiering a powerful alternative to expensive all-flash arrays for mixed workload environments.