为 Amazon ECS 和 亚马逊云科技 Lambda 配置.NET 垃圾回收

作者: 诺姆·约翰逊 | 2023 年 3 月

.NET 开发人员依靠.NET 的自动内存分配和垃圾回收 (GC) 机制来处理其应用程序的内存需求。对于大多数用例,开发人员不必担心 GC。但是,在.NET 应用在内存受限的环境(例如容器和 亚马逊云科技 L am bda 函数)中运行的现代架构中,GC 可能需要额外的信息来了解应用程序实际有多少内存可用。

Amazon Elastic Container Service (ECS)

.NET 容器应用程序作为 ECS 任务 部署到 ECS ,为该任务分配的内存量在任务定义中配置。ECS 使用 cgro up(一项 Linux 内核功能)根据任务的配置设置限制 CPU 和内存资源。对于使用 亚马逊云科技 Fargate 启动的 ECS 任务进行计算,内存和 CPU 是任务定义中的必需设置。对于启动到 EC2 实例的 ECS 任务,如果任务定义中定义的容器未定义内存和 CPU 设置,则需要任务内存和 CPU 设置。

任务的 cgroup 层次结构和任务中的各个容器是在启动 ECS 任务时创建的。任务的内存设置是为父级 cgroup 配置的,这限制了用于容器的子 cgroup 的内存量。但是,.NET GC 不支持遍历 cgroup 层次结构来确定可用内存量。这意味着,如果仅在任务定义级别配置内存,则.NET GC 不会看到 cgroup 的内存限制,而是检测底层主机计算的内存大小。

举例来说,运行以下应用程序将报告.NET GC 认为可用的可用内存。如果您使用 Fargate 将其部署为 ECS 任务,且最小内存设置为 0.5GB,则应用程序将报告有 4GB 的可用内存。4GB 来自底层主机计算。


var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();


app.MapGet("/", () => "Memory Test");
app.MapGet("/memory", () =>
{
    return GC.GetGCMemoryInfo().TotalAvailableMemoryBytes.ToString("N0");
});

app.Run();

这会导致当.NET GC 检测到.NET 进程接近为 ECS 任务配置的 0.5GB 限制时,它不会积极释放堆中未使用的内存。这可能会触发 OutofMemoryException 并关闭容器。

如果在 ECS 任务定义中对容器定义设置了硬内存限制,则容器的 cgroup 将设置内存限制,.NET GC 将看到正确的可用内存量供其管理。您可以在控制台的 “环境” 部分为任务定义设置硬盘内存限制。此容器硬盘内存限制也可以通过 亚马逊云科技 软件开发工具包、 适用于 PowerShell 的 亚马逊云科技 工具、 亚马逊云科技 CLI 、AW S C loudFormation 和 AW S 云开发 套件 ( CDK) 以编 程方式设置。

将新的任务定义部署到 ECS 后,上面的示例代码现在将正确地报告 GC 有 0.5GB 的可用内存。

如果任务定义定义了多个容器,则需要根据需要在不同的容器中分配任务定义的内存。

亚马逊云科技 .NET 部署工具

在最新版本的 亚马逊云科技 .NET 部署工具 中,部署到 ECS Fargate 时,容器硬内存限制现在设置为与任务内存限制相同的值。使用 适用于 Visual Studio 的 亚马逊云科技 工具包在命令行或 Visual Stu dio 中使用此工具 。

亚马逊云科技 Lambda

Lambda 中的.NET 代码也可以在内存受限的环境中运行。Lambda 函数的最小内存大小为 128 MB。与 Fargate 一样,Lambda 使用 Linux 的 cgroup 根据 Lambda 函数配置的内存大小限制 CPU 和内存设置。但是,对于 Lambda,cgroup 的使用在.NET 运行时中是完全隐藏的。这再次导致.NET GC 认为可用的内存比实际情况还要多。

例如,如果您使用内存大小为 128MB 部署以下函数,则它报告的内存大小可能会大于 128MB。


[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LambdaMemoryCheck;

public class Function
{
    public string FunctionHandler()
    {
        return GC.GetGCMemoryInfo().TotalAvailableMemoryBytes.ToString("N0");
    }
}

与 ECS 不同,Lambda 没有可以设置的容器硬内存限制。要告知.NET GC 有多少内存可用,你可以设置.NET GC 知道要查找的 dotnet_gcheapHardLim it 环境变量。 dotnet_gcheapHardLimit 的值 是 GC 本身也应该限制的十六进制字节数。为方便起见,下表给出了可用 Lambda 配置的十六进制值,最大为 1GB。

Lambda hexadecimal
128MB 0x8000000
192MB 0xC000000
256MB 0x10000000
320MB 0x14000000
384MB 0x18000000
448MB 0x1C000000
512MB 0x20000000
576MB 0x24000000
640MB 0x28000000
704MB 0x2C000000
768MB 0x30000000
832MB 0x34000000
896MB 0x38000000
960MB 0x3C000000
1024MB 0x40000000

dotnet_gcheapHardLim it 环境变量可以使用任何 亚马逊云科技 开发工具包和工具以编程方式进行设置。也可以在 亚马逊云科技 控制台中进行设置,也可以使用适用于 Visual Studio 的 亚马逊云科技 工具包在 Visual Studio 中进行 设置 。

结论

如果您的.NET 应用程序遇到内存问题,我们建议您使用容器硬内存限制来调整 GC,或者使用 dotn et_gcheapHardLimit 环境变量用于 Lambd a 函数。有关.NET GC 配置设置的更多信息,请查看 MSDN 上的这 篇文章

将来,亚马逊云科技和微软希望让.NET GC自动理解这些环境中的内存限制。微软已经打开了一个 GitHub问题 ,以跟踪ECS使用的层次结构组的处理情况。 亚马逊云科技 将研究如何为.NET Lambda 函数自动设置 dotnet_gcheapHardLim it 变量。

特别感谢微软.NET 团队的 Maoni Stephens 帮助理解.NET GC 行为并合作撰写这篇文章。

Norm Johanson

Norm Johan son

Norm Johanson 从事软件开发工作已有 20 多年,负责开发所有类型的应用程序。自 2010 年以来,他一直在 亚马逊云科技 工作,专注于在 亚马逊云科技 工作的.NET 开发人员经验。你可以在推特 @socketnorm 和 GitHub 上找到他 @normj