已解决:停止将结果存储在变量中,改用管道

发布: (2026年2月25日 GMT+8 20:20)
10 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容,我将为您翻译成简体中文并保持原有的格式、Markdown 语法以及技术术语不变。

Executive Summary

TL;DR: 将大量命令输出存入变量会一次性将所有数据加载到 RAM 中,导致内存耗尽并使服务器崩溃。应利用 PowerShell 管道 逐对象 流式处理数据,从而在处理大数据集时保持高效、低内存占用。

  • 将命令结果存入“桶”变量会把 所有 对象一次性加载到内存 → 大数据集会耗尽内存。
  • PowerShell 管道充当 传送带,逐对象处理数据,并保持 小且恒定、可预测 的内存占用。
  • “过滤‑左” 原则:尽可能早地过滤(例如使用原生命令的 -Filter 参数),以最小化数据传输和内存使用。
  • 对于超大数据集或需要重新处理时,可 写入磁盘(例如 Export‑CsvImport‑Csv),实现几乎为零的内存消耗。
  • 直接使用 ForEach-Object(或其别名 %)配合管道输入,可保证一次处理一个对象,避免在预加载变量上使用 foreach 循环带来的开销。

结论: 停止将巨大的命令输出存入变量。学会使用管道;它逐对象流式传输数据,防止内存耗尽,避免在生产服务器上出现灾难性的脚本失败。

真实案例

我记得就像是昨天发生的一样:星期五下午 3:00。一位初级工程师的任务是编写一个“简单”的清理脚本——在我们的 Web 农场中查找并记录所有超过 30 天的临时文件。他写了一行代码:

$files = Get-ChildItem -Path \\web-cluster-*-c$\temp -Recurse

十分钟后,我的呼叫器响了。我们的整个生产 Web 队列(prod-web-01prod-web-20)一个接一个地触发 内存压力警报 并崩溃。

为什么会这样? 该脚本试图把来自 20 台服务器的 数百万个文件对象 加载到管理机器上的单个变量中,导致资源灾难。

我们都经历过——这是一个因使用过程式思维而非流式处理而产生的经典错误。正如 Reddit 线程所概括的那样:

我们不建议将结果存储在变量中。

Source:

变量 vs. 管道

基于变量的方法

$myBigList = Get-ADUser -Filter *

PowerShell获取所有 用户,为每个用户创建一个对象,并 将它们全部保存在 $myBigList 中。

  • 5 万名用户 → 5 万对象占用内存。
  • 适用于几十或几百条记录,但对成千上万甚至上百万条记录来说 后果灾难性

基于管道的方法

Get-ADUser -Filter * | Where-Object {$_.Enabled -eq $false}

管道就像 传送带

  1. Get-ADUser 发出 第一个用户对象。
  2. Where-Object 对其进行评估,决定是保留还是丢弃。
  3. 接着发出下一个对象,如此循环。

结果: 无论处理 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]
支持我的工作
如果这篇文章对你有帮助,你可以请我喝杯咖啡:
👉

0 浏览
Back to Blog

相关文章

阅读更多 »

我们与战争部的协议

与五角大楼达成机密AI部署协议 昨天我们与五角大楼达成协议,部署先进的AI系统于机密…