Saturday, April 7, 2012

Balancing the number of mailboxes across Exchange 2010 and 2007 databases

Introduction

In Exchange 2010, you now have the option to allow mailboxes to be automatically distributed across databases. However, the algorithm used simply randomly allocates the new mailbox to your chosen databases - rather than ensuring the mailbox count is balanced, and doesn't do anything about re-distributing mailboxes if you add new databases.
To help with this, and of course to help with any situation where you want to balance the number of mailboxes across a set of databases, I've written a simple script that help with moving mailboxes to balance out your databases.
Using Generate-DBBalanceScript.ps1
There's nothing too complicated about the script- it doesn't balance based on mailbox size (a future version), but simply creates a script with Move-Mailbox or New-MoveRequest  commands that once complete, balances based on mailbox counts across the databases. You pass it the results of a Get-MailboxDatabase command, along with an output file that will contain the Mailbox move commands.
First of all, lets see it in action:
Of course, my example was the simplest - across all databases. Here's a few examples including the one above, and some others that show how to drill down to databases on specific servers:
Example One - Generate a move file based on all Exchange 2010 Databases:
.\Generate-BalanceMoveRequests.ps1 -DBs (Get-MailboxDatabase) -OutputPowershellFile .\moves.ps1
Example Two - Generate a move file based on  Exchange 2010 Databases located on a single server "servername":
$o=Get-MailboxDatabase -Server servername
.\Generate-BalanceMoveRequests.ps1 -DBs $o -OutputPowershellFile moves07.ps1 -Exchange2010:$false
Example Three - Generate a move file based on  Exchange 2007 Databases located on two servers, "serverone" and "servertwo":
$o=Get-MailboxDatabase | where {$_.Server -eq "serverone" -or $_.Server -eq "servertwo"}
.\Generate-BalanceMoveRequests.ps1 -DBs $o -OutputPowershellFile moves07.ps1 -Exchange2010:$false
Download Generate-DBBalanceScript.ps1
You can download Generate-DBBalanceScript.ps1 here or view the script below.
Hope this helps!
<#
    .SYNOPSIS
    Generates a Powershell file containing Mailbox Move Cmdlets to help balance mailbox databases, based on mailbox count, not size.
    .DESCRIPTION
    Using a list of Databases, works out the average number of mailboxes there should be per DB, then generates move commands to run seperately.
    
    By Steve Goodman
    .PARAMETER DBs 
    Databases to use
    The result of a Get-MailboxDatabase cmdlet. Eg. (Get-MailboxDatabase -Server name)
    .PARAMETER OutputPowershellFile 
    The filename to write, for example C:\output.ps1 or .\output.ps1
    .PARAMETER Exchange2010 
    By default, true. Set to $false to generate Exchagne2007 move-mailbox commands
    .EXAMPLE
    Generate a move file based on all Exchange 2010 Databases
     .\Generate-BalanceMoveRequests.ps1 -DBs (Get-MailboxDatabase) -OutputPowershellFile .\moves.ps1
    .EXAMPLE
    Generate a move file based on  Exchange 2007 Databases located on a single server "servername"
    $o=Get-MailboxDatabase -Server servername
    .\Generate-BalanceMoveRequests.ps1 -DBs $o -OutputPowershellFile moves07.ps1 -Exchange2010:$false
    .EXAMPLE
    Generate a move file based on  Exchange 2007 Databases located on two servers, "serverone" and "servertwo"
    $o=Get-MailboxDatabase | where {$_.Server -eq "serverone" -or $_.Server -eq "servertwo"}
    .\Generate-BalanceMoveRequests.ps1 -DBs $o -OutputPowershellFile moves07.ps1 -Exchange2010:$false
    
    #>

