Evenly Distributing Mailbox During Migrations (Part 1)

If you would like to read the next part in this article series please go to Evenly Distributing Mailbox During Migrations (Part 2).

Introduction

When performing a migration or transition to any version of Exchange, there are always a million and one things to consider. One of them is regarding the distribution of users’ mailboxes in the new system and a migration is the best opportunity to improve the current layout.

It is very important for every Exchange server to be under a load proportional to its relative power (for example, CPU, RAM and disk performance). If a mailbox server is under a great load, then you might have to either move some mailboxes to another server or upgrade its hardware. In some other cases, you might decide to add a new server and redistribute all or some mailboxes.

There are a few approaches regarding the distribution of mailboxes:

  1. Group users in mailbox databases [DB] according to their function. For example, all senior management in DB01, IT staff in DB02, etc.;
  2. Group mailboxes based on their quota limits. We can create a few DBs for users with 500MB limit, a few more DBs for users with 1GB, etc.;
  3. Distribute mailboxes based on user’s name. For example, a DB called DB-AB for all users whose first name start with an A or a B. A database called DB-CD, and so on;
  4. Place the same number of users in every DB independently of their size or type of usage;
  5. Randomly distribute users across all DBs, either by manually selecting a random DB or by using the new Exchange 2010 automatic mailbox distribution logic (see Understanding Automatic Mailbox Distribution).

I personally don’t recommend option 1 and 3. After all, imagine if all your DBs were ok but you had a major issue with only one DB, DB01. Most users would have access to their mailboxes except all your senior management! Regarding option 3, unless you have a very diverse environment, you will always end up with huge DBs such as DB-AB or DB-ST and really small ones like DB-UV as these are typically the most and less common initial letters for names…

In previous editions of Exchange, administrators used Storage Groups to separate users with different backup or high availability requirements for example. With Exchange 2010 you can still have one particular DB for all your senior management staff which gets replicated across 4 servers and all your other users distributed across other DBs that only get replicated between 2 servers. But, in most cases, mixing all users across all DBs is usually the best option.

You can also analyze your IMAP and ActiveSync logs, determine how everyone accesses their mailbox and then create an elaborate plan on how to distribute all users so that all DBs end up with the same load. After all, a user that uses Outlook, OWA, an iPhone and an iPad produces many more Input/Output Operations per Second than a user who only uses Outlook.

However, the last time I did a transition, I went through a different route. Because the whole environment is very reliable, highly available and practically all users have the same “user profile” (everyone uses Outlook and most users have a mobile device), I chose to distribute mailboxes across all DBs not by number, but by their size.

After all migrations were complete, I ended up with all DBs with the exact same size. Also, because normally the mailbox size is proportional to the number of items, all DBs had approximately the same number of items. Obviously they will not remain like this forever as there will always be users who send and receive a lot more e-mails than other users.

To achieve this, I wrote a script to help me automatically distribute the mailboxes. It basically checks the smallest DB and then moves a mailbox to that DB. The issue is when you want to move multiple users at the same time – we will see how to overcome this in the second part of this article. At the end of each migration, the script sends a report similar to this:


Figure 1.1:
Mailbox Migration Report

Script

So let’s start writing the script! First, we will define 4 possible sources. We can move all mailboxes from:

  1. the current Exchange 2007 environment;
  2. a specific Exchange 2007 server;
  3. a specific Exchange 2007 database;
  4. a CSV file with usernames or e-mail addresses.

Note that all these sources are local. This script will not perform a remote mailbox move!

As the target, we will only have the option of specifying an Exchange 2010 server so that mailboxes are distributed across all DBs in that server; if we don’t use this option, than mailboxes will be distributed across all 2010 DBs (only DBs that are mounted and not excluded from provisioning will be considered).

We can also use the BadItemLimit and SuspendWhenReadyToComplete move options as well as specify how many mailboxes we want to move. Finally, we can tell the script to send any errors by e-mail or just output them into the console.

We start by defining the parameters that can be used with the script:

