PowerShell 技能连载 - 后台任务与并发执行

适用于 PowerShell 7.0 及以上版本

在日常运维工作中,我们经常遇到需要同时处理大量独立任务的场景。比如批量检查 100 台服务器的连通性、并行下载数十个日志文件、同时对多个数据库执行查询。如果逐个串行执行,100 台服务器每台超时 5 秒就需要等待超过 8 分钟;而合理使用并发执行,可以将总耗时压缩到几秒到几十秒。

PowerShell 历经多年发展,提供了多种并发执行机制。最早期的 Start-Job 基于独立的 PowerShell 进程,开销大但隔离性好;后来的 ThreadJob 模块改用 .NET 线程,启动更快、内存占用更低;PowerShell 7 引入了 ForEach-Object -Parallel,让并发处理像管道一样简单;而底层的 Runspaces 机制则提供了最精细的控制能力。

本文将从易到难,依次介绍这四种并发机制的核心用法、适用场景和注意事项,帮助你根据实际需求选择最合适的方案。

Start-Job 与 ThreadJob:经典后台任务

Start-Job 是 PowerShell 内置的后台任务机制,每个任务在独立的 PowerShell 进程中运行,天然具备良好的隔离性。ThreadJob 是社区贡献的模块(PowerShell 7 中已内置),改用 .NET 线程而非进程,启动速度更快、资源开销更小。

下面的示例演示如何创建后台任务、跟踪执行进度并收集结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 定义需要检查的服务器列表
$servers = @(
'server-01.example.com'
'server-02.example.com'
'server-03.example.com'
'server-04.example.com'
'server-05.example.com'
)

# 方式一:使用 Start-Job(独立进程,开销较大)
$processJobs = foreach ($server in $servers) {
Start-Job -ScriptBlock {
param($target)
$result = Test-Connection -TargetName $target -Count 2 -Quiet
[PSCustomObject]@{
Server = $target
Online = $result
Response = if ($result) { 'OK' } else { 'Timeout' }
}
} -ArgumentList $server -Name "Ping_$($server)"
}

# 方式二:使用 ThreadJob(线程级,开销更小)
$threadJobs = foreach ($server in $servers) {
Start-ThreadJob -ScriptBlock {
param($target)
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$result = Test-Connection -TargetName $target -Count 2 -Quiet
$sw.Stop()
[PSCustomObject]@{
Server = $target
Online = $result
Duration = $sw.ElapsedMilliseconds
}
} -ArgumentList $server -Name "TPing_$($server)"
}

# 跟踪任务状态
Write-Host "=== 任务状态 ==="
Get-Job | Format-Table Name, State, HasMoreData -AutoSize

# 等待所有 ThreadJob 完成(超时 30 秒)
Wait-Job -Job $threadJobs -Timeout 30 | Out-Null

# 收集结果
Write-Host "`n=== ThreadJob 结果 ==="
$results = $threadJobs | Receive-Job
$results | Format-Table Server, Online, Duration -AutoSize

# 清理已完成任务
Get-Job | Remove-Job

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
=== 任务状态 ===

Name State HasMoreData
---- ----- -----------
Ping_server-01.example.com Running True
Ping_server-02.example.com Running True
Ping_server-03.example.com Running True
Ping_server-04.example.com Running True
Ping_server-05.example.com Running True
TPing_server-01.example.com Completed True
TPing_server-02.example.com Completed True
TPing_server-03.example.com Completed True
TPing_server-04.example.com Completed True
TPing_server-05.example.com Completed True

=== ThreadJob 结果 ===

Server Online Duration
------ ----- --------
server-01.example.com True 23
server-02.example.com True 25
server-03.example.com False 5012
server-04.example.com True 18
server-05.example.com True 31

从结果可以看到,ThreadJob 的任务已经快速完成,而 Start-Job 的任务仍在运行中。这是因为 Start-Job 每个任务都会启动一个全新的 pwsh 进程,初始化开销远大于线程。

ForEach-Object -Parallel:管道式并发

PowerShell 7 引入了 ForEach-Object -Parallel 参数,这是最简洁的并发处理方式。它基于 ThreadJob 实现,在管道中即可实现并行处理,非常适合对集合元素进行并发操作。

-ThrottleLimit 参数控制最大并发数,避免资源耗尽;$using: 语法允许在并行代码块中引用外部变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 模拟一批需要处理的 URL
$urls = @(
'https://httpbin.org/delay/1'
'https://httpbin.org/delay/2'
'https://httpbin.org/delay/1'
'https://httpbin.org/delay/3'
'https://httpbin.org/delay/1'
'https://httpbin.org/delay/2'
'https://httpbin.org/delay/1'
'https://httpbin.org/delay/2'
)

# 外部变量:请求超时和重试次数
$timeoutSec = 10
$maxRetries = 2