param(
    [parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false,HelpMessage="Mailbox database object")][array]$DBs,
    [parameter(Position=1,Mandatory=$true,ValueFromPipeline=$false,HelpMessage="Filename for output Powershell script")][string]$OutputPowershellFile,
    [parameter(Position=2,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Is this for Exchange 2010")][bool]$Exchange2010=$true
    )

# Check for Exchange cmdlets
if (!(Get-Command Get-MailboxDatabase -ErrorAction SilentlyContinue))
{
    throw "Exchange Cmdlets not available";
}
# Check input object
if ($DBs[0].GetType().Name -ne "MailboxDatabase") 
{
    throw "Object is not an array of mailbox databases"
}
if ($DBs.Count -eq 1)
{
    throw "You can't balance a single database";
}

# Check file does not exist
if ((Test-Path $OutputPowershellFile))
{
    throw "File $($OutputPowershellFile) already exists";
}

# Initialise file
"# Mailbox Mail Powershell Script File, generated $(date)`r`n"|Out-File -FilePath $OutputPowershellFile -NoClobber
if (!(Test-Path $OutputPowershellFile))
{
    throw "Could not write to $($OutputPowershellFile)";
}

# Initialise Variables
[int]$TotalMailboxes=0# Total Mailboxes
[int]$BalancedCount=0# Balanced Number of Mailboxes per DB
[array]$DBCounters=@()# Array with DB Id, Mailbox Count and Mailbox listing
[array]$UA_DBCounters=@()# Under allocated list from above, populated later
[array]$OA_DBCounters=@()# Over allocated list from above, populated later
[array]$PA_DBCounters=@()# Perfectly allocated list from above, populated later
[string]$Output=""# Variable to write output text to

# Gather initial mailbox counts
Write-Host "Gathering Mailbox Counts"
for ($i = 0$i -lt $DBs.Count; $i++)
{
    Write-Progress -activity "Gathering Mailbox Counts" -status "Processing Database $($DBs[$i].Identity)" -PercentComplete (($i / $DBs.Count)  * 100)
    $Mailboxes = (Get-Mailbox  -Database $DBs[$i].Identity -ResultSize Unlimited | select Identity)
    $DBCounters=$DBCounters+1;
    $DBCounters[$DBCounters.Count-1] = New-Object Object
    $DBCounters[$DBCounters.Count-1] | Add-Member NoteProperty Database $DBs[$i].Identity    
    $DBCounters[$DBCounters.Count-1] | Add-Member NoteProperty Total $Mailboxes.Count
    $DBCounters[$DBCounters.Count-1] | Add-Member NoteProperty Mailboxes $Mailboxes
    $TotalMailboxes+=$Mailboxes.Count 
}
Write-Progress -Activity "Gathering Mailbox Counts" -Completed -Status "Completed"
Write-Host "DB Counts are as follows:"
$DBCounters|Select Database,Total
$BalancedCount = $TotalMailboxes / $DBs.Count
Write-Host "Found $($TotalMailboxes) mailboxes across $($DBs.Count) databases. Aiming for $($BalancedCount) mailboxes per database."

Write-Host "Sorting Databases into over, under and perfectly allocated."
# Sort DBs into under-allocated, over-allocated and perfectly allocated
for ($i = 0$i -lt $DBCounters.Count; $i++)
{
    Write-Progress -activity "Sorting Databases into over, under and perfectly allocated" -status "Processing Database $($DBCounters[$i].Database)" -PercentComplete (($i / $DBCounters.Count)  * 100)
    if ($DBCounters[$i].Total -lt $BalancedCount)
    {
        $UA_DBCounters=$UA_DBCounters+1;
        $UA_DBCounters[$UA_DBCounters.Count-1] = $DBCounters[$i];
    } elseif ($DBCounters[$i].Total -gt $BalancedCount) {
        $OA_DBCounters=$OA_DBCounters+1;
        $OA_DBCounters[$OA_DBCounters.Count-1] = $DBCounters[$i];
    } else {
        $PA_DBCounters=$PA_DBCounters+1;
        $PA_DBCounters[$PA_DBCounters.Count-1] = $DBCounters[$i];
    }
}
Write-Progress -activity "Sorting Databases into over, under and perfectly allocated" -Completed -Status "Completed"

Write-Host "Found $($OA_DBCounters.Count) over, $($UA_DBCounters.Count) under and $($PA_DBCounters.Count) perfectly allocated";
# Make move list
Write-Host "Generating Powershell File '$($OutputPowershellFile)' to Balance DBs."
for ($i = 0$i -lt $OA_DBCounters.Count; $i++)
{
    Write-Progress -activity "Generating Powershell File '$($OutputPowershellFile)' to Balance DBs" -status "Processing Database $($OA_DBCounters[$i].Database)" -PercentComplete (($i / $OA_DBCounters.Count)  * 100)
    # Get the mailbox list
    [array]$OA_Mailboxes = $OA_DBCounters[$i].Mailboxes
    $UA_DBPointer=0;
    for ($j=$BalancedCount$j -lt $OA_Mailboxes.Count; $j++) 
    {
        # Move to next underallocated DB if required
        if ($UA_DBCounters[$UA_DBPointer].Total -ge $BalancedCount -and $UA_DBPointer -lt $UA_DBCounters.Count-1)
        {
                $UA_DBPointer++;
        }
        Write-Progress -activity "Generating move commands" -status "Processing $($OA_Mailboxes[$j].Identity)" -PercentComplete (($j / $OA_Mailboxes.Count)  * 100)
        # Generate a Powershell command for the move request
        if ($Exchange2010)
        {
            $Output+="New-MoveRequest -Identity '$($OA_Mailboxes[$j].Identity)' -TargetDatabase '$($UA_DBCounters[$UA_DBPointer].Database)' -Confirm:" + '$false' + "`r`n";
        } else {
            $Output+="Move-Mailbox -Identity '$($OA_Mailboxes[$j].Identity)' -TargetDatabase '$($UA_DBCounters[$UA_DBPointer].Database)'`r`n";
        }        
        $OA_DBCounters[$i].Total--# Take one mailbox of the total on the overallocated DB
        $UA_DBCounters[$UA_DBPointer].Total++# Add one mailbox tot the total of the current underallocated DB
        
        
    }
    Write-Progress -activity "Generating move commands" -Completed -Status "Completed"
}
Write-Progress -activity "Generating Powershell File '$($OutputPowershellFile)' to Balance DBs" -Completed -Status "Completed"
Write-Host "DB Counts will be as follows after executing $($OutputPowershellFile):"
$OA_DBCounters|Select Database,Total
$UA_DBCounters|Select Database,Total
$PA_DBCounters|Select Database,Total
Write-Host "Writing Powershell file '$($OutputPowershellFile)'"
$Output | Out-File -FilePath $OutputPowershellFile -NoClobber -Append