Param (

      [Parameter(Position=0, Mandatory=$False, HelpMessage=“The source server to move mailboxes from.”)]

      [ValidateNotNullOrEmpty()]

      [String] $Source2007Server,

     

      [Parameter(Position=1, Mandatory=$False, HelpMessage=“The source database to move mailboxes from in the format of `”Server\StorageGroup\Database`”, e.g.: `”MMEM01\SG2\DB-AB`”.”)]

      [ValidateNotNullOrEmpty()]

      [String] $Source2007Database,

     

      [Parameter(Position=2, Mandatory=$False, HelpMessage=“The file containing users to be moved.”)]

      [ValidateNotNullOrEmpty()]

      [String] $SourceFile,

     

      [Parameter(Position=3, Mandatory=$False, HelpMessage=“The destination server to move mailboxes to.”)]

      [ValidateNotNullOrEmpty()]

      [String] $Target2010Server,

      [Parameter(Position=4, Mandatory=$False, HelpMessage=“Specifies the number of bad items to skip if the move encounters corrupted items in the mailbox.”)]

      [ValidateScript({$_-ge 0})]

      [Int] $BadItemLimit= 0,

      [Parameter(Position=5, Mandatory=$False, HelpMessage=“Specifies the number of bad items to skip if the move encounters corrupted items in the mailbox.”)]

      [Switch] $SuspendWhenReadyToComplete,

     

      [Parameter(Position=6, Mandatory=$False, HelpMessage=“The maximum number of mailboxes to be moved.”)]

      [ValidateNotNullOrEmpty()]

      [Int] $NumberOfMoves,

     

      [Parameter(Position=7, Mandatory=$False, HelpMessage=“Specifies if error messages should be sent by e-mail.”)]

      [Switch] $SendErrorsByEmail

)

Now let’s write the sendErrorfunction that will send any errors by e-mail or just write them into the console, and validate all the possible sources. In here we validate, for example, that a source server is actually an Exchange 2007 mailbox server and that a source DB is specified using the right format of “Server\Storage_Group\DatabaseName”:

FunctionsendError ([String] $strError)

{

      Write-Warning$strError

      If ($SendErrorsByEmail) { Send-MailMessage-From[email protected]-To[email protected]-Subject“Move Error!”-Body$strError-SMTPserver“smtp.letsexchange.com”-DeliveryNotificationOptiononFailure-PriorityHigh }

      Exit

}

# Validate Input

If ($Source2007Database-and$Source2007Server)

{

      sendError“Please use only -Source2007Server or -Source2007Database, not both.”

}

If ($Source2007Server)

{

      # Verify that the mentioned server is actually a 2007 Mailbox server and that there are mailboxes in it to be moved

      $2007server= Get-ExchangeServer $Source2007Server

      If (!$2007server.IsMailboxServer -or$2007server.AdminDisplayVersion -notmatch“Version 8”)

      {

            sendError“`”$Source2007Server`” is not a valid Exchange 2007 Mailbox server!”

      }

      If ((Get-Mailbox -ResultSize Unlimited -Server $Source2007Server | Measure-Object).Count -eq 0)

      {

            sendError“There are no users in `”$Source2007Server`” server.”

      }

}

If ($Source2007Database)