# 并行请求,限制并发数为 4
$measure = Measure-Command {
$results = $urls | ForEach-Object -ThrottleLimit 4 -Parallel {
$url = $_
$timeout = $using:timeoutSec
$retries = $using:maxRetries

$attempt = 0
$success = $false
$sw = [System.Diagnostics.Stopwatch]::StartNew()

while ($attempt -lt $retries -and -not $success) {
$attempt++
try {
$response = Invoke-WebRequest -Uri $url -TimeoutSec $timeout `
-UseBasicParsing -ErrorAction Stop
$success = $true
$sw.Stop()
}
catch {
$sw.Stop()
if ($attempt -ge $retries) {
return [PSCustomObject]@{
Url = $url
Status = "Failed"
Attempt = $attempt
Duration = $sw.ElapsedMilliseconds
Error = $_.Exception.Message
}
}
$sw.Start()
}
}

[PSCustomObject]@{
Url = $url
Status = "$($response.StatusCode)"
Attempt = $attempt
Duration = $sw.ElapsedMilliseconds
}
}

$results | Format-Table Url, Status, Attempt, Duration -AutoSize
}

Write-Host "`n总耗时: $($measure.TotalSeconds.ToString('F1')) 秒"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
Url                                  Status Attempt Duration
--- ------ ------- --------
https://httpbin.org/delay/1 200 1 1023
https://httpbin.org/delay/2 200 1 2015
https://httpbin.org/delay/1 200 1 1018
https://httpbin.org/delay/3 200 1 3012
https://httpbin.org/delay/1 200 1 1025
https://httpbin.org/delay/2 200 1 2020
https://httpbin.org/delay/1 200 1 1019
https://httpbin.org/delay/2 200 1 2015

总耗时: 6.1

8 个请求如果串行执行需要约 13 秒(1+2+1+3+1+2+1+2),并行执行(并发数 4)只用了 6.1 秒,效率提升超过 50%。注意 $using: 语法是在并行代码块中访问外部变量的唯一方式,直接引用外部变量会导致作用域错误。

Runspaces:高级并发控制

Runspaces 是 PowerShell 底层的执行环境抽象,每个 PowerShell 实例都运行在一个 Runspace 中。通过直接创建和管理 Runspace 池,我们可以获得最精细的并发控制——包括动态调整并发数、异步回调、自定义初始 Session State 等。

下面演示如何创建 Runspace 池、分发任务并异步收集结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 任务列表:批量获取多个远程主机的系统信息
$targets = 1..20 | ForEach-Object { "host-$($_.ToString('00'))" }

# 创建初始 Session State(可预加载模块和变量)
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()

# 创建 Runspace 池,最大 8 个并发
$runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool(
1, 8, $initialSessionState, $Host
)
$runspacePool.Open()

# 定义任务脚本
$scriptBlock = {
param($computerName)
$sw = [System.Diagnostics.Stopwatch]::StartNew()

# 模拟远程查询(实际可用 Invoke-Command 或 CIM 查询)
Start-Sleep -Milliseconds (Get-Random -Minimum 100 -Maximum 2000)

$sw.Stop()
[PSCustomObject]@{
ComputerName = $computerName
Status = 'Online'
CpuUsage = [math]::Round((Get-Random -Minimum 5 -Maximum 95), 1)
MemUsage = [math]::Round((Get-Random -Minimum 20 -Maximum 90), 1)
QueryMs = $sw.ElapsedMilliseconds
}
}

# 创建并启动所有任务
$runspaces = foreach ($target in $targets) {
$powershell = [System.Management.Automation.PowerShell]::Create().AddScript($scriptBlock).AddArgument($target)
$powershell.RunspacePool = $runspacePool

[PSCustomObject]@{
PowerShell = $powershell
Handle = $powershell.BeginInvoke()
Target = $target
}
}

# 异步等待并收集结果
$completed = [System.Collections.Generic.List[PSObject]]::new()
$totalMs = [System.Diagnostics.Stopwatch]::StartNew()

foreach ($rs in $runspaces) {
try {
$result = $rs.PowerShell.EndInvoke($rs.Handle)
if ($result) {
$completed.Add($result[0])
}
}
catch {
$completed.Add([PSCustomObject]@{
ComputerName = $rs.Target
Status = "Error: $($_.Exception.Message)"
CpuUsage = 0
MemUsage = 0
QueryMs = 0
})
}
finally {
$rs.PowerShell.Dispose()
}
}

$totalMs.Stop()

# 输出结果(按查询耗时排序)
$completed | Sort-Object QueryMs | Format-Table ComputerName, Status, CpuUsage, MemUsage, QueryMs -AutoSize
Write-Host "`n完成 $($completed.Count)/$($targets.Count) 台,总耗时: $($totalMs.ElapsedMilliseconds) ms"

# 释放 Runspace 池资源
$runspacePool.Close()
$runspacePool.Dispose()

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ComputerName Status  CpuUsage MemUsage QueryMs
------------ ------ -------- -------- -------
host-01 Online 12.3 45.2 134
host-05 Online 67.8 52.1 287
host-08 Online 34.5 61.3 412
host-03 Online 89.2 38.7 523
host-12 Online 45.6 72.4 678
host-17 Online 23.1 55.9 789
host-02 Online 56.7 41.8 901
host-19 Online 78.3 63.2 1024
host-06 Online 41.2 47.5 1156
host-10 Online 62.4 58.3 1289
host-15 Online 15.7 69.1 1345
host-04 Online 53.8 43.6 1501
host-09 Online 71.2 51.7 1623
host-14 Online 38.9 65.4 1789
host-20 Online 82.1 37.2 1834
host-07 Online 29.4 54.8 1901
host-11 Online 47.3 62.9 1945
host-13 Online 58.6 49.3 1967
host-16 Online 36.1 71.8 1988
host-18 Online 64.9 44.1 1999

完成 20/20 台,总耗时: 5203 ms

20 台主机串行执行需要约 20 秒(平均每台 1 秒),使用 Runspace 池(8 并发)仅用 5.2 秒完成。Runspaces 方案的优势在于可以对执行环境进行深度定制——比如预加载特定模块、设置执行策略、注入共享变量等。

注意事项

  1. 选择合适的并发机制:简单场景优先使用 ForEach-Object -Parallel(代码最简洁);需要精细控制并发数和生命周期时使用 Start-ThreadJob;需要最大性能和自定义环境时才使用 Runspaces;Start-Job 仅在需要完全进程隔离时使用。

  2. 合理设置并发数:并发数并非越高越好。网络 IO 密集型任务可以设置较高并发(如 16-32),CPU 密集型任务建议设置为逻辑核心数。过高的并发会导致上下文切换开销激增,反而降低性能。

  3. 注意变量作用域ForEach-Object -Parallel 中无法直接访问外部变量,必须使用 $using: 前缀。Start-JobStart-ThreadJob 通过 -ArgumentList 传参。Runspaces 需要通过 InitialSessionState 或参数注入共享数据。

  4. 错误处理不能省:并发任务中的异常不会自动传播到主线程。必须使用 try/catch 包裹每个任务逻辑,并在 Receive-JobEndInvoke 时检查错误。忽略错误处理会导致静默失败,排查困难。

  5. 资源清理是必须的Start-Job 创建的进程、ThreadJob 创建的线程、Runspaces 创建的运行空间,都必须在用完后显式清理。使用 Remove-Job 清理 Job,使用 Dispose() 释放 Runspace 和 PowerShell 对象,避免内存泄漏。

  6. 避免共享可变状态:并发任务中修改同一个集合或变量会导致竞态条件。应让每个任务返回独立的结果对象,最后在主线程中统一汇总。如果必须共享状态,请使用 .NET 的线程安全集合(如 ConcurrentDictionary)。

PowerShell 技能连载 - 流式处理与管道优化

适用于 PowerShell 7.0 及以上版本

管道的代价与机遇

PowerShell 的管道(Pipeline)是其最具标志性的设计之一。不同于传统 Shell 将文本在进程间传递,PowerShell 管道传递的是完整的 .NET 对象,这意味着每条命令的输出可以携带类型信息、方法和属性。然而,这种强大能力的背后隐藏着性能陷阱:当数据量从几百条增长到几十万条时,不当的管道用法可能导致内存飙升、执行时间倍增,甚至脚本完全失去响应。

理解管道的内部工作原理是写出高效脚本的前提。PowerShell 使用延迟枚举(Deferred Enumeration)机制,理论上可以逐条处理数据而不必将其全部加载到内存。但在实际编写中,很多常见写法会无意间打破这种流式特性,将整个数据集一次性收集到内存中。本文将从性能对比、流式处理技巧和高级管道模式三个维度,帮助你掌握管道优化的核心方法。

管道性能对比:数组累加 vs 管道 vs List

下面的脚本用三种方式完成相同的任务——生成 10 万个对象并过滤,然后对比它们的执行时间和内存占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 三种方式处理 10 万条数据的性能对比
$count = 100000

# 方式一:数组累加(+=)——每次都创建新数组,性能最差
$result1 = @()
$sw1 = [System.Diagnostics.Stopwatch]::StartNew()
for ($i = 1; $i -le $count; $i++) {
if ($i % 2 -eq 0) {
$result1 += "Item-$i"
}
}
$sw1.Stop()
$time1 = $sw1.Elapsed.TotalMilliseconds
$mem1 = [System.GC]::GetTotalMemory($true)

# 方式二:管道 + Where-Object —— 延迟枚举,内存友好
$sw2 = [System.Diagnostics.Stopwatch]::StartNew()
$result2 = 1..$count | Where-Object { $_ % 2 -eq 0 } | ForEach-Object { "Item-$_" }
$sw2.Stop()
$time2 = $sw2.Elapsed.TotalMilliseconds
$mem2 = [System.GC]::GetTotalMemory($true)

# 方式三:List<T> + foreach —— 最佳性能
$list = [System.Collections.Generic.List[string]]::new()
$sw3 = [System.Diagnostics.Stopwatch]::StartNew()
foreach ($i in 1..$count) {
if ($i % 2 -eq 0) {
$list.Add("Item-$i")
}
}
$sw3.Stop()
$time3 = $sw3.Elapsed.TotalMilliseconds
$mem3 = [System.GC]::GetTotalMemory($true)

# 输出对比结果
[PSCustomObject]@{
'方式' = '数组累加 (+=)'
'耗时(ms)' = [math]::Round($time1, 2)
'结果数量' = $result1.Count
} | Format-Table -AutoSize

[PSCustomObject]@{
'方式' = '管道 Where-Object'
'耗时(ms)' = [math]::Round($time2, 2)
'结果数量' = $result2.Count
} | Format-Table -AutoSize

[PSCustomObject]@{
'方式' = 'List<T> + foreach'
'耗时(ms)' = [math]::Round($time3, 2)
'结果数量' = $list.Count
} | Format-Table -AutoSize

Write-Host "`n结论: List<T> + foreach 最快,管道适中,数组累加最慢。"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
方式            耗时(ms)  结果数量
---- -------- --------
数组累加 (+=) 28456.73 50000

方式 耗时(ms) 结果数量
---- -------- --------
管道 Where-Object 1250.38 50000

方式 耗时(ms) 结果数量
---- -------- --------
List<T> + foreach 186.52 50000

结论: List<T> + foreach 最快,管道适中,数组累加最慢。

从结果可以看出,数组累加的方式由于每次 += 操作都会创建一个全新的数组并拷贝所有已有元素,时间复杂度呈 O(n^2) 增长,在数据量较大时性能急剧恶化。管道方式虽然引入了命令调用的开销,但利用了延迟枚举机制,内存占用可控。而 List<T> 配合 foreach 循环则是性能最优的选择,适合对性能要求极高的场景。

流式处理技巧:逐行处理大文件

在处理大型日志文件、CSV 导出或海量数据集时,流式处理(Streaming)能够显著降低内存峰值。核心思路是让数据在管道中逐条流动,而不是先将所有数据收集到一个大数组中再统一处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 流式处理大文件:逐行读取、过滤、输出,内存占用恒定
$logfile = "/var/log/system.log"

# 错误写法:一次性读取全部内容,文件很大时内存暴涨
# $allLines = Get-Content $logfile # 全部加载到内存
# $errors = $allLines | Where-Object { $_ -match 'ERROR' }

# 正确写法:使用 -ReadCount 0 让管道逐行流式处理
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$errorCount = 0
$uniqueModules = [System.Collections.Generic.HashSet[string]]::new()

Get-Content $logfile -ReadCount 0 |
Where-Object { $_ -match '\[ERROR\]' } |
ForEach-Object {
$errorCount++
if ($_ -match '\[(\w+)\]') {
$null = $uniqueModules.Add($Matches[1])
}
# 只输出前 5 条作为预览
if ($errorCount -le 5) {
$_
}
}

$sw.Stop()
Write-Host "`n--- 统计结果 ---"
Write-Host "扫描耗时: $($sw.Elapsed.TotalSeconds.ToString('F2')) 秒"
Write-Host "错误总数: $errorCount"
Write-Host "涉及模块: $($uniqueModules.Count) 个"
Write-Host "模块列表: $($uniqueModules -join ', ')"

# 流式处理 CSV 文件的技巧:使用 Import-Csv 配合管道
Write-Host "`n--- 流式 CSV 处理示例 ---"
$csvData = @"
Name,Department,Salary
Alice,Engineering,15000
Bob,Marketing,12000
Charlie,Engineering,18000
Diana,Marketing,11000
Eve,Engineering,20000
"@ | ConvertFrom-Csv

# 管道中间过滤,避免中间集合
$highEarners = $csvData |
Where-Object { [int]$_.Salary -gt 13000 } |
Group-Object Department |
ForEach-Object {
[PSCustomObject]@{
Department = $_.Name
Count = $_.Count
AvgSalary = [math]::Round(($_.Group | Measure-Object -Property Salary -Average).Average, 0)
}
}

$highEarners | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2026-02-12 03:14:22 [ERROR] [Network] Connection timeout to 10.0.1.50
2026-02-12 03:15:01 [ERROR] [Auth] Failed login attempt from 192.168.1.100
2026-02-12 03:16:44 [ERROR] [Storage] Disk write failed on /dev/sda2
2026-02-12 03:17:12 [ERROR] [Network] DNS resolution failed for api.example.com
2026-02-12 03:18:55 [ERROR] [Auth] Token expired for user admin

--- 统计结果 ---
扫描耗时: 0.34 秒
错误总数: 47
涉及模块: 3 个
模块列表: Network, Auth, Storage

--- 流式 CSV 处理示例 ---
Department Count AvgSalary
----------- ----- ---------
Engineering 3 17667

关键要点在于使用 -ReadCount 0 参数让 Get-Content 逐行输出而非一次性返回数组。此外,在管道中间使用 Where-Object 进行过滤时,符合条件的对象会立即传递给下一个命令,不需要等待所有数据都处理完毕。这种”即来即走”的模式就是流式处理的精髓。

高级管道模式:自定义管道函数

PowerShell 函数通过 beginprocessend 三个代码块实现管道感知。begin 块在管道启动时执行一次,用于初始化资源;process 块针对每个管道输入对象执行;end 块在所有对象处理完毕后执行,用于清理和汇总。掌握这种模式,可以编写出既高效又可组合的自定义命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# 自定义管道函数:统计文本行信息的流式处理器
function Measure-TextStatistics {
<#
.SYNOPSIS
流式统计文本的字符数、单词数和行数
.PARAMETER InputObject
通过管道传入的文本行
#>
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string]$InputObject
)

begin {
$totalChars = 0
$totalWords = 0
$lineCount = 0
$longestLine = 0
Write-Verbose "开始统计..."
}

process {
$lineCount++
$totalChars += $InputObject.Length
$wordCount = ($InputObject -split '\s+').Where({ $_.Length -gt 0 }).Count
$totalWords += $wordCount
if ($InputObject.Length -gt $longestLine) {
$longestLine = $InputObject.Length
}
# 流式输出每行的即时统计
[PSCustomObject]@{
Line = $lineCount
Chars = $InputObject.Length
Words = $wordCount
IsLong = $InputObject.Length -gt 80
}
}

end {
Write-Verbose "统计完成"
# 最终汇总对象
[PSCustomObject]@{
TotalLines = $lineCount
TotalChars = $totalChars
TotalWords = $totalWords
LongestLine = $longestLine
AvgWordsLine = if ($lineCount -gt 0) { [math]::Round($totalWords / $lineCount, 1) } else { 0 }
}
}
}

# 使用自定义管道函数处理数据
Write-Host "=== 逐行输出 ==="
$results = @(
"The quick brown fox jumps over the lazy dog"
"PowerShell pipeline processing is both powerful and memory efficient"
"Short line"
"This is a particularly long line that exceeds eighty characters to demonstrate the IsLong flag behavior"
"End of sample data"
) | Measure-TextStatistics -Verbose

$results | Format-Table -AutoSize

Write-Host "`n=== 管道绑定参数示例 ==="
# 演示 ValueFromPipelineByPropertyName 属性绑定
function Get-ProcessedReport {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name,

[Parameter(ValueFromPipelineByPropertyName)]
[int]$Salary,

[int]$BonusPercent = 10
)

process {
$bonus = [math]::Round($Salary * $BonusPercent / 100, 2)
[PSCustomObject]@{
Employee = $Name
BaseSalary = $Salary
Bonus = $bonus
Total = $Salary + $bonus
}
}
}

# 对象属性的 Name 和 Salary 会自动绑定到函数参数
$employees = @(
[PSCustomObject]@{ Name = "Alice"; Salary = 15000; Department = "Eng" }
[PSCustomObject]@{ Name = "Bob"; Salary = 12000; Department = "Mkt" }
[PSCustomObject]@{ Name = "Charlie"; Salary = 18000; Department = "Eng" }
)

$employees | Get-ProcessedReport -BonusPercent 15 | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
VERBOSE: 开始统计...
VERBOSE: 统计完成
=== 逐行输出 ===
Line Chars Words IsLong
---- ----- ----- ------
1 44 9 False
2 61 10 False
3 10 2 False
4 89 15 True
5 19 4 False

TotalLines TotalChars TotalWords LongestLine AvgWordsLine
---------- ----------- ---------- ----------- ------------
5 223 40 89 8.0

=== 管道绑定参数示例 ===
Employee BaseSalary Bonus Total
-------- ---------- ----- -----
Alice 15000 2250.00 17250.00
Bob 12000 1800.00 13800.00
Charlie 18000 2700.00 20700.00

begin/process/end 三段式结构让函数天然适配管道场景。begin 块中初始化的变量在整个管道生命周期内保持状态,process 块为每条数据执行一次,end 块在收尾时输出汇总。第二个示例展示了 ValueFromPipelineByPropertyName 参数绑定的威力——当管道对象的属性名与函数参数名一致时,PowerShell 会自动完成映射,无需手动提取属性再传参。这种设计让函数之间的组合变得自然流畅。

注意事项

  1. 避免在循环中使用 += 累加数组。每次 += 都会创建新数组并复制所有已有元素,数据量大时性能呈指数级恶化。应改用 List<T>Add 方法或直接使用管道收集结果。

  2. 大文件处理务必使用流式读取Get-Content 默认会一次性读取全部内容,加上 -ReadCount 0 参数后改为逐行输出。对于超大文件(GB 级别),还可以考虑使用 [System.IO.StreamReader] 进行更底层的流式读取。

  3. 理解管道中的”阻塞”操作Sort-ObjectGroup-ObjectMeasure-Object 等命令必须等待所有输入才能产出结果,它们会打破流式特性。在不需要全局排序或分组的场景中,应尽量将这类命令放在管道末端或避免使用。

  4. 自定义管道函数必须包含 process。如果函数只有 beginend 块而没有 process 块,管道传入的对象会被忽略。这是初学者编写管道函数时最常见的错误之一。

  5. 注意管道中的类型转换开销。当管道中的对象需要在不同类型间转换时(例如字符串转数字),会在每次处理时产生额外的解析成本。对于高频操作,建议在进入管道前统一完成类型转换。

  6. 权衡可读性与性能。管道写法天然具有良好的可读性和可组合性,在大多数场景下其性能已经足够。不要为了微小的性能提升而牺牲代码的可维护性——只有在经过实际测量确认存在瓶颈时,才需要切换到 List<T> + foreach 等更底层的方式。

PowerShell 技能连载 - 性能分析器

适用于 PowerShell 5.1 及以上版本

背景引入

脚本跑得慢,是每个 PowerShell 用户迟早会遇到的问题。也许是一段处理 CSV 的管道耗时过长,也许是一个循环遍历数千台远程计算机的报表任务跑了一整天。面对性能瓶颈,靠直觉猜测往往事倍功半——真正拖慢脚本的”罪犯”可能藏在你想不到的地方。

PowerShell 本身提供了多种轻量级的计时和度量工具,从简单的 Measure-Command 到功能更丰富的 Measure-Object,再到手动插入时间戳实现分段计时。善用这些工具,可以在不引入第三方模块的前提下,快速定位脚本中最耗时的代码段。

本文将从易到难介绍三种性能分析方法:单段代码计时、多段代码对比基准测试,以及完整的脚本分段性能分析器。掌握这些技巧后,你可以像专业开发者一样系统性地优化 PowerShell 脚本的执行效率。

方法一:使用 Measure-Command 快速计时

Measure-Command 是 PowerShell 内置的计时命令行工具,适合对单段代码进行快速计时。它接受一个脚本块作为参数,执行该脚本块并返回执行耗时。与手动记录 Get-Date 相比,Measure-Command 的精度更高,使用也更简洁。

下面的例子展示了如何用 Measure-Command 分别测量数组创建和哈希表查找的耗时。我们将每次测试重复多轮并取平均值,以减少偶然波动带来的干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 定义测试参数
$iterations = 1000
$arrayTimes = @()
$hashtableTimes = @()

for ($i = 1; $i -le $iterations; $i++) {
# 测量数组追加操作
$arrayTime = Measure-Command {
$arr = @()
foreach ($j in 1..500) {
$arr += $j
}
}
$arrayTimes += $arrayTime.TotalMilliseconds

# 测量哈希表查找操作
$ht = @{}
foreach ($j in 1..500) {
$ht[$j] = "value_$j"
}
$htTime = Measure-Command {
$null = $ht[250]
}
$hashtableTimes += $htTime.TotalMilliseconds
}

# 计算平均耗时
$avgArray = ($arrayTimes | Measure-Object -Average).Average
$avgHash = ($hashtableTimes | Measure-Object -Average).Average

Write-Host ("数组追加 平均耗时: {0:N2} ms" -f $avgArray)
Write-Host ("哈希表查找 平均耗时: {0:N4} ms" -f $avgHash)

执行结果示例:

1
2
数组追加 平均耗时: 8.73 ms
哈希表查找 平均耗时: 0.0012 ms

从结果可以看出,数组使用 += 运算符追加元素时,每次操作都会重建整个数组,导致性能明显低于哈希表的键值查找。这个发现为我们选择数据结构提供了数据支撑。

方法二:构建可复用的基准测试框架

当需要对比同一任务的多种实现方式时,逐一编写 Measure-Command 会显得繁琐。我们可以封装一个轻量的基准测试函数,一次传入多个候选实现,自动完成多轮迭代、结果汇总和排名。

下面这个 Invoke-Benchmark 函数接受一个哈希表,键为实现的名称,值为对应的脚本块。函数会对每个实现执行指定次数的迭代,并按平均耗时从快到慢排序输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function Invoke-Benchmark {
param(
[hashtable]$Implementations,
[int]$Iterations = 100
)

$results = @()

foreach ($name in $Implementations.Keys) {
$scriptBlock = $Implementations[$name]
$timings = @()

for ($i = 1; $i -le $Iterations; $i++) {
$elapsed = Measure-Command -Expression $scriptBlock
$timings += $elapsed.TotalMilliseconds
}

$stats = $timings | Measure-Object -Average -Minimum -Maximum

$results += [PSCustomObject]@{
Name = $name
AvgMs = [math]::Round($stats.Average, 4)
MinMs = [math]::Round($stats.Minimum, 4)
MaxMs = [math]::Round($stats.Maximum, 4)
Runs = $Iterations
}
}

# 按平均耗时排序并输出
$results | Sort-Object AvgMs | Format-Table -AutoSize
}

# 定义三种字符串拼接方式的候选实现
$impls = @{
'加号拼接' = {
$s = ''
foreach ($i in 1..200) {
$s = $s + "item_$i"
}
}
'格式化拼接' = {
$s = ''
foreach ($i in 1..200) {
$s = '{0}{1}' -f $s, "item_$i"
}
}
'StringBuilder' = {
$sb = [System.Text.StringBuilder]::new()
foreach ($i in 1..200) {
$null = $sb.Append("item_$i")
}
$s = $sb.ToString()
}
}

Invoke-Benchmark -Implementations $impls -Iterations 50

执行结果示例:

1
2
3
4
5
Name          AvgMs    MinMs    MaxMs Runs
---- ----- ----- ----- ----
StringBuilder 0.0812 0.0450 0.2100 50
加号拼接 0.3256 0.2801 0.5130 50
格式化拼接 0.4103 0.3502 0.8901 50

结果清晰地表明,StringBuilder 在循环拼接场景下具有明显优势。加号拼接每次都创建新字符串,格式化运算符也有类似开销,而 StringBuilder 在内部维护可变缓冲区,避免了重复分配内存。

方法三:脚本分段性能分析器

在实际排障场景中,我们需要知道一个较长脚本中各段代码分别占用了多少时间。下面实现一个轻量的分析器,通过在脚本关键位置插入”标签”来记录各段的起止时间,最终输出每段的耗时统计和占比。

Start-ProfileBlock 函数在调用时记录一个标签和时间戳,Stop-ProfileSession 则汇总所有标签并计算各段耗时。这个方案无需修改脚本的核心逻辑,只在关键位置添加一行调用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 性能分析器的全局存储
$script:ProfileMarks = [System.Collections.Generic.List[PSObject]]::new()

function Start-ProfileBlock {
param([string]$Label)
$script:ProfileMarks.Add(
[PSCustomObject]@{
Label = $Label
Time = Get-Date
}
)
}

function Stop-ProfileSession {
$totalMarks = $script:ProfileMarks.Count
if ($totalMarks -lt 2) {
Write-Warning "至少需要两个标记点才能计算耗时。"
return
}

$segments = @()
for ($i = 1; $i -lt $totalMarks; $i++) {
$prev = $script:ProfileMarks[$i - 1]
$curr = $script:ProfileMarks[$i]
$duration = ($curr.Time - $prev.Time).TotalMilliseconds
$segments += [PSCustomObject]@{
Segment = "$($prev.Label) -> $($curr.Label)"
Label = $prev.Label
Duration = [math]::Round($duration, 2)
}
}

$totalDuration = ($segments | Measure-Object -Property Duration -Sum).Sum

foreach ($seg in $segments) {
$seg | Add-Member -NotePropertyName 'Percent' `
-NotePropertyValue ([math]::Round($seg.Duration / $totalDuration * 100, 1)) `
-PassThru
}

$segments | Sort-Object Duration -Descending | Format-Table -AutoSize
Write-Host ("总耗时: {0:N2} ms" -f $totalDuration)

# 清理标记以便复用
$script:ProfileMarks.Clear()
}

# ---- 模拟被分析的脚本 ----

Start-ProfileBlock -Label "开始"

# 第一段:读取 CSV 数据
$data = @(1..5000 | ForEach-Object {
[PSCustomObject]@{
Id = $_
Name = "User_$_"
Score = Get-Random -Minimum 0 -Maximum 100
Active = [bool](Get-Random -Minimum 0 -Maximum 2)
}
})
Start-ProfileBlock -Label "数据生成"

# 第二段:过滤和排序
$filtered = @()
foreach ($item in $data) {
if ($item.Active -and $item.Score -ge 50) {
$filtered += $item
}
}
$sorted = $filtered | Sort-Object Score -Descending
Start-ProfileBlock -Label "过滤排序"

# 第三段:输出报表
$report = $sorted | Select-Object -First 20 | Format-Table -AutoSize | Out-String
Write-Host $report
Start-ProfileBlock -Label "报表输出"

# 输出性能分析报告
Stop-ProfileSession

执行结果示例:

1
2
3
4
5
6
7
Segment                      Label      Duration Percent
------- ----- -------- -------
过滤排序 -> 报表输出 过滤排序 182.45 67.3
数据生成 -> 过滤排序 数据生成 72.10 26.6
开始 -> 数据生成 开始 16.30 6.0

总耗时: 270.85 ms

分析结果一目了然:过滤排序阶段占用了将近七成的时间。罪魁祸首是数组使用 += 在循环中不断重建。如果改用 [System.Collections.Generic.List[PSObject]]Add 方法,该阶段的耗时会大幅缩短。这就是分段分析器的价值——它帮你把有限的优化精力集中到真正需要改进的地方。

注意事项

  1. Measure-Command 会吞掉输出Measure-Command 执行脚本块时会丢弃所有输出流,包括 Write-Host。如果你需要在计时的同时观察输出,请使用手动记录 Get-Date 的方式,或者在脚本块内将结果写入文件。

  2. 首次运行存在冷启动偏差:PowerShell 的命令解析、程序集加载和 JIT 编译在首次执行时会产生额外开销。做基准测试时建议先跑一轮”预热”,再正式计时。这也是 Invoke-Benchmark 函数默认执行多次迭代的原因之一。

  3. 避免在循环中使用数组 += 操作:如上文示例所示,$array += $item 每次都会创建新数组并复制所有元素。对于大规模数据,应改用 List<T>Add 方法,或预分配好数组大小后按索引赋值。

  4. 注意管道开销:PowerShell 管道每传递一个对象都会经过一系列的处理步骤(如 BeginProcessProcessRecord),对于简单的循环操作,使用 foreach 语句(而非 ForEach-Object)可以避免管道开销,显著提升性能。

  5. 分析器的时钟精度Get-Date 在 Windows 上的理论精度约为 15.6 毫秒(系统时钟分辨率),对于耗时极短的操作,建议用 Measure-Command[System.Diagnostics.Stopwatch] 来获得更高精度。Stopwatch 使用高分辨率硬件计时器,精度可达纳秒级。

  6. 生产环境中的性能分析:在实际生产脚本中嵌入性能分析代码时,建议通过开关参数(如 $DebugProfile)控制是否启用,避免在正常运行时产生不必要的性能损耗和日志文件。同时,分析结果可以导出为 CSV 或 JSON,便于后续对比和趋势分析。

PowerShell 技能连载 - 内存管理与性能分析

适用于 PowerShell 5.1 及以上版本

PowerShell 脚本在处理大数据集、长时间运行的任务或复杂的对象管道时,内存消耗会快速增长。对象在管道中逐个传递,每个中间步骤都会产生新的 .NET 对象,而 .NET 的垃圾回收器(GC)并不总是能及时回收。理解 PowerShell 的内存模型和 GC 机制,对于编写高性能脚本至关重要。

本文将介绍内存诊断方法、GC 调优策略,以及减少内存占用的编码技巧。

内存诊断与监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 当前进程内存信息
$proc = Get-Process -Id $PID
Write-Host "PowerShell 进程内存分析" -ForegroundColor Cyan
Write-Host " 工作集 (Working Set): $([math]::Round($proc.WorkingSet64 / 1MB, 2)) MB"
Write-Host " 专用字节 (Private Bytes): $([math]::Round($proc.PrivateMemorySize64 / 1MB, 2)) MB"
Write-Host " 虚拟内存: $([math]::Round($proc.VirtualMemorySize64 / 1MB, 2)) MB"
Write-Host " 句柄数: $($proc.HandleCount)"
Write-Host " 线程数: $($proc.Threads.Count)"

# .NET CLR 内存统计
$heap = [GC]::GetTotalMemory($false)
Write-Host "`n.NET 堆内存:$([math]::Round($heap / 1MB, 2)) MB" -ForegroundColor Yellow

# GC 代数统计
Write-Host "`nGC 代数回收次数:" -ForegroundColor Cyan
Write-Host " Gen 0: $([GC]::CollectionCount(0))"
Write-Host " Gen 1: $([GC]::CollectionCount(1))"
Write-Host " Gen 2: $([GC]::CollectionCount(2))"

# 内存消耗函数
function Measure-ScriptMemory {
param([scriptblock]$ScriptBlock, [string]$Label = "Test")

[GC]::Collect()
[GC]::WaitForPendingFinalizers()
$before = [GC]::GetTotalMemory($true)

$sw = [System.Diagnostics.Stopwatch]::StartNew()
& $ScriptBlock
$sw.Stop()

$after = [GC]::GetTotalMemory($false)
$diff = $after - $before

[PSCustomObject]@{
Label = $Label
BeforeMB = [math]::Round($before / 1MB, 2)
AfterMB = [math]::Round($after / 1MB, 2)
DeltaMB = [math]::Round($diff / 1MB, 2)
TimeMs = $sw.ElapsedMilliseconds
Gen0 = [GC]::CollectionCount(0)
Gen1 = [GC]::CollectionCount(1)
Gen2 = [GC]::CollectionCount(2)
}
}

# 对比不同方式的内存消耗
$result1 = Measure-ScriptMemory -Label "数组累加" -ScriptBlock {
$arr = @()
foreach ($i in 1..10000) { $arr += $i }
}

$result2 = Measure-ScriptMemory -Label "List 泛型" -ScriptBlock {
$list = [System.Collections.Generic.List[int]]::new()
foreach ($i in 1..10000) { $list.Add($i) }
}

$result3 = Measure-ScriptMemory -Label "赋值变量" -ScriptBlock {
$result = foreach ($i in 1..10000) { $i }
}

"数组累加", "List 泛型", "赋值变量" | ForEach-Object {
switch ($_) {
"数组累加" { $result1 }
"List 泛型" { $result2 }
"赋值变量" { $result3 }
}
} | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PowerShell 进程内存分析
工作集 (Working Set): 156.78 MB
专用字节 (Private Bytes): 142.34 MB
虚拟内存: 1024.56 MB
句柄数: 523
线程数: 12

.NET 堆内存:34.56 MB

GC 代数回收次数:
Gen 0: 15
Gen 1: 3
Gen 2: 1

Label BeforeMB AfterMB DeltaMB TimeMs Gen0 Gen1 Gen2
----- -------- ------- ------- ------ ---- ---- ----
数组累加 34.56 42.31 7.75 234 18 3 1
List 泛型 42.31 42.34 0.03 12 18 3 1
赋值变量 42.34 42.35 0.01 8 18 3 1

垃圾回收与内存释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# 大对象堆(LOH)分析
# 超过 85,000 字节的对象存储在 LOH 中,LOH 的回收代价很高
function Get-LargeObjectStats {
# 使用 GC 内存信息
$totalMemory = [GC]::GetTotalMemory($false)
$heapSize = [System.AppDomain]::CurrentDomain.MonitoringTotalAllocatedMemorySize

Write-Host "总分配内存:$([math]::Round($heapSize / 1MB, 2)) MB" -ForegroundColor Cyan
Write-Host "当前存活内存:$([math]::Round($totalMemory / 1MB, 2)) MB" -ForegroundColor Yellow

# 强制 GC 回收
Write-Host "`n执行完整 GC 回收..." -ForegroundColor Yellow
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
[GC]::Collect() # 二次回收确保 Finalizer 队列的对象被回收

$afterGC = [GC]::GetTotalMemory($true)
$freed = $totalMemory - $afterGC
Write-Host "回收后内存:$([math]::Round($afterGC / 1MB, 2)) MB(释放 $([math]::Round($freed / 1MB, 2)) MB)" -ForegroundColor Green
}

Get-LargeObjectStats

# 处理大文件时的内存优化
function Get-LargeFileHashBatch {
param(
[string]$Path,
[int]$BatchSize = 100
)

$files = [System.IO.Directory]::EnumerateFiles($Path, "*", [System.IO.SearchOption]::AllDirectories)
$batch = @()
$totalProcessed = 0

foreach ($file in $files) {
$info = [System.IO.FileInfo]::new($file)
$hash = (Get-FileHash $file -Algorithm SHA256).Hash

$batch += [PSCustomObject]@{
File = $info.Name
SizeKB = [math]::Round($info.Length / 1KB, 2)
Hash = $hash.Substring(0, 16) + "..."
}

$totalProcessed++

if ($batch.Count -ge $BatchSize) {
# 输出批次结果并释放引用
$batch
$batch = @()

if ($totalProcessed % 500 -eq 0) {
[GC]::Collect()
$mem = [math]::Round([GC]::GetTotalMemory($false) / 1MB, 2)
Write-Host " 已处理 $totalProcessed 个文件,内存:$mem MB" -ForegroundColor DarkGray
}
}
}

# 输出最后一批
if ($batch) { $batch }
Write-Host "`n总计处理:$totalProcessed 个文件" -ForegroundColor Green
}

# 使用流式处理替代一次性加载
function Read-LargeCsvStreaming {
param(
[string]$Path,
[int]$TopN = 10
)

$reader = [System.IO.StreamReader]::new($Path)
$header = $reader.ReadLine()
$columns = $header -split ','

$count = 0
while ($null -ne ($line = $reader.ReadLine()) -and $count -lt $TopN) {
$values = $line -split ','
$obj = [ordered]@{}
for ($i = 0; $i -lt $columns.Count; $i++) {
$obj[$columns[$i].Trim('"')] = $values[$i].Trim('"')
}
[PSCustomObject]$obj
$count++
}

$reader.Close()
$reader.Dispose()
}

Write-Host "`n流式处理避免了将整个文件加载到内存" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
7
8
9
总分配内存:256.78 MB
当前存活内存:42.31 MB

执行完整 GC 回收...
回收后内存:18.45 MB(释放 23.86 MB)
已处理 500 个文件,内存:24.56 MB
已处理 1000 个文件,内存:22.34 MB
总计处理:1234 个文件
流式处理避免了将整个文件加载到内存

性能分析实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 脚本性能分析器
function Measure-ScriptPerformance {
param(
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,

[int]$Iterations = 3
)

$results = @()

# 预热(排除 JIT 编译影响)
Write-Host "预热中..." -ForegroundColor DarkGray
& $ScriptBlock | Out-Null

for ($i = 1; $i -le $Iterations; $i++) {
[GC]::Collect()
[GC]::WaitForPendingFinalizers()

$memBefore = [GC]::GetTotalMemory($true)
$sw = [System.Diagnostics.Stopwatch]::StartNew()

& $ScriptBlock | Out-Null

$sw.Stop()
$memAfter = [GC]::GetTotalMemory($false)

$results += [PSCustomObject]@{
Iteration = $i
TimeMs = $sw.ElapsedMilliseconds
MemoryKB = [math]::Round(($memAfter - $memBefore) / 1KB, 2)
Gen0 = [GC]::CollectionCount(0)
}

Write-Host " 第 $i 次:$($sw.ElapsedMilliseconds) ms" -ForegroundColor DarkGray
}

$avg = ($results | Measure-Object TimeMs -Average).Average
$min = ($results | Measure-Object TimeMs -Minimum).Minimum
$max = ($results | Measure-Object TimeMs -Maximum).Maximum

Write-Host "`n性能统计:" -ForegroundColor Cyan
Write-Host " 平均:$([math]::Round($avg, 2)) ms"
Write-Host " 最快:$min ms"
Write-Host " 最慢:$max ms"

return $results
}

# 对比字符串拼接方式
Write-Host "=== 字符串拼接对比 ===" -ForegroundColor Yellow

Measure-ScriptPerformance -Iterations 3 -ScriptBlock {
$s = ""
foreach ($i in 1..1000) { $s += "Line $i`n" }
} | Out-Null

Measure-ScriptPerformance -Iterations 3 -ScriptBlock {
$sb = [System.Text.StringBuilder]::new()
foreach ($i in 1..1000) { $sb.AppendLine("Line $i") | Out-Null }
$sb.ToString()
} | Out-Null

执行结果示例:

1
2
3
4
5
6
7
8
9
预热中...
1 次:45 ms
第 2 次:42 ms
第 3 次:40 ms

性能统计:
平均:42.33 ms
最快:40 ms
最慢:45 ms

注意事项

  1. 数组 += 陷阱:PowerShell 中 $arr += $item 每次都创建新数组,复杂度 O(n^2),大数据集必须用 List 泛型
  2. GC 非万能:显式调用 [GC]::Collect() 有其代价,不应过度使用,主要依赖 GC 的自动回收
  3. 大对象处理:超过 85KB 的对象进入 LOH,LOH 的碎片化是内存泄漏的常见原因
  4. 流式处理:处理大文件时使用 StreamReader/EnumerateFiles 代替 Get-Content/Get-ChildItem,避免一次性加载
  5. 对象释放:使用 IDisposable 对象(如 Stream、Connection)后务必调用 Dispose() 或使用 using 语句
  6. 32 位限制:Windows PowerShell 5.1(32 位)进程有 2GB 内存上限,处理大数据时应使用 64 位 PowerShell

PowerShell 技能连载 - 并行处理实战

适用于 PowerShell 7.0 及以上版本(部分示例兼容 5.1)

随着服务器核心数增加和运维任务量增长,单线程处理已经无法满足效率需求。PowerShell 7 引入了 ForEach-Object -Parallel,让并行处理变得像管道一样简单。对于仍在使用 PowerShell 5.1 的环境,.NET 的 Runspace API 同样提供了强大的并行能力。合理使用并行处理,可以将原本需要数小时的任务缩短到几分钟。

本文将对比不同的并行方案,并给出实用的并行处理模式。

ForEach-Object -Parallel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 基本并行用法(PowerShell 7+)
$servers = @("SRV01", "SRV02", "SRV03", "SRV04", "SRV05")

# 串行获取磁盘信息
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$serial = foreach ($server in $servers) {
Invoke-Command -ComputerName $server -ScriptBlock {
Get-PSDrive -PSProvider FileSystem | Select-Object Name,
@{N='FreeGB'; E={[math]::Round($_.Free / 1GB, 2)}},
@{N='UsedGB'; E={[math]::Round($_.Used / 1GB, 2)}}
}
}
$sw.Stop()
Write-Host "串行执行:$($sw.ElapsedMilliseconds) ms"

# 并行获取磁盘信息
$sw.Restart()
$parallel = $servers | ForEach-Object -ThrottleLimit 5 -Parallel {
Invoke-Command -ComputerName $_ -ScriptBlock {
Get-PSDrive -PSProvider FileSystem | Select-Object Name,
@{N='FreeGB'; E={[math]::Round($_.Free / 1GB, 2)}},
@{N='UsedGB'; E={[math]::Round($_.Used / 1GB, 2)}}
}
}
$sw.Stop()
Write-Host "并行执行:$($sw.ElapsedMilliseconds) ms"

# 带返回值的并行处理
$results = 1..100 | ForEach-Object -ThrottleLimit 10 -Parallel {
# 模拟耗时计算
Start-Sleep -Milliseconds 50
[PSCustomObject]@{
Input = $_
Squared = $_ * $_
Thread = [System.Threading.Thread]::CurrentThread.ManagedThreadId
}
}

$uniqueThreads = $results.Thread | Sort-Object -Unique
Write-Host "使用了 $($uniqueThreads.Count) 个线程处理 100 个任务"
$results | Select-Object -First 5 | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
串行执行:12534 ms
并行执行:3102 ms
使用了 6 个线程处理 100 个任务
Input Squared Thread
----- ------- ------
1 1 7
2 4 8
3 9 4
4 16 7
5 25 8

变量传递与状态共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 向并行代码块传递变量
$threshold = 80
$warningEmail = "admin@contoso.com"

$drives = @(
@{ Server = "SRV01"; Drive = "C:" },
@{ Server = "SRV02"; Drive = "D:" },
@{ Server = "SRV03"; Drive = "C:" }
)

$alerts = $drives | ForEach-Object -ThrottleLimit 3 -Parallel {
# 使用 $using: 引用外部变量
$thresh = $using:threshold
$email = $using:warningEmail

# 模拟获取磁盘使用率
$usage = Get-Random -Minimum 50 -Maximum 100

if ($usage -gt $thresh) {
[PSCustomObject]@{
Server = $_.Server
Drive = $_.Drive
Usage = $usage
Alert = "$($_.Server) $($_.Drive) 使用率 ${usage}% 超过阈值 ${thresh}%"
NotifyTo = $email
}
}
}

if ($alerts) {
Write-Host "磁盘告警:" -ForegroundColor Red
$alerts | Format-Table Server, Drive, Usage, Alert -AutoSize
} else {
Write-Host "所有磁盘使用率正常" -ForegroundColor Green
}

# 使用 ConcurrentDictionary 共享状态(线程安全)
$counter = [System.Collections.Concurrent.ConcurrentDictionary[string, int]]::new()

1..50 | ForEach-Object -ThrottleLimit 10 -Parallel {
$dict = $using:counter
$category = @("Web", "API", "DB", "Cache")[(Get-Random -Minimum 0 -Maximum 4)]
$dict.AddOrUpdate($category, 1, { param($k, $v) $v + 1 })
}

Write-Host "`n分类统计:" -ForegroundColor Cyan
$counter.GetEnumerator() | Sort-Object Value -Descending |
ForEach-Object { Write-Host " $($_.Key): $($_.Value)" }

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
磁盘告警:
Server Drive Usage Alert
------ ----- ----- -----
SRV01 C: 92 SRV01 C: 使用率 92% 超过阈值 80%
SRV03 C: 87 SRV03 C: 使用率 87% 超过阈值 80%

分类统计:
Web: 16
API: 14
DB: 11
Cache: 9

Runspace 池(兼容 5.1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# PowerShell 5.1 兼容的并行方案
function Invoke-RunspaceJob {
param(
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock,

[Parameter(Mandatory)]
[object[]]$InputData,

[int]$ThrottleLimit = 4
)

$pool = [runspacefactory]::CreateRunspacePool(1, $ThrottleLimit)
$pool.Open()

$jobs = [System.Collections.Generic.List[hashtable]]::new()

foreach ($item in $InputData) {
$ps = [powershell]::Create()
$ps.RunspacePool = $pool

# 添加脚本和参数
$ps.AddScript($ScriptBlock).AddArgument($item) | Out-Null

$jobs.Add(@{
PowerShell = $ps
Handle = $ps.BeginInvoke()
Input = $item
})
}

# 等待所有任务完成
$results = @()
foreach ($job in $jobs) {
try {
$output = $job.PowerShell.EndInvoke($job.Handle)
if ($output) { $results += $output }
} catch {
Write-Host "任务失败:$($job.Input) - $($_.Exception.Message)" -ForegroundColor Red
} finally {
$job.PowerShell.Dispose()
}
}

$pool.Close()
$pool.Dispose()

return $results
}

# 使用 Runspace 并行 Ping 测试
$computers = @(
"SRV01", "SRV02", "SRV03", "SRV04", "SRV05",
"DB01", "DB02", "WEB01", "WEB02", "APP01"
)

$pingResults = Invoke-RunspaceJob -InputData $computers -ThrottleLimit 5 -ScriptBlock {
param([string]$Computer)

$sw = [System.Diagnostics.Stopwatch]::StartNew()
$ping = Test-Connection -ComputerName $Computer -Count 1 -Quiet -ErrorAction SilentlyContinue
$sw.Stop()

[PSCustomObject]@{
Computer = $Computer
Online = $ping
LatencyMs = $sw.ElapsedMilliseconds
}
}

$pingResults | Sort-Object Computer | Format-Table -AutoSize

$online = ($pingResults | Where-Object { $_.Online }).Count
Write-Host "在线:$online / $($computers.Count)" -ForegroundColor Green

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
Computer Online LatencyMs
-------- ------ ---------
APP01 True 2
DB01 True 3
DB02 True 2
SRV01 True 1
SRV02 True 2
SRV03 True 1
SRV04 True 2
SRV05 True 2
WEB01 True 3
WEB02 True 2
在线:10 / 10

并行文件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 并行处理大量文件
function Invoke-ParallelFileProcess {
param(
[Parameter(Mandatory)]
[string]$Path,

[Parameter(Mandatory)]
[scriptblock]$ProcessBlock,

[string]$Filter = "*.*",
[int]$ThrottleLimit = 4
)

$files = Get-ChildItem $Path -Filter $Filter -File
Write-Host "找到 $($files.Count) 个文件,使用 $ThrottleLimit 并发处理" -ForegroundColor Cyan

$sw = [System.Diagnostics.Stopwatch]::StartNew()

$results = $files | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
$file = $_
$block = $using:ProcessBlock

try {
& $block $file
} catch {
[PSCustomObject]@{
File = $file.Name
Error = $_.Exception.Message
Status = "Failed"
}
}
}

$sw.Stop()

$success = ($results | Where-Object { $_.Status -ne "Failed" }).Count
$failed = ($results | Where-Object { $_.Status -eq "Failed" }).Count

Write-Host "`n处理完成:成功 $success,失败 $failed" -ForegroundColor $(if ($failed) { "Yellow" } else { "Green" })
Write-Host "耗时:$($sw.ElapsedMilliseconds) ms"

return $results
}

# 示例:批量计算文件哈希
$hashResults = Invoke-ParallelFileProcess -Path "C:\Projects\MyApp\bin" -Filter "*.dll" -ThrottleLimit 8 -ProcessBlock {
param($file)
$hash = (Get-FileHash $file.FullName -Algorithm SHA256).Hash
[PSCustomObject]@{
File = $file.Name
SizeKB = [math]::Round($file.Length / 1KB, 2)
SHA256 = $hash.Substring(0, 16) + "..."
Status = "OK"
}
}

$hashResults | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
找到 24 个文件,使用 8 并发处理

处理完成:成功 24,失败 0
耗时:1234 ms
File SizeKB SHA256 Status
---- ------ ------ ------
Microsoft.AI.dll 456.78 a1b2c3d4e5f6... OK
Newtonsoft.Json 1234.56 f1e2d3c4b5a6... OK
System.Text.Json 234.56 1a2b3c4d5e6f... OK
MyApp.Core.dll 567.89 b2c3d4e5f6a1... OK

注意事项

  1. 并发数控制ThrottleLimit 不是越大越好,CPU 密集型任务设为 CPU 核心数,I/O 密集型可设为 2-4 倍
  2. 线程安全:并行代码中不要直接修改外部变量,使用 $using: 传递只读数据,或使用 ConcurrentDictionary 共享可变状态
  3. 错误处理:每个并行任务应有独立的 try-catch,避免一个任务失败导致整个并行操作中断
  4. 内存消耗:每个 runspace 有独立的内存空间,大量并发会消耗更多内存
  5. 调试困难:并行代码的调试比串行代码复杂得多,建议先用串行模式验证逻辑正确性
  6. PowerShell 7 优势ForEach-Object -Parallel 是 PowerShell 7 独有功能,5.1 只能使用 Runspace 方案

PowerShell 技能连载 - 管道性能优化

适用于 PowerShell 5.1 及以上版本

PowerShell 的管道是其核心特性之一,它让命令之间的数据流转变得优雅直观。但在处理大量数据时,管道的逐对象传递机制会成为性能瓶颈——每个对象都需要经过完整的管道流程,产生额外的内存分配和方法调用开销。当处理数万甚至百万级对象时,这些微小的开销会被放大为显著的延迟。

本文将分析管道性能瓶颈的根源,并介绍多种优化策略。

性能测量与对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 测量不同方式的性能差异
$items = 1..100000

# 方式 1:管道 + ForEach-Object
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$result1 = $items | ForEach-Object { $_ * 2 }
$sw.Stop()
Write-Host "管道 ForEach-Object:$($sw.ElapsedMilliseconds) ms,结果数:$($result1.Count)"

# 方式 2:赋值变量 + foreach 语句
$sw.Restart()
$result2 = foreach ($item in $items) { $item * 2 }
$sw.Stop()
Write-Host "foreach 语句:$($sw.ElapsedMilliseconds) ms,结果数:$($result2.Count)"

# 方式 3:LINQ(PowerShell 7+)
$sw.Restart()
$result3 = [System.Linq.Enumerable]::Select(
[int[]]$items, [Func[int, int]]{ param($x) $x * 2 }
)
$sw.Stop()
Write-Host "LINQ Select:$($sw.ElapsedMilliseconds) ms,结果数:$($result3.Count)"

# 方式 4:List 累积器
$sw.Restart()
$list = [System.Collections.Generic.List[int]]::new($items.Count)
foreach ($item in $items) { $list.Add($item * 2) }
$sw.Stop()
Write-Host "List 累积器:$($sw.ElapsedMilliseconds) ms,结果数:$($list.Count)"

执行结果示例:

1
2
3
4
管道 ForEach-Object3456 ms,结果数:100000
foreach 语句:234 ms,结果数:100000
LINQ Select45 ms,结果数:100000
List 累积器:189 ms,结果数:100000

管道瓶颈分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# 分析 Where-Object 的性能问题
$processes = Get-Process

# 慢速:管道 + 脚本块
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$heavy1 = $processes | Where-Object { $_.WorkingSet64 -gt 50MB }
$sw.Stop()
Write-Host "管道 Where-Object:$($sw.ElapsedMilliseconds) ms"

# 快速:直接用 .NET 方法
$sw.Restart()
$heavy2 = [System.Linq.Enumerable]::Where(
[System.Diagnostics.Process[]]$processes,
[Func[System.Diagnostics.Process, bool]]{ param($p) $p.WorkingSet64 -gt 50MB }
)
$sw.Stop()
Write-Host "LINQ Where:$($sw.ElapsedMilliseconds) ms"

# 快速:foreach 语句 + 条件
$sw.Restart()
$heavy3 = foreach ($p in $processes) {
if ($p.WorkingSet64 -gt 50MB) { $p }
}
$sw.Stop()
Write-Host "foreach + 条件:$($sw.ElapsedMilliseconds) ms"

# 分析内存使用
$data = 1..50000
$before = [GC]::GetTotalMemory($true)

# 管道方式产生更多临时对象
$null = $data | ForEach-Object { $_ * 2 }
$after = [GC]::GetTotalMemory($false)
Write-Host "`n管道内存增量:$([math]::Round(($after - $before) / 1KB)) KB"

$before = [GC]::GetTotalMemory($true)
$null = foreach ($item in $data) { $item * 2 }
$after = [GC]::GetTotalMemory($false)
Write-Host "foreach 内存增量:$([math]::Round(($after - $before) / 1KB)) KB"

执行结果示例:

1
2
3
4
5
6
管道 Where-Object:89 ms
LINQ Where:12 ms
foreach + 条件:8 ms

管道内存增量:1280 KB
foreach 内存增量:320 KB

大数据集处理优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 优化文件处理:避免一次性加载全部内容
function Get-LargeFileStats {
param([string]$Path, [int]$SampleRate = 1)

$totalCount = 0
$totalSize = 0L
$extensions = @{}

$sw = [System.Diagnostics.Stopwatch]::StartNew()

# 使用 EnumerateFiles 避免一次性加载所有 FileInfo
$files = [System.IO.Directory]::EnumerateFiles($Path, "*", [System.IO.SearchOption]::AllDirectories)

foreach ($file in $files) {
$totalCount++
if ($totalCount % $SampleRate -ne 0) { continue }

$info = [System.IO.FileInfo]::new($file)
$totalSize += $info.Length

$ext = $info.Extension
if (-not $ext) { $ext = "(无扩展名)" }
if (-not $extensions.ContainsKey($ext)) {
$extensions[$ext] = @{ Count = 0; Size = 0L }
}
$extensions[$ext].Count++
$extensions[$ext].Size += $info.Length
}

$sw.Stop()

$top = $extensions.GetEnumerator() |
Sort-Object { $_.Value.Size } -Descending |
Select-Object -First 10

Write-Host "扫描完成:$totalCount 个文件,$([math]::Round($totalSize / 1MB, 2)) MB" -ForegroundColor Green
Write-Host "耗时:$($sw.ElapsedMilliseconds) ms" -ForegroundColor Cyan

foreach ($entry in $top) {
[PSCustomObject]@{
Extension = $entry.Key
Count = $entry.Value.Count
SizeMB = [math]::Round($entry.Value.Size / 1MB, 2)
}
}
}

# 全量扫描
Get-LargeFileStats -Path "C:\Projects" | Format-Table -AutoSize

# 采样扫描(每 10 个文件取 1 个,速度更快)
# Get-LargeFileStats -Path "C:\Projects" -SampleRate 10

执行结果示例:

1
2
3
4
5
6
7
8
扫描完成:45678 个文件,2345.67 MB
耗时:3456 ms
Extension Count SizeMB
--------- ----- ------
.cs 12345 890.12
.dll 2345 456.78
.json 3456 234.56
.csproj 456 12.34

批量处理与并行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# PowerShell 7 的 ForEach-Object -Parallel
$urls = @(
"https://httpbin.org/delay/1"
"https://httpbin.org/delay/1"
"https://httpbin.org/delay/1"
"https://httpbin.org/delay/1"
"https://httpbin.org/delay/1"
)

# 顺序处理
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$results1 = $urls | ForEach-Object {
$resp = Invoke-WebRequest $_ -UseBasicParsing -TimeoutSec 10
$resp.StatusCode
}
$sw.Stop()
Write-Host "顺序请求 5 个 URL:$($sw.ElapsedMilliseconds) ms"

# 并行处理(PowerShell 7+)
$sw.Restart()
$results2 = $urls | ForEach-Object -ThrottleLimit 5 -Parallel {
$resp = Invoke-WebRequest $_ -UseBasicParsing -TimeoutSec 10
$resp.StatusCode
}
$sw.Stop()
Write-Host "并行请求 5 个 URL(5 并发):$($sw.ElapsedMilliseconds) ms"

# Runspace 池(PowerShell 5.1 兼容的并行方案)
function Invoke-Parallel {
param(
[Parameter(Mandatory)][scriptblock]$ScriptBlock,
[Parameter(Mandatory)][object[]]$InputObject,
[int]$ThrottleLimit = 4
)

$runspacePool = [runspacefactory]::CreateRunspacePool(1, $ThrottleLimit)
$runspacePool.Open()

$jobs = foreach ($item in $InputObject) {
$powershell = [powershell]::Create().AddScript($ScriptBlock).AddArgument($item)
$powershell.RunspacePool = $runspacePool
@{
PowerShell = $powershell
Handle = $powershell.BeginInvoke()
}
}

$results = foreach ($job in $jobs) {
$job.PowerShell.EndInvoke($job.Handle)
$job.PowerShell.Dispose()
}

$runspacePool.Close()
$runspacePool.Dispose()
return $results
}

# 使用 Runspace 池并行处理
$numbers = 1..20
$squares = Invoke-Parallel -InputObject $numbers -ThrottleLimit 4 -ScriptBlock {
Start-Sleep -Milliseconds 100
$_ * $_
}
Write-Host "Runspace 并行计算完成:$($squares.Count) 个结果"

执行结果示例:

1
2
3
顺序请求 5 个 URL:5234 ms
并行请求 5 个 URL(5 并发):1102 ms
Runspace 并行计算完成:20 个结果

注意事项

  1. 避免过早优化:管道代码更易读易维护,只有在确实遇到性能问题时才需要优化
  2. 测量优先:使用 Measure-Command[Stopwatch] 测量后再决定优化方向
  3. 内存权衡:数组赋值 ($result = foreach {...}) 会将所有结果存入内存,大数据集注意内存压力
  4. 并行开销ForEach-Object -Parallel 有 runspace 创建开销,小任务量时可能更慢
  5. LINQ 限制:需要精确的类型转换,类型不匹配时会报错,调试成本较高
  6. GC 压力:大量临时对象会增加垃圾回收压力,适时调用 [GC]::Collect() 释放内存

PowerShell 技能连载 - 进程管理进阶

适用于 PowerShell 5.1 及以上版本(Windows)

进程管理是系统运维的基本功——排查 CPU 飙高、清理僵尸进程、监控关键服务、控制进程资源占用。虽然任务管理器能做基本的进程管理,但 PowerShell 可以实现更精细的控制:批量操作、历史趋势分析、自动告警、进程树追踪。掌握进程管理的高级技巧,是高效排查系统问题的关键。

本文将讲解进程的查询、监控、控制,以及自动化的进程管理方案。

进程查询与筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 按条件筛选进程
Write-Host "=== CPU 占用 Top 10 ===" -ForegroundColor Cyan
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 |
Select-Object Name, Id,
@{N='CPU(s)'; E={[math]::Round($_.CPU, 2)}},
@{N='内存MB'; E={[math]::Round($_.WorkingSet64 / 1MB, 1)}} |
Format-Table -AutoSize

Write-Host "`n=== 内存占用 Top 10 ===" -ForegroundColor Cyan
Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 10 |
Select-Object Name, Id,
@{N='内存MB'; E={[math]::Round($_.WorkingSet64 / 1MB, 1)}},
@{N='CPU(s)'; E={[math]::Round($_.CPU, 2)}} |
Format-Table -AutoSize

# 查找特定进程的所有实例
$nodeProcesses = Get-Process -Name "node" -ErrorAction SilentlyContinue
if ($nodeProcesses) {
Write-Host "`nNode.js 进程($($nodeProcesses.Count) 个):" -ForegroundColor Cyan
$nodeProcesses | Format-Table Id, CPU,
@{N='内存MB'; E={[math]::Round($_.WorkingSet64 / 1MB, 1)}},
StartTime -AutoSize
}

# 按用户筛选进程
function Get-ProcessByUser {
param([string]$UserName)

Get-CimInstance Win32_Process |
ForEach-Object {
$owner = Invoke-CimMethod -InputObject $_ -MethodName GetOwner -ErrorAction SilentlyContinue
if ($owner.User -eq $UserName) {
[PSCustomObject]@{
PID = $_.ProcessId
Name = $_.Name
User = $owner.User
MemoryMB = [math]::Round($_.WorkingSetSize / 1MB, 1)
CmdLine = if ($_.CommandLine.Length -gt 80) { $_.CommandLine.Substring(0, 80) + "..." } else { $_.CommandLine }
}
}
} | Sort-Object MemoryMB -Descending
}

Get-ProcessByUser -UserName "admin" | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
=== CPU 占用 Top 10 ===
Name Id CPU(s) 内存MB
---- -- ------ ------
chrome 12345 1250.3 512.5
node 12346 456.7 256.3
msedge 12347 345.2 198.7
vscode 12348 234.5 345.2
powershell 12349 123.4 85.6

=== 内存占用 Top 10 ===
Name Id 内存MB CPU(s)
---- -- ------ ------
chrome 12345 512.5 1250.3
vscode 12348 345.2 234.5
node 12346 256.3 456.7

Node.js 进程(3 个):
Id CPU 内存MB StartTime
-- --- ------ ---------
12346 456.7 256.3 2025-07-22 06:00:00
12350 12.3 64.1 2025-07-22 07:30:00
12351 5.6 32.5 2025-07-22 08:15:00

进程监控与告警

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 监控关键进程资源使用
function Watch-CriticalProcesses {
param(
[string[]]$ProcessNames = @("sqlservr", "w3wp", "MyApp"),
[double]$MemoryThresholdMB = 1024,
[double]$CPUThreshold = 80,
[int]$IntervalSeconds = 60
)

while ($true) {
$timestamp = Get-Date -Format "HH:mm:ss"
Write-Host "`n[$timestamp] 进程监控" -ForegroundColor Cyan

foreach ($name in $ProcessNames) {
$procs = Get-Process -Name $name -ErrorAction SilentlyContinue

if (-not $procs) {
Write-Host " $name : 未运行" -ForegroundColor Red
continue
}

foreach ($proc in $procs) {
$memMB = [math]::Round($proc.WorkingSet64 / 1MB, 1)
$cpuS = [math]::Round($proc.CPU, 2)

$memAlert = $memMB -gt $MemoryThresholdMB
$status = if ($memAlert) { "ALERT" } else { "OK" }
$color = if ($memAlert) { "Red" } else { "Green" }

Write-Host " $name (PID:$($proc.Id)) : 内存 ${memMB}MB CPU ${cpuS}s [$status]" -ForegroundColor $color
}
}

Start-Sleep -Seconds $IntervalSeconds
}
}

# 启动监控(Ctrl+C 停止)
# Watch-CriticalProcesses -MemoryThresholdMB 512

# 一次性检查并告警
function Test-ProcessHealth {
$alerts = @()

# 检查关键服务进程
$critical = @(
@{ Name = "Winmgmt"; DisplayName = "WMI 服务" },
@{ Name = "svchost"; DisplayName = "系统服务" },
@{ Name = "lsass"; DisplayName = "安全认证" }
)

foreach ($svc in $critical) {
$proc = Get-Process -Name $svc.Name -ErrorAction SilentlyContinue
if (-not $proc) {
$alerts += "[$($svc.DisplayName)] 进程未找到"
}
}

# 检查内存压力
$os = Get-CimInstance Win32_OperatingSystem
$memPct = [math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize * 100, 1)
if ($memPct -gt 85) {
$alerts += "系统内存使用率 ${memPct}%,超过 85% 阈值"
}

# 检查僵尸进程
$zombies = Get-Process | Where-Object { $_.Responding -eq $false }
if ($zombies) {
$alerts += "$($zombies.Count) 个无响应进程:$($zombies.Name -join ', ')"
}

if ($alerts) {
Write-Host "健康检查发现 $($alerts.Count) 个问题:" -ForegroundColor Red
$alerts | ForEach-Object { Write-Host " ! $_" -ForegroundColor Red }
} else {
Write-Host "所有进程健康" -ForegroundColor Green
}
}

Test-ProcessHealth

执行结果示例:

1
2
3
4
5
6
[08:30:15] 进程监控
sqlservr (PID:2048) : 内存 1024.5MB CPU 345.2s [OK]
w3wp (PID:3072) : 内存 512.3MB CPU 123.4s [OK]
MyApp (PID:4096) : 内存 1536.7MB CPU 567.8s [ALERT]

所有进程健康

进程树与命令行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# 获取进程树(父子关系)
function Get-ProcessTree {
param([int]$ParentPid = $PID)

$allProcesses = Get-CimInstance Win32_Process

function Build-Tree {
param([int]$Pid, [int]$Indent = 0)

$proc = $allProcesses | Where-Object { $_.ProcessId -eq $Pid }
if ($proc) {
$prefix = " " * $Indent + "+--"
$memMB = [math]::Round($proc.WorkingSetSize / 1MB, 1)
Write-Host "$prefix $($proc.Name) (PID:$Pid, ${memMB}MB)" -ForegroundColor $(if ($Indent -eq 0) { "Cyan" } else { "White" })

$children = $allProcesses | Where-Object { $_.ParentProcessId -eq $Pid }
foreach ($child in $children) {
Build-Tree -Pid $child.ProcessId -Indent ($Indent + 1)
}
}
}

Build-Tree -Pid $ParentPid
}

Write-Host "当前 PowerShell 进程树:" -ForegroundColor Cyan
Get-ProcessTree -ParentPid $PID

# 获取进程的完整命令行
function Get-ProcessCommandLine {
param([Parameter(Mandatory)][int[]]$ProcessId)

foreach ($pid in $ProcessId) {
$proc = Get-CimInstance Win32_Process -Filter "ProcessId = $pid"
if ($proc) {
[PSCustomObject]@{
PID = $proc.ProcessId
Name = $proc.Name
CmdLine = $proc.CommandLine
ExePath = $proc.ExecutablePath
}
}
}
}

# 查看所有 Web 服务器进程的命令行
$webProcs = Get-Process -Name "w3wp" -ErrorAction SilentlyContinue
if ($webProcs) {
Get-ProcessCommandLine -ProcessId $webProcs.Id |
Select-Object PID, Name, CmdLine |
Format-List
}

执行结果示例:

1
2
3
4
5
6
7
8
9
当前 PowerShell 进程树:
+-- pwsh (PID:5120, 85.6MB)
+-- node (PID:5234, 32.1MB)
+-- node (PID:5456, 16.5MB)

PID : 3072
Name : w3wp.exe
CmdLine : c:\windows\system32\inetsrv\w3wp.exe -ap "MyAppPool" -v "v4.0" ...
ExePath : c:\windows\system32\inetsrv\w3wp.exe

安全终止进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 安全的进程终止函数
function Stop-ProcessSafe {
param(
[Parameter(Mandatory)]
[string]$Name,

[int]$GracePeriodSeconds = 10,

[switch]$Force
)

$processes = Get-Process -Name $Name -ErrorAction SilentlyContinue
if (-not $processes) {
Write-Host "未找到进程:$Name" -ForegroundColor Yellow
return
}

foreach ($proc in $processes) {
Write-Host "正在停止 $($proc.Name) (PID:$($proc.Id))..." -ForegroundColor Cyan

if ($Force) {
$proc.Kill()
Write-Host " 已强制终止" -ForegroundColor Red
continue
}

# 尝试优雅关闭
try {
$proc.CloseMainWindow() | Out-Null
$proc.WaitForExit($GracePeriodSeconds * 1000)

if (-not $proc.HasExited) {
Write-Host " 优雅关闭超时,强制终止..." -ForegroundColor Yellow
$proc.Kill()
}

Write-Host " 已停止" -ForegroundColor Green
} catch {
Write-Host " 停止失败:$($_.Exception.Message)" -ForegroundColor Red
}
}
}

# 按端口查找并终止占用进程
function Stop-ProcessByPort {
param([Parameter(Mandatory)][int]$Port)

$connections = Get-NetTCPConnection -LocalPort $Port -ErrorAction SilentlyContinue
if (-not $connections) {
Write-Host "端口 $Port 未被占用" -ForegroundColor Green
return
}

$pids = $connections | Select-Object -ExpandProperty OwningProcess -Unique
foreach ($pid in $pids) {
$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
if ($proc) {
Write-Host "端口 $Port 被占用:$($proc.Name) (PID:$pid)" -ForegroundColor Yellow
Stop-Process -Id $pid -Force
Write-Host " 已终止进程" -ForegroundColor Green
}
}
}

Stop-ProcessByPort -Port 8080

执行结果示例:

1
2
3
4
5
6
正在停止 node (PID:5234)...
已停止
正在停止 node (PID:5456)...
已停止
端口 8080 被占用:node (PID:6001)
已终止进程

注意事项

  1. CPU 属性含义Process.CPU 是进程自启动以来消耗的 CPU 时间(秒),不是当前使用率。计算使用率需要两次采样
  2. 管理员权限:终止其他用户的进程或访问某些进程信息需要管理员权限
  3. 进程名匹配Get-Process -Name 不含扩展名(node 而非 node.exe),且大小写不敏感
  4. 僵尸进程Responding 属性检测进程是否有响应,但只适用于有窗口的进程
  5. WMI 查询Get-CimInstance Win32_Process 可以获取命令行参数和父进程信息,Get-Process 不能
  6. 资源释放:终止进程后,内存和网络端口可能不会立即释放,需要短暂等待

PowerShell 技能连载 - 性能优化与内存管理

适用于 PowerShell 5.1 及以上版本

PowerShell 的便利性往往以性能为代价——管道对象传递、灵活的类型转换、丰富的 .NET 集成,这些特性在处理小规模数据时非常方便,但面对大量数据(数万行 CSV、上千个文件、数百台服务器)时,性能瓶颈会非常明显。理解 PowerShell 的性能特征并掌握优化技巧,可以将脚本执行时间从数小时缩短到数秒。

本文将讲解常见的性能陷阱、优化技巧、内存管理策略,以及如何度量和对比脚本性能。

性能度量

优化之前先度量。PowerShell 提供了多种性能测量工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 使用 Measure-Command 测量代码执行时间
Measure-Command {
Get-ChildItem C:\ -Recurse -File | Where-Object { $_.Extension -eq '.log' }
} | Select-Object TotalSeconds, TotalMilliseconds

# 使用 Stopwatch 精确计时
$sw = [System.Diagnostics.Stopwatch]::StartNew()

# 你的代码
1..10000 | ForEach-Object { $_ * 2 }

$sw.Stop()
Write-Host "耗时:$($sw.Elapsed.TotalMilliseconds) 毫秒"

# 批量对比不同方案的性能
$iterations = 10000

$results = @(
@{
Name = 'ForEach-Object(管道)'
Time = (Measure-Command {
1..$iterations | ForEach-Object { $_ * 2 }
}).TotalMilliseconds
}
@{
Name = 'foreach 语句'
Time = (Measure-Command {
foreach ($i in 1..$iterations) { $i * 2 }
}).TotalMilliseconds
}
@{
Name = 'LINQ'
Time = (Measure-Command {
[System.Linq.Enumerable]::Range(1, $iterations) |
ForEach-Object { $_ * 2 }
}).TotalMilliseconds
}
)

$results | ForEach-Object {
[PSCustomObject]@{
方法 = $_.Name
耗时ms = [math]::Round($_.Time, 2)
}
} | Sort-Object 耗时ms | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
TotalSeconds TotalMilliseconds
------------ -----------------
2.34 2345.67

耗时:15.23 毫秒

方法 耗时ms
---- ------
foreach 语句 12.34
LINQ 45.67
ForEach-Object(管道) 234.56

注意foreach 语句通常比 ForEach-Object 快 10-20 倍,因为后者需要经过完整的管道处理。在性能敏感的场景中优先使用 foreach 语句。

字符串拼接优化

字符串操作是 PowerShell 中最常见的性能陷阱之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 陷阱:使用 += 拼接字符串(每次都创建新字符串)
Measure-Command {
$result = ""
1..10000 | ForEach-Object { $result += "Line $_`n" }
} | Select-Object TotalMilliseconds

# 优化一:使用 StringBuilder
Measure-Command {
$sb = [System.Text.StringBuilder]::new()
1..10000 | ForEach-Object { [void]$sb.AppendLine("Line $_") }
$result = $sb.ToString()
} | Select-Object TotalMilliseconds

# 优化二:使用数组和 -join
Measure-Command {
$lines = 1..10000 | ForEach-Object { "Line $_" }
$result = $lines -join "`n"
} | Select-Object TotalMilliseconds

# 优化三:使用赋值表达式和 -join(最快)
Measure-Command {
$result = 1..10000 | ForEach-Object { "Line $_" } | Join-String -Separator "`n"
} | Select-Object TotalMilliseconds

执行结果示例:

1
2
3
4
5
6
TotalMilliseconds
-----------------
2345.67 # +=
45.23 # StringBuilder
38.45 # 数组 + -join
32.12 # Join-String

集合操作优化

处理大量数据时,集合的选择至关重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 陷阱:数组 += 操作(每次复制整个数组)
Measure-Command {
$array = @()
1..5000 | ForEach-Object { $array += "Item-$_" }
} | Select-Object TotalMilliseconds

# 优化:使用 List
Measure-Command {
$list = [System.Collections.Generic.List[string]]::new()
1..5000 | ForEach-Object { $list.Add("Item-$_") }
$result = $list.ToArray()
} | Select-Object TotalMilliseconds

# 哈希表查找 vs 数组过滤
$data = 1..10000 | ForEach-Object { @{ Id = $_; Name = "Item-$_" } }

# 数组查找(线性扫描,O(n))
Measure-Command {
$data | Where-Object { $_.Id -eq 5000 }
} | Select-Object TotalMilliseconds

# 哈希表查找(O(1))
$lookup = @{}
$data | ForEach-Object { $lookup[$_.Id] = $_ }

Measure-Command {
$lookup[5000]
} | Select-Object TotalMilliseconds

执行结果示例:

1
2
3
4
5
6
7
8
9
10
TotalMilliseconds
-----------------
1234.56 # 数组 +=
12.34 # List<T>

# Where-Object 过滤
TotalMilliseconds: 45.67

# 哈希表查找
TotalMilliseconds: 0.12

文件处理优化

处理大量文件时,I/O 操作往往是最大的性能瓶颈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 陷阱:逐行读取大文件
Measure-Command {
$lines = @()
Get-Content "C:\Logs\large-app.log" | ForEach-Object {
if ($_ -match 'ERROR') { $lines += $_ }
}
} | Select-Object TotalMilliseconds

# 优化一:使用 Select-String
Measure-Command {
$lines = Select-String -Path "C:\Logs\large-app.log" -Pattern 'ERROR'
} | Select-Object TotalMilliseconds

# 优化二:分批读取(减少管道开销)
Measure-Command {
$lines = Get-Content "C:\Logs\large-app.log" -ReadCount 5000 |
ForEach-Object { $_ | Where-Object { $_ -match 'ERROR' } }
} | Select-Object TotalMilliseconds

# 优化三:使用 StreamReader(处理超大文件)
Measure-Command {
$reader = [System.IO.StreamReader]::new("C:\Logs\large-app.log")
$errors = while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if ($line -match 'ERROR') { $line }
}
$reader.Close()
} | Select-Object TotalMilliseconds

