为何在ForEach循环内使用Start-IISCommitDelay会导致PowerShell内存暴涨?
IISCommitDelay作用解析及循环调用内存激增原因
当在ForEach-Object的Process块中反复调用Start-IISCommitDelay和Stop-IISCommitDelay时,100次循环后PowerShell内存占用会剧增至1.5GB;若将这两个命令移至Begin和End块,内存使用恢复正常,但即便调用[System.GC]::Collect()也无法释放内存。以下解析IISCommitDelay的具体作用及内存异常的成因:
一、IISCommitDelay的核心作用
Start-IISCommitDelay和Stop-IISCommitDelay是IIS管理模块的配套命令,核心是批量延迟提交配置变更,避免频繁写入磁盘和刷新配置。- 默认情况下,每一次IIS配置修改(比如新建站点、调整应用池)都会立即写入
applicationHost.config,并触发IIS的配置刷新操作。 - 调用
Start-IISCommitDelay后,后续所有配置变更都会暂存到内存缓存中,直到执行Stop-IISCommitDelay时,才会一次性把所有缓存的变更写入磁盘,完成一次配置刷新。 - 这个机制专为批量配置场景设计:减少磁盘IO次数,避免多次刷新导致的IIS服务波动,大幅提升大规模配置操作的效率。
二、内存异常的成因分析
1. Process块反复调用导致内存激增
- 每次在Process块中调用
Start-IISCommitDelay,都会创建一个独立的配置延迟会话。虽然Stop-IISCommitDelay会结束会话,但PowerShell与IIS配置API的交互中,可能存在未被正确释放的资源句柄或内存缓存。 - 100次循环就会生成100个独立会话,每个会话都会暂存当前配置副本、变更记录等数据,这些临时资源无法及时回收,导致内存占用不断累加。
- 另外,
New-IISSite等命令返回的对象(如$TestSite)在延迟会话中会被额外引用,无法被.NET GC及时回收,进一步加剧内存占用。
2. Begin/End块调用后内存无法释放的原因
- 把
Start-IISCommitDelay放在Begin块、Stop-IISCommitDelay放在End块时,只会创建一个全局延迟会话,所有配置变更都在同一个会话中处理,因此不会出现内存累加激增的情况。 - 调用
[System.GC]::Collect()无效,是因为IIS配置API内部持有部分非托管资源(比如文件句柄、COM对象引用),这些资源不受.NET GC直接管理。即便触发GC,非托管资源也需要特定清理机制释放,而PowerShell的IIS模块并未完全暴露这些清理接口。 - 此外,PowerShell的模块加载上下文本身会保留部分配置缓存,这类缓存不会因GC调用立即释放,需等到PowerShell会话结束后才会被清理。
测试用PowerShell代码
#Requires -RunAsAdministrator #Requires -PSEdition Desktop #Requires -Version 5.1 #Requires -Modules webadministration, iisadministration Import-Module iisadministration Import-Module webadministration $NumberOfnewWebsites = 1..100 $BindingPort = 80 # Bindingport is the place to start - script will test ports until it finds an unused port and use that port for the website. $NumberOfnewWebsites | ForEach-Object -Begin { $WebsiteNumber = 0 $PortsInUse = (Get-NetTCPConnection | Where-Object localport -GE $BindingPort).LocalPort | Sort-Object $Bindingports = $NumberOfnewWebsites for ($i = 0; $i -lt $NumberOfnewWebsites.Count; $i++) { $BindingPortInUse = $true while ($BindingPortInUse) { if ($BindingPort -in $PortsInUse) { $Bindingport = $BindingPort + 1 } else { $BindingPortInUse = $false $Bindingports[$i] = $BindingPort $Bindingport = $BindingPort + 1 } } } # Start-IISCommitDelay } -Process { Start-IISCommitDelay $WebsiteNameTemplate = "TestWebsite" # Actual WebsiteName will be websitename + WebsiteID $WebsiteID = 1 # If 1 is not free, search for the next free ID $ApplicationNameTemplate = "TestApplication" # Search for free websiteID. Startingpoint is $WebsiteID $IDsInUse = (Get-IISSite).ID while ($WebsiteID -in $IDsInUse) { $WebsiteID = $WebsiteID + 1 } # Build variables for later $WebsiteName = $WebsiteNameTemplate + "_" + $WebsiteID $WebsiteHostHeader = [System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName $Binding = "*:" + $Bindingports[$WebsiteNumber] + ":" + $WebsiteHostHeader $ApplicationName = $ApplicationNameTemplate + "_" + $WebsiteID # Write some progress (and no, it does not affect performance) $WebsiteNumber += 1 $Status = $WebsiteNumber.ToString() + " of " + ($NumberOfnewWebsites).count.ToString() Write-Progress -Activity "Creating Websites" -CurrentOperation $WebsiteName -Status $Status -PercentComplete (($WebsiteNumber / $NumberOfnewWebsites.count) * 100) # Create folders for the application $WebsiteDirectory = "d:\websites\jndata\$WebsiteName" $ApplicationDirectory = "d:\websites\jndata\Applications\$ApplicationName" New-Item -ItemType Directory -Path $WebsiteDirectory -Force | Out-Null New-Item -ItemType Directory -Path $ApplicationDirectory -Force | Out-Null # Create Website with a webapplication # Website $ConfigSectionCollection = Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools" | Get-IISConfigCollection New-IISConfigCollectionElement -ConfigCollection $configSectionCollection -ConfigAttribute @{"name" = "$ApplicationName"; "autoStart" = $true; "managedPipelineMode" = "Integrated" } $TestSite = New-IISSite -Name $WebsiteName -BindingInformation $Binding -PhysicalPath $WebsiteDirectory -Passthru # Application $TestSite.Applications.Add("/$ApplicationName", $ApplicationDirectory) | Out-Null $TestSite.Applications["/$ApplicationName"].ApplicationPoolName = "$ApplicationName" # Commit (To storage?) Stop-IISCommitDelay } -End { # Stop-IISCommitDelay } $Status = "Done. Created " + ($NumberOfnewWebsites).count.ToString() + " websites." Write-Progress -Status $Status -Activity "Creating Websites"
内容的提问来源于stack exchange,提问作者Morten Worm Due
相关产品推荐
相关产品推荐

