ASP.NET Core 8 的内存占用可以更低吗?

Maoni Stephens 是 .NET 垃圾回收器 (GC) 的首席架构师之一,她在2023年8月份发表了一篇关于 .NET GC 新功能的博客文章,该功能称为 Dynamic Adaption To Application Sizes (DATAS),该功能将随 .NET 8 一起提供。此功能将在应用运行时自动增加或减少服务器 GC 模式下的托管堆数量。它减少了 .NET 应用使用的内存总量,使服务器 GC 模式成为内存受限环境(如 Docker 容器或 Kubernetes Pod)的可行选项,这些环境可以访问多个逻辑 CPU 内核。

服务器 GC 模式和工作站 GC 模式之间的差异

工作站模式最初是为客户端应用程序设计的。过去,执行应用代码的线程会停止,直到 GC 运行完成。在桌面应用程序中,您不希望在几毫秒甚至几秒钟内出现冻结,因此 Workstation GC 经过调整,可以更频繁地执行运行,并更快地完成单个运行。从 .NET Framework 4.0 开始,我们还具有后台 GC 运行模式,可最大程度地减少线程被阻塞的时间。

相比之下,服务器 GC 旨在最大限度地提高服务的吞吐量,这些服务将随着时间的推移接收短期请求。GC 运行频率较低,但可能需要更长的时间。最后,您将在 GC 上运行上花费更少的时间,而将更多的时间花在服务代码上。

最明显的区别如下:Workstation GC 仅使用单个托管堆。托管堆由以下子堆组成:

  • 小对象堆 (SOH) 及其三代 0、1 和 2。小于 85,000 字节的对象将在此处分配。
  • 大型对象堆 (LOH),用于大于或等于 85,000 字节的对象。
  • 固定对象堆 (POH),主要由为此执行互操作和固定缓冲区的库使用(例如,用于网络或其他 I/O 方案)。

在服务器 GC 模式下,您将拥有多个这样的托管堆,默认情况下每个逻辑 CPU 内核一个,但这可以通过 GCHeapCount 进行调整。

托管堆数量增加,以及 GC 运行执行频率较低,是解释为什么服务器 GC 模式下内存消耗要高得多的重要因素。

但是,如果您希望从服务器 GC 模式中受益,同时在运行时动态调整托管堆的数量,该怎么办?一个典型的方案是在云中运行的服务,它必须在特定的突发时间处理大量请求,但之后它应该缩减以减少内存消耗。到目前为止,除了使用不同的配置值重新启动服务外,您没有办法实现这一点。纵向扩展也需要重新启动,因此许多开发团队只是试图通过 GCHeapCountConserveMemory 选项找到折衷方案。

这时,.NET 8 带来了一项名为"动态适应应用程序大小"(DATAS) 的新功能就派上用场了。DATAS 在运行时将按以下方式运行:

  1. GC 将仅从单个托管堆开始。
  2. 根据称为"吞吐量成本百分比"的指标,GC 将决定增加托管堆的数量是否可行。这将在每三次 GC 运行时进行评估。
  3. 还有一个称为"空间成本"的指标,GC 使用它来决定是否应该减少托管堆的数量。
  4. 如果 GC 决定增加或减少托管堆的数量,它将阻塞您的线程(类似于压缩 GC 运行)并创建或删除托管堆。相应的内存区域将被移动。当涉及到托管堆中内存的内部组织时,在 .NET 6 和 .NET 7 中从段切换到区域,使此方案成为可能。

优点和缺点?

DATAS 允许在内存受限环境中使用服务器 GC 模式,例如在 Docker 容器、Kubernetes Pod 。在您的服务将受到大量请求的攻击突发期间,GC 将动态增加托管堆的数量,以便从服务器 GC 的优化吞吐量设置中受益。突发结束后,GC 将再次减少托管堆的数量,从而减少应用使用的内存总量。即使在突发期间,GC 也可能选择将托管堆增加到每个逻辑 CPU 内核少于 1 个,因此您最终可能会使用更少的内存,而无需手动配置托管堆的数量。

请记住:当应用只有一个逻辑 CPU 内核可用时,应始终使用 Workstation GC 模式。仅当应用有两个或更多可用内核时,服务器 GC 模式才有用。此外,我建议您验证您是否确实需要服务器 GC 模式。使用 K6NBomber 等工具来衡量 Web 应用的吞吐量。如果仔细设计了应用的内存使用情况,则吞吐量可能根本没有差异。永远记住:.NET GC 只会在分配内存时执行其运行。

DATAS 是一项很棒的新功能,它将 Workstation GC 和 Server GC 的优势结合在一起:您开始时内存更少,当请求激增时,GC 可以动态扩展其托管堆的数量以提高吞吐量。当请求数在以后的某个时间点减少时,也可以减少托管堆的数量以释放内存。

DATAS 可以在.NET 8 产品中使用,但是并没有默认启用,需要手动进行指定:若要试用 DATAS,需要安装 .NET 8 SDK,创建一个 .NET 8 应用(例如 ASP.NET Core),然后可以将以下两行添加到 .csproj 文件:

<PropertyGroup>

<ServerGarbageCollection>true</ServerGarbageCollection>

<GarbageCollectionAdapatationMode>1</GarbageCollectionAdapatationMode>

</PropertyGroup>

您还可以在构建项目时通过命令行参数指定它:

dotnet build /p:ServerGarbageCollection=true /p:GarbageCollectionAdapatationMode=1

或者在 runtimeconfig.json 中:

"configProperties": {

"System.GC.Server": true,

"System.GC.DynamicAdaptationMode": 1

}

或者通过环境变量:

set DOTNET_gcServer=1

set DOTNET_GCDynamicAdaptationMode=1

请记住:使用上述方法之一时,不得设置 GCHeapCount 选项。如果这样做,GC 将只使用指定数量的堆,而不会激活 DATAS。同样重要的是:如果要在工作站模式下运行,只需将 ServerGarbageCollection 或相应的配置属性/环境变量分别设置为 false 或零。

默认情况下,我的 ASP.NET Core 应用将使用哪种 GC 模式?

你的 ASP.NET Core 应用可以访问多少个逻辑 CPU 内核?如果小于两个,则将使用 Workstation GC 模式。否则,默认情况下将激活服务器 GC 模式。因此,在 Docker、Kubernetes 或云环境中为应用指定约束时要特别小心,因为这些环境可能会突然进入另一个 GC 模式,占用的内存比预期的要多。