# CSV 处理优化
# 陷阱:Import-Csv 后再过滤
Measure-Command {
Import-Csv "C:\Data\large.csv" | Where-Object { $_.Status -eq 'Active' }
} | Select-Object TotalMilliseconds

# 优化:使用 StreamReader + 直接解析
Measure-Command {
$reader = [System.IO.StreamReader]::new("C:\Data\large.csv")
$header = $reader.ReadLine() -split ','
$active = while (-not $reader.EndOfStream) {
$values = $reader.ReadLine() -split ','
if ($values[3] -eq 'Active') {
$obj = [ordered]@{}
for ($i = 0; $i -lt $header.Count; $i++) {
$obj[$header[$i]] = $values[$i]
}
[PSCustomObject]$obj
}
}
$reader.Close()
} | Select-Object TotalMilliseconds

执行结果示例:

1
2
3
4
5
6
TotalMilliseconds
-----------------
5678.90 # 逐行 += 拼接
123.45 # Select-String
89.23 # ReadCount 分批
34.56 # StreamReader

内存管理

处理大量数据时,内存管理同样重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 监控当前 PowerShell 进程的内存使用
$proc = Get-Process -Id $PID
Write-Host "工作集:$([math]::Round($proc.WorkingSet64/1MB, 2)) MB"
Write-Host "私有内存:$([math]::Round($proc.PrivateMemorySize64/1MB, 2)) MB"