{

      If ($Source2007Database-notlike“*\*\*”)

      {

            sendError“Please use `”Server\StorageGroup\Database`” format.”

      }

     

      $2007DB= Get-MailboxDatabase $Source2007Database

      If ($2007DB.ExchangeVersion -notlike“*(8*”)

      {

            sendError“`”$Source2007Database`” is not a valid Exchange 2007 database.”

      }

     

      If ((Get-Mailbox -ResultSize Unlimited -Database $Source2007Database | Measure-Object).Count -eq 0)

      {

            sendError“There are no users in `”$Source2007Database`” database.”

      }

}

Now that we know the source is valid and we have at least one mailbox to move, let’s check the target. If we use $Target2010Server then we get all databases in that server. If not, then we get all databases in the Exchange 2010 environment. This works because if we run Get-MailboxDatabase from a 2010 server without using the -IncludePreExchange2010 parameter, we will only get DBs that are on 2010. Also, we only want DBs that are mounted and not excluded from provisioning:

If ($Target2010Server)

{

      $2010server= Get-ExchangeServer $Target2010Server

      If (!$2010server.IsMailboxServer -or$2010server.AdminDisplayVersion -notmatch“Version 14”)

      {

            sendError“`”$Target2010Server`” is not a valid Exchange 2010 Mailbox server!”

      }

     

      $DBs= Get-MailboxDatabase -Server $Target2010Server -Status | ? {$_.IsExcludedFromProvisioning -eq$False-and$_.Mounted -eq$True} | SortName

}

Else

{

      # If we don’t specify a target 2010 mailbox server, then get all available databases in 2010

      $DBs= Get-MailboxDatabase -Status | ? {$_.IsExcludedFromProvisioning -eq$False-and$_.Mounted -eq$True} | SortName

}

# Check if we found any databases to move the users to

If ($DBs.Count -lt 1) { sendError“No suitable target databases were found!” }

Now let’s just initialize a few variables that we will be using throughout this script. These include a variable to hold all the HTML code for the report and some other variables like the count of how many mailboxes we have moved so far and what time the script started.

The method I used to evenly distribute the mailboxes was the following:

  1. Create an array with the target DBs that will be used for the move;
  2. For each one of them, check their current size which is the size of the DB minus the white space;
  3. Pick a mailbox to move;
  4. Go through the array and get the smallest DB;
  5. Increment the DB size (in the array) with the size of the mailbox being moved;
  6. Move the user;
  7. Perform steps 3 to 6 until all mailboxes have been moved or $NumberOfMoves has been reached.

As you probably know, the size of the mailbox reported by the Get-MailboxStatistics cmdlet is not exactly the amount of data that will be moved. For this reason incrementing the size of the DB with this value is not 100% accurate. However, because we are using the same method for all users, after moving everyone the end result will be what we want.

So, we create the array ($arrDBSizes) using the number of target DBs and then capture and save the information we want in it:

# Initialize variables

[String] $global:moveReport=$null

[DateTime] $global:startDate=Get-Date

[Int] $global:intCount= 0

[Int] $intNumberDBs=$DBs.Count

$arrDBSizes=New-Object‘Object[,]’$intNumberDBs,2

[Int] $x= 0

# Get the sizes of all DBs that will be used for the move and save them into the array

ForEach ($dbin$DBs)

{

      $arrDBSizes[$x, 0] =$db.Identity

      $arrDBSizes[$x, 1] = ($db.DatabaseSize $db.AvailableNewMailboxSpace).ToMB()

      $x++

}

After this, $arrDBSizes contains a list of all DBs to be used as targets and their original size. We can check this by using a simple function to print the array. This function is also useful if you want to see what the end state of all DBs will be if you migrated X amount of users without actually moving them!

FunctionprintDBSizesArray

{

      For ([Int] $x= 0; $x-lt$intNumberDBs; $x++)

      {

            Write-Host$arrDBSizes[$x, 0]  $arrDBSizes[$x, 1]

      }

     

      Write-Host“`n”

}

printDBSizesArray

We now have everything regarding the source and target DBs to start the actual move process which will depend on the source method selected. As we go through each user one by one, we will use the moveUser function to actual move the user and which we will define in the second part of this article.

In this script, I used a CSV with the aliases of the mailboxes to be moved and a header called User. If you don’t want to use a header in the CSV or want to use a text file instead, don’t forget to update the code below:

# The source is an Exchange 2007 server, so move all mailboxes found in the server

If ($Source2007Server)

{

      ForEach ($mbxin (Get-Mailbox -ResultSize Unlimited -Server $Source2007Server))

      {

            moveUser$mbx

      }

}

# The source is a specific DB, so move only mailboxes found in that DB

If ($Source2007Database)

{

      ForEach ($mbxin (Get-Mailbox -ResultSize Unlimited -Database $Source2007Database))

      {

            moveUser$mbx

      }

}

If ($SourceFile)

{

      # Read the users to move from a CSV file containing the alias of the users in a column named ‘User’

      Import-Csv$SourceFile | ForEach { moveUser$_.User }

}

# If the user didn’t specify any source, then move all Exchange 2007 mailboxes

If (!$Source2007Server-and!$Source2007Database-and!$SourceFile)

{

      ForEach ($mbxin (Get-MailboxServer | ? {$_.AdminDisplayVersion -match“version 8”} | Get-Mailbox -ResultSize Unlimited)) { moveUser$mbx }

}

Conclusion

In this first part of the two-article series, we wrote the foundation of the script. We validated all the possible sources and targets, and created a list of target databases that will be used for the moves. In the second part we will write the core of the script which includes the moveUser and the produceReport functions.

If you would like to read the next part in this article series please go to Evenly Distributing Mailbox During Migrations (Part 2).

About The Author

Leave a Comment

Your email address will not be published. Required fields are marked *

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Scroll to Top