已解决:停止将结果存储在变量中,改用管道
Source: Dev.to
请提供您希望翻译的正文内容,我将为您翻译成简体中文并保持原有的格式、Markdown 语法以及技术术语不变。
Executive Summary
TL;DR: 将大量命令输出存入变量会一次性将所有数据加载到 RAM 中,导致内存耗尽并使服务器崩溃。应利用 PowerShell 管道 逐对象 流式处理数据,从而在处理大数据集时保持高效、低内存占用。
- 将命令结果存入“桶”变量会把 所有 对象一次性加载到内存 → 大数据集会耗尽内存。
- PowerShell 管道充当 传送带,逐对象处理数据,并保持 小且恒定、可预测 的内存占用。
- “过滤‑左” 原则:尽可能早地过滤(例如使用原生命令的
-Filter参数),以最小化数据传输和内存使用。 - 对于超大数据集或需要重新处理时,可 写入磁盘(例如
Export‑Csv→Import‑Csv),实现几乎为零的内存消耗。 - 直接使用
ForEach-Object(或其别名%)配合管道输入,可保证一次处理一个对象,避免在预加载变量上使用foreach循环带来的开销。
结论: 停止将巨大的命令输出存入变量。学会使用管道;它逐对象流式传输数据,防止内存耗尽,避免在生产服务器上出现灾难性的脚本失败。
真实案例
我记得就像是昨天发生的一样:星期五下午 3:00。一位初级工程师的任务是编写一个“简单”的清理脚本——在我们的 Web 农场中查找并记录所有超过 30 天的临时文件。他写了一行代码:
$files = Get-ChildItem -Path \\web-cluster-*-c$\temp -Recurse
十分钟后,我的呼叫器响了。我们的整个生产 Web 队列(prod-web-01 … prod-web-20)一个接一个地触发 内存压力警报 并崩溃。
为什么会这样? 该脚本试图把来自 20 台服务器的 数百万个文件对象 加载到管理机器上的单个变量中,导致资源灾难。
我们都经历过——这是一个因使用过程式思维而非流式处理而产生的经典错误。正如 Reddit 线程所概括的那样:
“我们不建议将结果存储在变量中。”
Source: …
变量 vs. 管道
基于变量的方法
$myBigList = Get-ADUser -Filter *
PowerShell 会 获取所有 用户,为每个用户创建一个对象,并 将它们全部保存在 $myBigList 中。
- 5 万名用户 → 5 万对象占用内存。
- 适用于几十或几百条记录,但对成千上万甚至上百万条记录来说 后果灾难性。
基于管道的方法
Get-ADUser -Filter * | Where-Object {$_.Enabled -eq $false}
管道就像 传送带:
Get-ADUser发出 第一个用户对象。Where-Object对其进行评估,决定是保留还是丢弃。- 接着发出下一个对象,如此循环。
结果: 无论处理 100 条还是 1000 万条对象,内存占用都 保持极小、恒定且可预测。
专业提示: 变量会在你能够操作之前把所有内容收集起来。管道则让你 在项目到达时就立即处理。对于大规模自动化,管道是唯一可扩展的方案。
三种实用方法(快速修复 → “破窗”)
1️⃣ 直接管道到 ForEach-Object(最直接的解决方案)
与其先保存到变量再用 foreach 迭代,不如直接把输出管道传给 ForEach-Object(别名 %)。这样可以保证 一次处理一个。
不好的做法(占用大量内存)
# WARNING: Loads ALL VMs into memory first!
$allVMs = Get-VM -ComputerName prod-hyperv-cluster
foreach ($vm in $allVMs) {
if ($vm.State -eq 'Off') {
Write-Host "$($vm.Name) is currently off. Removing snapshot."
Get-VMSnapshot -VMName $vm.Name | Remove-VMSnapshot
}
}
好的做法(流式处理)
# Processes one VM at a time. Beautiful.
Get-VM -ComputerName prod-hyperv-cluster | ForEach-Object {
if ($_.State -eq 'Off') {
Write-Host "$($_.Name) is currently off. Removing snapshot."
# $_ represents the current object in the pipeline
Get-VMSnapshot -VMName $_.Name | Remove-VMSnapshot
}
}
2️⃣ 及早过滤 – “左侧过滤”原则
在命令链中尽可能 左侧(越早)进行过滤。避免先拉取大量数据再在本地丢弃大部分。
效率低的做法(后置过滤)
# Pulls ALL users, then filters. Bad for network & memory.
Get-ADUser -Filter * -Properties LastLogonDate |
Where-Object {
$_.Enabled -eq $false -and $_.LastLogonDate -lt (Get-Date).AddDays(-90)
} |
Select-Object Name
效率高的做法(左侧过滤)
# Let the Domain Controller do the heavy lifting.
$ninetyDays = (Get-Date).AddDays(-90).ToFileTime()
Get-ADUser -Filter {
Enabled -eq $false -and LastLogonTimestamp -lt $ninetyDays
} -Properties LastLogonTimestamp |
Select-Object Name
通过使用 cmdlet 本身的 -Filter 参数,你让 AD 服务器只返回 匹配的用户,从而大幅减少进入管道的对象数量。
3️⃣ 将巨量、可重复使用的数据集写入磁盘
当数据集真的非常庞大且需要多次处理(或源 API 较慢)时,不要把它保存在内存中。先把它导出到文件,需要时再流式读取。
# Export once (near‑zero memory)
Get-ADUser -Filter * -Properties * | Export-Csv -Path 'AllUsers.csv' -NoTypeInformation
# Later, stream it back for each processing pass
Import-Csv -Path 'AllUsers.csv' | Where-Object { $_.Enabled -eq $false } | ForEach-Object {
# Process each filtered record...
}
导出/导入循环使用 磁盘 I/O 而非 RAM,使你能够在不一次性加载完整数据集的情况下重复处理这些数据。
要点
- 永不在可以流式处理时将大量命令输出存入变量。
- 优先使用管道 (
|) 进行所有数据处理任务。 - 提前过滤,使用原生的
-Filter参数。 - 写入磁盘,当必须重新处理巨大的数据集时。
采用这些模式,您的脚本将能够优雅扩展,而不会导致生产服务器宕机。 🚀
使用 PowerShell 流式处理大型数据集
将数据写入磁盘上的临时文件。
这是一种“投机取巧”但极其有效的方法。你只需一次性将所有内容写入文件(如 CSV 或 JSONL),随后逐行读取该文件,几乎不占用内存。
处理流程
第 1 步 – 将海量数据集导出到文件
# Export‑Csv 很好,因为它能干净地处理对象。
Get-VeryLargeDataset -Server prod-db-01 |
Export-Csv -Path C:\temp\dataset.csv -NoTypeInformation
第 2 步 – 从文件流式处理数据
# Import‑Csv 在管道中会逐条流式读取记录。
Import-Csv -Path C:\temp\dataset.csv |
ForEach-Object {
# 对每一行 ($_ ) 进行处理,一次只处理一条。
# 整个文件不会被一次性加载到内存中。
if ($_.Status -eq 'Failed') {
Invoke-MyRetryLogic -ID $_.TransactionID
}
}
第 3 步 – 清理工作!
Remove-Item -Path C:\temp\dataset.csv
方法、优缺点
| 方法 | 优点 | 缺点 |
|---|---|---|
| 1. 流水线 (Pipelining) | • 极低的内存使用 • 符合 PowerShell 的惯用写法 • 对大多数任务来说速度快 | • 数据是瞬时的;如果不重新运行初始命令,就无法轻松重新处理 |
| 2. 左侧过滤 (Filtering Left) | • 最高效的方法 • 减少内存、CPU 和网络负载 | • 依赖源命令具备强大的服务器端过滤能力 |
| 3. 写入磁盘 (Spooling to Disk) | • 能处理几乎无限大的数据量 • 数据持久化,可供后续重新处理 | • 由于磁盘 I/O 最慢 • 需要临时磁盘空间 • 代码更复杂 |
要点
下次你准备键入 $results = … 时,先停一下,思考一下:
“这个命令可能返回多少条记录?”
如果答案是 “我不知道” 或 “很多”,请为自己(以及服务器)着想:放弃使用变量,改用流水线。以后在周五下午 3 点不被叫去处理问题的你,一定会感谢现在的决定。
👉 阅读原文于 [TechResolve.blog]
☕ 支持我的工作
如果这篇文章对你有帮助,你可以请我喝杯咖啡:
👉