# GC 基础操作
# 查看当前 GC 内存
[System.GC]::GetTotalMemory($false) / 1MB

# 强制垃圾回收(释放未引用的对象)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
[System.GC]::Collect()

Write-Host "GC 后内存:$([math]::Round([System.GC]::GetTotalMemory($true)/1MB, 2)) MB"

# 大对象处理模式
# 对于已知大小的集合,预分配容量
$list = [System.Collections.Generic.List[PSObject]]::new(10000)

# 使用 using 语句确保资源释放(PowerShell 7+)
Measure-Command {
using ($reader = [System.IO.StreamReader]::new("C:\Logs\large-app.log")) {
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
}
}
} | Select-Object TotalMilliseconds

# 处理大文件时分块处理,避免全部加载到内存
function Process-LargeFile {
param([string]$Path, [int]$ChunkSize = 10000)

$reader = [System.IO.StreamReader]::new($Path)
$chunk = [System.Collections.Generic.List[string]]::new($ChunkSize)
$lineNum = 0

while (-not $reader.EndOfStream) {
$chunk.Add($reader.ReadLine())
$lineNum++

if ($chunk.Count -ge $ChunkSize) {
# 处理当前块
Process-Chunk -Data $chunk -LineStart ($lineNum - $ChunkSize)

# 清空块,释放内存
$chunk.Clear()
[System.GC]::Collect()
}
}

# 处理最后不满一块的数据
if ($chunk.Count -gt 0) {
Process-Chunk -Data $chunk -LineStart ($lineNum - $chunk.Count)
}

$reader.Close()
}

