You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

1. Ditch Single-Recipient Queries for Batch Processing

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 -Recipients parameter in Get-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,}$" }
    
2. Narrow Down the Log Scope as Much as Possible

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 with Where-Object after retrieving logs—server-side filtering reduces the amount of data transferred from each transport server.
3. Parallelize Queries Across Transport Servers

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
4. Bonus: Handle Large Recipient Batches

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.

Key Notes to Avoid Headaches
  • 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 MaxParallelThreads too 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 07:44:10