Exchange 2013 CU9批量查询6000+联系人30天使用状态的性能问题
Hey Scott, let's break down how to fix that agonizingly slow query you're dealing with—6000+ contacts, 32 transport servers, and 30 days of logs is a perfect storm for brute-force methods. Here's how to optimize this to get results in a reasonable time:
Right now, if you're looping through each contact one by one, you're making thousands of separate calls to each transport server—this is the biggest bottleneck. Instead:
- Import all your contacts first, deduplicate them, and pass the entire array to the
-Recipientsparameter inGet-MessageTrackingLog. This lets Exchange handle server-side filtering for multiple recipients at once, which is way faster than repeated single queries. - Example of importing and cleaning recipients:
# From CSV (adjust the column name to match your file) $allRecipients = Import-CSV "contacts.csv" | Select-Object -ExpandProperty EmailAddress -Unique # Or from a plain text list # $allRecipients = Get-Content "contacts.txt" | Select-Object -Unique # Validate format to avoid bad queries $validRecipients = $allRecipients | Where-Object { $_ -match "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" }
Don't let Exchange scan more logs than necessary:
- Explicitly set time bounds: Add
-Start (Get-Date).AddDays(-30) -End (Get-Date)to your query. Without this, Exchange might scan older logs that you don't care about. - Stick to server-side filters: Use parameters like
-EventId Send(which you're already doing) instead of filtering withWhere-Objectafter retrieving logs—server-side filtering reduces the amount of data transferred from each transport server.
Querying 32 servers one after another is going to take forever. Use PowerShell runspaces (lighter and faster than Start-Job) to query multiple servers at the same time. Here's a simplified example of how to implement this:
function Get-RecipientUsageReport { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string[]]$Recipients, [datetime]$StartDate = (Get-Date).AddDays(-30), [datetime]$EndDate = (Get-Date), [int]$MaxParallelThreads = 8 # Adjust based on your server's capacity ) # Get all target transport servers $transportServers = Get-TransportService *EXC* # Set up runspace pool for parallel processing $runspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxParallelThreads) $runspacePool.Open() $jobQueue = @() foreach ($server in $transportServers) { # Create a script block for each server's query $scriptBlock = { param($ServerName, $RecipientsList, $Start, $End) Get-MessageTrackingLog -Server $ServerName -Recipients $RecipientsList -EventId Send -Start $Start -End $End | Select-Object RecipientAddress, Timestamp, Sender } # Add the job to the runspace pool $psJob = [powershell]::Create().AddScript($scriptBlock).AddArgument($server.Name).AddArgument($Recipients).AddArgument($StartDate).AddArgument($EndDate) $psJob.RunspacePool = $runspacePool $jobQueue += @{ Job = $psJob; AsyncResult = $psJob.BeginInvoke() } } # Collect results from all jobs $allResults = @() foreach ($job in $jobQueue) { try { $allResults += $job.Job.EndInvoke($job.AsyncResult) } catch { Write-Warning "Failed to retrieve logs from $($job.Job.Arguments[0]): $_" } $job.Job.Dispose() } # Clean up runspace pool $runspacePool.Close() $runspacePool.Dispose() # Generate a summary report $usageSummary = $allResults | Group-Object RecipientAddress | ForEach-Object { [PSCustomObject]@{ EmailAddress = $_.Name LastUsedTimestamp = $_.Group.Timestamp | Sort-Object -Descending | Select-Object -First 1 TotalSent = $_.Count } } # Add entries for recipients with no activity foreach ($recipient in $Recipients) { if (-not ($usageSummary.EmailAddress -contains $recipient)) { $usageSummary += [PSCustomObject]@{ EmailAddress = $recipient LastUsedTimestamp = $null TotalSent = 0 } } } return $usageSummary } # Usage example $report = Get-RecipientUsageReport -Recipients $validRecipients -MaxParallelThreads 10 $report | Export-CSV "RecipientUsageReport.csv" -NoTypeInformation
If you hit limits with the -Recipients parameter (e.g., too many addresses in one query), split your recipient list into smaller batches (like 1000 addresses per batch) and process each batch sequentially while still using parallel server queries. This avoids overwhelming Exchange with too many recipients in a single request.
- Check log retention: Confirm your Exchange 2013 servers are retaining at least 30 days of message tracking logs (default is 30 days, but it's worth verifying).
- Adjust thread count: Don't set
MaxParallelThreadstoo high (e.g., don't exceed 16 on most systems)—this can cause resource contention on your management server or transport servers. - Test with a small subset: Run the function with 10-20 recipients first to make sure it works before scaling to 6000+.
内容的提问来源于stack exchange,提问作者ScottUK