执行结果示例:

1
2
3
4
5
工作集:245.67 MB
私有内存:312.34 MB

GC 前内存:156.78 MB
GC 后内存:89.23 MB

管道优化

理解管道的执行方式对优化至关重要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 陷阱:多次遍历集合
Measure-Command {
$data = Get-ChildItem C:\Projects -Recurse -File
$largeFiles = $data | Where-Object { $_.Length -gt 1MB }
$recentFiles = $data | Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-7) }
$ps1Files = $data | Where-Object { $_.Extension -eq '.ps1' }
} | Select-Object TotalMilliseconds

# 优化:单次遍历,分类存储
Measure-Command {
$largeFiles = [System.Collections.Generic.List[IO.FileInfo]]::new()
$recentFiles = [System.Collections.Generic.List[IO.FileInfo]]::new()
$ps1Files = [System.Collections.Generic.List[IO.FileInfo]]::new()

foreach ($file in (Get-ChildItem C:\Projects -Recurse -File)) {
if ($file.Length -gt 1MB) { $largeFiles.Add($file) }
if ($file.LastWriteTime -gt (Get-Date).AddDays(-7)) { $recentFiles.Add($file) }
if ($file.Extension -eq '.ps1') { $ps1Files.Add($file) }
}
} | Select-Object TotalMilliseconds

# 过滤优化:尽早过滤,减少后续处理量
# 差:获取所有文件后再过滤
Get-ChildItem C:\ -Recurse -File | Where-Object { $_.Extension -eq '.log' }

# 好:在命令参数中过滤
Get-ChildItem C:\ -Recurse -Filter *.log

执行结果示例:

1
2
3
4
TotalMilliseconds
-----------------
345.67 # 多次遍历
123.45 # 单次遍历

注意事项

  1. 先度量再优化:不要凭直觉优化,使用 Measure-Command 确认瓶颈所在
  2. foreach 优于 ForEach-Object:在不需要管道流式处理时,使用 foreach 语句代替 ForEach-Object
  3. **避免数组 +=**:用 [List<T>][ArrayList] 替代,或直接赋值为数组
  4. 哈希表用于查找:需要频繁查找时,将数据构建为哈希表,避免线性扫描
  5. 流式处理:处理大量数据时,尽量逐条处理而不是全部加载到内存
  6. 合理使用 GC:不要频繁调用 [GC]::Collect(),它会暂停所有线程。仅在处理完大对象后手动触发

PowerShell 技能连载 - 并行处理与 Runspace

适用于 PowerShell 5.1 及以上版本,ForEach-Object -Parallel 需要 PowerShell 7

PowerShell 默认是单线程顺序执行的——一个命令完成后再执行下一个。当需要处理数百台服务器、上千个文件或大量 API 请求时,串行执行的等待时间会线性增长。并行处理是解决这类性能瓶颈的关键手段,PowerShell 提供了多种并行方案,从简单到复杂依次为:Start-JobForEach-Object -Parallel、Runspace 池。

本文将对比这三种方案,并深入讲解 Runspace 池的高性能用法。

三种并行方案对比

在选择并行方案前,需要了解各方案的特点和适用场景:

方案 最低版本 启动开销 内存占用 适用场景
Start-Job 5.1 高(新进程) 简单后台任务
ForEach-Object -Parallel 7.0 中(新 runspace) 快速并行遍历
Runspace 池 5.1 低(线程复用) 高性能批量操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 方案一:Start-Job(最简单,但开销最大)
$jobs = @()
$servers = @('SRV01', 'SRV02', 'SRV03', 'SRV04', 'SRV05')

foreach ($server in $servers) {
$jobs += Start-Job -ScriptBlock {
param($srv)
Test-Connection -ComputerName $srv -Count 1 -Quiet
Get-Service -ComputerName $srv -Name WinRM |
Select-Object Status, Name
} -ArgumentList $server
}

# 等待所有作业完成
$results = $jobs | Wait-Job | Receive-Job
$jobs | Remove-Job

$results | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
Status Name
------ ----
True WinRM
True WinRM
False WinRM
True WinRM
True WinRM

ForEach-Object -Parallel(PowerShell 7+)

PowerShell 7 引入了 ForEach-Object -Parallel,这是最便捷的并行方案。它在底层使用新的 runspace 来并行执行脚本块,支持控制并发数和超时:

1
2
3
4
5
6
7
8
9
10
11
# 基本用法:并行 ping 多台服务器
$servers = 1..50 | ForEach-Object { "192.168.1.$_" }

$servers | ForEach-Object -Parallel {
$result = Test-Connection -ComputerName $_ -Count 1 -Quiet
[PSCustomObject]@{
Server = $_
Online = $result
Time = Get-Date -Format 'HH:mm:ss.fff'
}
} -ThrottleLimit 10 | Sort-Object Server | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
Server       Online Time
------ ------ ----
192.168.1.1 True 08:15:32.123
192.168.1.2 True 08:15:32.156
192.168.1.3 False 08:15:32.189
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 传递外部变量到并行脚本块
$credential = Get-Credential
$logPath = "C:\Logs"

1..20 | ForEach-Object -Parallel {
# 使用 $using: 引用外部变量
$server = "SRV$_"
$session = New-PSSession -ComputerName $server -Credential $using:credential

Invoke-Command -Session $session -ScriptBlock {
Get-EventLog -LogName System -Newest 10 |
Select-Object TimeGenerated, EntryType, Message
} | Export-Csv "$using:logPath\$server-events.csv" -NoTypeInformation

Remove-PSSession $session
Write-Host "完成:$server"
} -ThrottleLimit 5 -AsJob | Wait-Job

执行结果示例:

1
2
3
4
完成:SRV1
完成:SRV2
完成:SRV3
...

注意$using: 语法用于将外部变量传递到并行脚本块中。但 $using: 只能传递可序列化的对象,不能传递活动会话或运行时对象。

Runspace 池高性能并行

Runspace 是 PowerShell 执行环境的最小单元。通过手动创建和管理 runspace,可以实现最低的启动开销和最高的吞吐量。这是处理大规模并行任务的最优方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 创建 Runspace 池
$maxThreads = 8
$runspacePool = [runspacefactory]::CreateRunspacePool(1, $maxThreads)
$runspacePool.Open()

# 定义任务列表
$servers = @('SRV01', 'SRV02', 'SRV03', 'SRV04', 'SRV05',
'SRV06', 'SRV07', 'SRV08', 'SRV09', 'SRV10')

$scriptBlock = {
param($ServerName)
$startTime = Get-Date

# 模拟远程操作(实际中替换为真实命令)
Start-Sleep -Milliseconds (Get-Random -Min 100 -Max 500)

$cpu = Get-Random -Min 10 -Max 95
$mem = Get-Random -Min 30 -Max 85

[PSCustomObject]@{
Server = $ServerName
CPU = $cpu
Memory = $mem
Status = if ($cpu -gt 80) { 'Warning' } else { 'OK' }
Duration = ((Get-Date) - $startTime).TotalMilliseconds
}
}

# 创建并启动所有 Runspace
$runspaces = @()
foreach ($server in $servers) {
$powershell = [powershell]::Create().AddScript($scriptBlock).AddArgument($server)
$powershell.RunspacePool = $runspacePool

$runspaces += [PSCustomObject]@{
Pipe = $powershell
Handle = $powershell.BeginInvoke()
Server = $server
}
}

# 收集结果
$results = @()
foreach ($rs in $runspaces) {
$result = $rs.Pipe.EndInvoke($rs.Handle)
if ($result) {
$results += $result
}
$rs.Pipe.Dispose()
}

$runspacePool.Close()
$runspacePool.Dispose()

$results | Sort-Object Server | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
Server CPU Memory Status  Duration
------ --- ------ ------ --------
SRV01 45 62 OK 312.45
SRV02 72 58 OK 287.33
SRV03 89 81 Warning 456.12
SRV04 34 45 OK 198.67
SRV05 56 71 OK 234.89
...

带 进度反馈的 Runspace

长时间运行的并行任务需要进度反馈。通过将 runspace 状态存入字典,可以实时查询进度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
$maxThreads = 4
$runspacePool = [runspacefactory]::CreateRunspacePool(1, $maxThreads)
$runspacePool.Open()

$tasks = 1..20 | ForEach-Object { "Task-$_" }
$scriptBlock = {
param($TaskName)
$totalSteps = 5
for ($step = 1; $step -le $totalSteps; $step++) {
Start-Sleep -Milliseconds (Get-Random -Min 200 -Max 600)
}
[PSCustomObject]@{
Task = $TaskName
Status = 'Completed'
Steps = $totalSteps
}
}

$runspaces = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()

foreach ($task in $tasks) {
$ps = [powershell]::Create().AddScript($scriptBlock).AddArgument($task)
$ps.RunspacePool = $runspacePool
$handle = $ps.BeginInvoke()

$runspaces[$task] = [PSCustomObject]@{
Pipe = $ps
Handle = $handle
}
}

# 等待并显示进度
$completed = 0
$total = $tasks.Count
while ($completed -lt $total) {
Start-Sleep -Milliseconds 500

foreach ($key in @($runspaces.Keys)) {
$rs = $runspaces[$key]
if ($rs.Handle.IsCompleted -and -not $rs.Done) {
$rs.Done = $true
$completed++
$pct = [math]::Round($completed / $total * 100)
Write-Progress -Activity "并行任务执行" `
-Status "$completed / $total 已完成 ($pct%)" `
-PercentComplete $pct
}
}
}

# 收集结果
$results = @()
foreach ($key in @($runspaces.Keys)) {
$rs = $runspaces[$key]
$result = $rs.Pipe.EndInvoke($rs.Handle)
if ($result) { $results += $result }
$rs.Pipe.Dispose()
}

$runspacePool.Close()
$runspacePool.Dispose()
$results | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
Task    Status    Steps
---- ------ -----
Task-1 Completed 5
Task-2 Completed 5
Task-3 Completed 5
...
Task-20 Completed 5

并行文件处理实战

以下是一个使用 Runspace 池并行处理文件的实用示例——批量计算文件哈希:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function Get-FileHashParallel {
<#
.SYNOPSIS
并行计算文件哈希值
#>
param(
[string]$Path = "C:\Projects",
[int]$ThrottleLimit = 8,
[string]$Algorithm = 'SHA256'
)

$files = Get-ChildItem -Path $Path -File -Recurse |
Where-Object { $_.Length -gt 1MB }
Write-Host "共 $($files.Count) 个文件需要计算哈希" -ForegroundColor Cyan

$pool = [runspacefactory]::CreateRunspacePool(1, $ThrottleLimit)
$pool.Open()

$script = {
param($FilePath, $Algo)
$hash = Get-FileHash -Path $FilePath -Algorithm $Algo
[PSCustomObject]@{
File = $FilePath
Hash = $hash.Hash
SizeMB = [math]::Round((Get-Item $FilePath).Length / 1MB, 2)
}
}

$runspaces = @()
$sw = [System.Diagnostics.Stopwatch]::StartNew()

foreach ($file in $files) {
$ps = [powershell]::Create().AddScript($script)
$ps.AddArgument($file.FullName).AddArgument($Algorithm) | Out-Null
$ps.RunspacePool = $pool
$runspaces += @{ Pipe = $ps; Handle = $ps.BeginInvoke() }
}

$results = @()
foreach ($rs in $runspaces) {
$result = $rs.Pipe.EndInvoke($rs.Handle)
if ($result) { $results += $result }
$rs.Pipe.Dispose()
}

$pool.Close()
$pool.Dispose()
$sw.Stop()

Write-Host "`n耗时:$($sw.Elapsed.TotalSeconds) 秒" -ForegroundColor Green
$results | Format-Table -AutoSize
}

Get-FileHashParallel -Path "C:\Projects" -ThrottleLimit 8

执行结果示例:

1
2
3
4
5
6
7
8
9
共 42 个文件需要计算哈希

耗时:3.82 秒

File Hash SizeMB
---- ---- ------
C:\Projects\app-v1.0.zip A1B2C3D4E5F6... 125.3
C:\Projects\database-backup.bak F6E5D4C3B2A1... 342.7
C:\Projects\config.json 1234567890AB... 1.2

注意事项

  1. 线程安全:Runspace 中的代码不应直接修改外部变量或共享状态,应通过返回值传递结果
  2. 并发数控制ThrottleLimit 或 Runspace 池大小不宜过大,通常设为 CPU 核心数的 2-4 倍
  3. 错误处理:Runspace 中的异常不会自动传播到主线程,需要在脚本块内捕获并通过返回值传递错误信息
  4. 资源释放:使用完毕后必须调用 Dispose() 释放 Runspace 和 PowerShell 对象,避免内存泄漏
  5. $using: 限制ForEach-Object -Parallel 中的 $using: 只能传递可序列化的值,不能传递 StreamWriter、数据库连接等运行时对象
  6. 模块导入:每个 Runspace 是独立的执行环境,需要单独导入模块。可以在脚本块开头添加 Import-Module 语句

PowerShell 技能连载 - 系统性能监控实战

适用于 PowerShell 7.0 及以上版本

在服务器运维和 DevOps 实践中,系统性能监控是保障业务稳定运行的基石。无论是排查突发的性能抖动,还是进行容量规划和趋势分析,都需要一套可靠的监控手段。传统的 GUI 工具(如任务管理器、perfmon)虽然直观,但不适合自动化场景和大规模服务器管理。

PowerShell 提供了对 WMI/CIM 类、.NET 性能计数器和系统 API 的完整访问能力,让我们可以用脚本化的方式采集、分析和导出系统性能数据。本文将介绍如何使用 PowerShell 构建一套实用的系统性能监控方案,涵盖 CPU、内存、磁盘、进程、网络等关键指标。

采集 CPU、内存和磁盘基础指标

系统监控的第一步是获取核心资源的使用情况。通过 CIM 类可以高效地采集 CPU 利用率、内存使用率和磁盘空间信息,这些是判断系统健康状态最基本的指标。

下面的函数将 CPU、内存和磁盘三项指标整合到一个对象中,方便后续统一处理和比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function Get-SystemPerformanceSnapshot {
<#
.SYNOPSIS
获取系统核心性能指标的快照
#>
$cpu = Get-CimInstance -ClassName Win32_Processor |
Measure-Object -Property LoadPercentage -Average |
Select-Object -ExpandProperty Average

$os = Get-CimInstance -ClassName Win32_OperatingSystem
$totalMemoryGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
$freeMemoryGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
$usedMemoryPct = [math]::Round(
($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) /
$os.TotalVisibleMemorySize * 100, 1
)

$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
$freeGB = [math]::Round($_.FreeSpace / 1GB, 2)
$totalGB = [math]::Round($_.Size / 1GB, 2)
$usedPct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
[PSCustomObject]@{
Drive = $_.DeviceID
TotalGB = $totalGB
FreeGB = $freeGB
UsedPct = $usedPct
}
}

[PSCustomObject]@{
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
CpuUsagePct = $cpu
TotalMemoryGB = $totalMemoryGB
FreeMemoryGB = $freeMemoryGB
MemoryUsedPct = $usedMemoryPct
Disks = $disks
}
}

Get-SystemPerformanceSnapshot

执行结果示例:

1
2
3
4
5
6
7
Timestamp     : 2025-04-22 09:15:30
CpuUsagePct : 23
TotalMemoryGB : 31.89
FreeMemoryGB : 14.52
MemoryUsedPct : 54.5
Disks : {@{Drive=C:; TotalGB=476.68; FreeGB=218.35; UsedPct=54.2},
@{Drive=D:; TotalGB=931.51; FreeGB=612.08; UsedPct=34.3}}

可以看到,一条命令就能拿到系统当前的核心资源状态。当 CPU 或内存使用率超过阈值时,运维人员可以第一时间感知并介入。

进程监控与资源排行

系统性能异常往往由个别进程引起。通过分析进程的资源占用情况,可以快速定位问题根源——是内存泄漏、CPU 密集计算,还是磁盘 I/O 瓶颈。

下面这段脚本按 CPU 和内存占用分别列出 Top N 进程,并标记出超出阈值的高消耗进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function Get-TopProcesses {
<#
.SYNOPSIS
获取资源占用最高的进程列表
.PARAMETER Top
返回的进程数量,默认 10
.PARAMETER CpuThreshold
CPU 使用率告警阈值(百分比),默认 80
.PARAMETER MemThresholdMB
内存占用告警阈值(MB),默认 500
#>
param(
[int]$Top = 10,
[double]$CpuThreshold = 80,
[double]$MemThresholdMB = 500
)

$processes = Get-Process | Where-Object { $_.Id -gt 0 } |
Select-Object Id, ProcessName,
@{N='CPU_Sec'; E={[math]::Round($_.CPU, 2)}},
@{N='Memory_MB'; E={[math]::Round($_.WorkingSet64 / 1MB, 2)}},
@{N='Threads'; E={$_.Threads.Count}},
StartTime

$byCpu = $processes | Sort-Object CPU_Sec -Descending | Select-Object -First $Top
$byMem = $processes | Sort-Object Memory_MB -Descending | Select-Object -First $Top

$alerts = $processes | Where-Object {
$_.CPU_Sec -gt $CpuThreshold -or $_.Memory_MB -gt $MemThresholdMB
}

[PSCustomObject]@{
TopByCpu = $byCpu
TopByMem = $byMem
Alerts = $alerts
}
}

$result = Get-TopProcesses -Top 5
$result.TopByCpu | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
 Id ProcessName CPU_Sec Memory_MB Threads StartTime
-- ----------- ------- --------- ------- ---------
4212 chrome 312.45 892.31 38 4/22/2025 8:30:12
3088 devenv 187.20 645.80 27 4/22/2025 8:15:05
1524 postgres 145.67 412.55 16 4/22/2025 7:00:00
892 dwm 120.33 210.40 12 4/22/2025 6:58:01
1024 powershell 98.12 156.70 8 4/22/2025 9:10:30

在日常运维中,将此脚本放入定时任务,每隔几分钟运行一次,就能持续跟踪进程资源变化趋势。当某个进程突然飙升至告警阈值之上,可以及时触发通知。

网络连接统计与异常检测

网络连接状态是排查服务可用性和安全事件的重要依据。大量 TIME_WAIT 连接可能意味着短连接风暴,异常的外连 IP 可能暗示安全风险,某个端口连接数暴涨可能表示正在遭受攻击或业务流量激增。

以下脚本通过 Get-NetTCPConnection 统计连接状态分布和端口连接数,帮助快速发现网络层面的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function Get-NetworkConnectionStats {
<#
.SYNOPSIS
统计本机 TCP 连接状态和端口分布
#>
$connections = Get-NetTCPConnection |
Where-Object { $_.State -ne 'Bound' }

# 按连接状态分组统计
$stateStats = $connections |
Group-Object State |
Sort-Object Count -Descending |
ForEach-Object {
[PSCustomObject]@{
State = $_.Name
Count = $_.Count
}
}

# 按本地监听端口分组统计
$portStats = $connections |
Where-Object { $_.LocalPort -gt 0 } |
Group-Object LocalPort |
Sort-Object Count -Descending |
Select-Object -First 10 |
ForEach-Object {
[PSCustomObject]@{
Port = $_.Name
Count = $_.Count
}
}

# 统计远端 IP 连接数(排查异常外连)
$remoteIps = $connections |
Where-Object { $_.RemoteAddress -and $_.RemoteAddress -ne '0.0.0.0' -and $_.RemoteAddress -ne '::' } |
Group-Object RemoteAddress |
Sort-Object Count -Descending |
Select-Object -First 5 |
ForEach-Object {
[PSCustomObject]@{
RemoteIP = $_.Name
Count = $_.Count
}
}

[PSCustomObject]@{
ConnectionStates = $stateStats
TopPorts = $portStats
TopRemoteIps = $remoteIps
TotalConnections = $connections.Count
}
}

$stats = Get-NetworkConnectionStats
$stats.ConnectionStates | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
State       Count
----- -----
Established 142
TimeWait 89
CloseWait 23
Listen 18
SynSent 5

当 TIME_WAIT 或 CLOSE_WAIT 连接数异常增多时,往往意味着应用层的连接管理存在问题。结合远端 IP 统计,还能识别出是否存在异常的频繁外连行为。

数据导出为 CSV 和 JSON

监控数据如果不能持久化存储,就只能在当下查看,无法做历史趋势分析。将采集到的性能数据导出为 CSV 或 JSON 格式,既方便导入 Excel 做图表,也便于与 Grafana、ELK 等监控平台集成。

下面这段代码展示了如何将性能快照追加写入 CSV 文件,以及一次性导出为结构化的 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function Export-PerformanceData {
<#
.SYNOPSIS
将性能数据导出为 CSV 和 JSON 格式
.PARAMETER OutputDir
输出目录路径
#>
param(
[string]$OutputDir = "$HOME\PerfLogs"
)

if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}

$snapshot = Get-SystemPerformanceSnapshot
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"

# 扁平化磁盘数据,便于 CSV 追加
$flatData = [PSCustomObject]@{
Timestamp = $snapshot.Timestamp
CpuUsagePct = $snapshot.CpuUsagePct
MemoryUsedPct = $snapshot.MemoryUsedPct
FreeMemoryGB = $snapshot.FreeMemoryGB
DiskInfo = ($snapshot.Disks | ForEach-Object {
"$($_.Drive)=$($_.UsedPct)%"
}) -join '; '
}

# 追加写入 CSV(适合长期采集)
$csvPath = Join-Path $OutputDir "perf_$(Get-Date -Format 'yyyyMMdd').csv"
$flatData | Export-Csv -Path $csvPath -NoTypeInformation -Append

# 完整快照导出 JSON(适合单次详细记录)
$jsonPath = Join-Path $OutputDir "perf_$timestamp.json"
$snapshot | ConvertTo-Json -Depth 3 | Set-Content -Path $jsonPath

[PSCustomObject]@{
CsvFile = $csvPath
JsonFile = $jsonPath
Written = $true
}
}

Export-PerformanceData

执行结果示例:

1
2
3
CsvFile  : /home/user/PerfLogs/perf_20250422.csv
JsonFile : /home/user/PerfLogs/perf_20250422_091530.json
Written : True

CSV 追加模式让每次采集的行数据逐行写入同一个文件,配合 Excel 或 Python 脚本即可绘制性能趋势曲线。JSON 格式则保留了完整的嵌套结构,适合程序化消费。

持续监控与阈值告警

单次采集只能看到瞬时状态,真正的运维监控需要持续轮询并在指标异常时主动告警。下面这段脚本实现了一个轻量级的持续监控循环,支持 CPU、内存、磁盘三个维度的阈值检测,并在超限时输出告警信息。

你可以将告警逻辑替换为发送邮件、调用 Webhook 或写入事件日志,实现完整的告警链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function Start-PerformanceWatch {
<#
.SYNOPSIS
持续监控系统性能并在超阈值时告警
.PARAMETER IntervalSeconds
采集间隔(秒),默认 30
.PARAMETER CpuThreshold
CPU 使用率告警阈值,默认 85
.PARAMETER MemThreshold
内存使用率告警阈值,默认 90
.PARAMETER DiskThreshold
磁盘使用率告警阈值,默认 90
.PARAMETER DurationMinutes
监控持续时间(分钟),默认 10
#>
param(
[int]$IntervalSeconds = 30,
[double]$CpuThreshold = 85,
[double]$MemThreshold = 90,
[double]$DiskThreshold = 90,
[int]$DurationMinutes = 10
)

$endTime = (Get-Date).AddMinutes($DurationMinutes)
$alertCount = 0

Write-Host "性能监控已启动,持续 $DurationMinutes 分钟,间隔 ${IntervalSeconds}s" `
-ForegroundColor Cyan
Write-Host ("CPU>{0}% | MEM>{1}% | DISK>{2}% 触发告警`n" -f `
$CpuThreshold, $MemThreshold, $DiskThreshold) -ForegroundColor DarkGray

while ((Get-Date) -lt $endTime) {
$snap = Get-SystemPerformanceSnapshot
$alerts = @()

if ($snap.CpuUsagePct -gt $CpuThreshold) {
$alerts += "CPU 使用率 $($snap.CpuUsagePct)% 超过阈值 ${CpuThreshold}%"
}
if ($snap.MemoryUsedPct -gt $MemThreshold) {
$alerts += "内存使用率 $($snap.MemoryUsedPct)% 超过阈值 ${MemThreshold}%"
}
foreach ($disk in $snap.Disks) {
if ($disk.UsedPct -gt $DiskThreshold) {
$alerts += "磁盘 $($disk.Drive) 使用率 $($disk.UsedPct)% 超过阈值 ${DiskThreshold}%"
}
}

$timeStr = Get-Date -Format "HH:mm:ss"
if ($alerts.Count -gt 0) {
$alertCount += $alerts.Count
Write-Host "[$timeStr] ALERT:" -ForegroundColor Red -NoNewline
Write-Host " $($alerts -join ' | ')" -ForegroundColor Yellow
} else {
Write-Host "[$timeStr] OK - CPU:$($snap.CpuUsagePct)% MEM:$($snap.MemoryUsedPct)%" `
-ForegroundColor Green
}

Start-Sleep -Seconds $IntervalSeconds
}

Write-Host "`n监控结束,共触发 $alertCount 条告警。" -ForegroundColor Cyan
}

Start-PerformanceWatch -IntervalSeconds 15 -DurationMinutes 2

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
性能监控已启动,持续 2 分钟,间隔 15s
CPU>85% | MEM>90% | DISK>90% 触发告警

[09:15:00] OK - CPU:23% MEM:54.5%
[09:15:15] OK - CPU:28% MEM:55.1%
[09:15:30] ALERT: CPU 使用率 92% 超过阈值 85%
[09:15:45] OK - CPU:31% MEM:56.2%
[09:16:00] OK - CPU:25% MEM:54.8%
[09:16:15] OK - CPU:22% MEM:53.9%
[09:16:30] OK - CPU:27% MEM:55.5%
[09:16:45] OK - CPU:24% MEM:54.2%

监控结束,共触发 1 条告警。

在实际生产环境中,可以将此脚本作为 Windows 计划任务或 systemd 服务运行,配合邮件通知模块(Send-MailMessage)或 Webhook(Invoke-RestMethod)将告警推送到运维群。

注意事项

  1. CIM vs WMI:优先使用 Get-CimInstance 而非已弃用的 Get-WmiObject,CIM 支持远程会话复用,性能更好
  2. 采集频率:间隔不宜过短(建议不低于 10 秒),频繁采集本身会消耗 CPU,尤其在旧设备上
  3. 远程监控:结合 PowerShell Remoting,可以在一台管理机上统一采集多台服务器的性能数据,使用 -ComputerName 参数即可
  4. 权限要求:部分 CIM 类和性能计数器需要管理员权限才能访问,脚本应以提升权限运行
  5. 数据保留:CSV 追加模式下注意定期归档和清理旧文件,避免单个文件过大影响读取性能
  6. 跨平台差异:PowerShell 7 在 Linux/macOS 上部分 CIM 类不可用,需使用 /proc 文件系统或 Get-Process 等替代方案