提高 .NET Lambda 函数中的 SnapStart 性能

作者: Philip Pittle |

亚马逊云科技最近增加了对 .NET Lambda 函数的 Amazon Lambda SnapStart 支持,以提供更快的函数启动性能,从几秒钟到低至亚秒,通常只需很少的代码更改或根本不做任何代码更改。这篇文章探讨了最大限度地提高几种不同类型 .NET 工作负载的 SnapStart 性能提升的技术。有关 SnapStart 的高级概述和简介,请参阅 https://aws.amazon.com/blogs/aws/aws-lambda-snapstart-for-python-and-net-functions-is-now-generally-available/ 以获取更多内容。

导言

启用 SnapStart 后,发布新功能版本时会拍摄 Firecracker 微虚拟机快照。快照过程包括预热 .NET 进程、对初始化执行环境的内存和磁盘状态进行快照、加密快照,然后将其缓存以实现低延迟访问。

显示 Amazon Lambda 请求生命周期的图表

图 1:Amazon Lambda 请求生命周期

当您随后首次调用函数版本时,随着调用的扩展,Lambda 将从缓存的快照中恢复新的执行环境,而不是从头开始初始化它们,从而缩短启动延迟,因为大部分初始化代码已经执行了。

优化 SnapStart 功能性能

在拍摄快照之前,Lambda 将自行初始化,并创建应用程序函数处理程序的实例并调用其构造函数。这提供了在快照出现之前执行应用程序初始化代码的机会,并且在此阶段可以完成的工作越多,冷启动期间需要完成的工作就越少。这意味着缩短冷启动时间的重大机会。

如果涉及网络或文件 IO 的昂贵操作(例如加载配置或密钥)可以在快照之前完成,那么冷启动时间将受益。涉及反射的初始化,例如构建服务集合和解析大型类,也是优化的主要候选对象。由于 C# 必须在运行时将字节码转换为原生机器指令,因此在 Snapshot 之前执行尽可能多的 Lambda 函数代码,可确保大部分 JITing 已经执行,从而进一步缩短冷启动时间。

挂钩

您可以使用 Amazon.Lambda.Core.SnapshotRestore 类的静态方法自定义 SnapStart 流程并将回调添加到您的应用程序中:

Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(BeforeCheckpoint);
Amazon.Lambda.Core.SnapshotRestore.RegisterAfterRestore(AfterCheckpoint);

  • RegisterBeforeSnapshot:这个挂钩允许你在拍摄快照之前执行代码。用它来:
    • 从磁盘或通过网络调用加载静态配置。
    • 初始化依赖注入并构建初始对象图。
    • 确保您的应用程序代码已经是 JITED。
    • 为您的应用程序执行任何初始化逻辑。
  • RegisterAfterRestore:此挂钩允许您在恢复快照后执行代码。用它来:
    • 刷新快照期间缓存的应用程序数据或配置
    • 重新建立网络连接

以下是从 https://github.com/aws-samples/serverless-dotnet-demo 存储库中摘录的简化示例。完整源代码可在无服务器 dotnet 演示项目中找到。该 GetProduct 函数在其构造函数中调用 Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot,传递一个回调,该回调通过虚拟请求调用该 FunctionHandler 方法。这样可以确保在拍摄快照时该方法已经是 JITED 了。

public class Function
{
    private readonly ProductsDAO _dataAccess;
    public Function()
    {
        _dataAccess = new DynamoDbProducts();
        Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () =>
        {
            for (int i = 1; i <= 10; i++)
            {
                string itemId = "12345ForWarming";

                var request = new APIGatewayHttpApiV2ProxyRequest
                {
                    PathParameters = new Dictionary<string, string> {{"id", itemId}},
                    RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext
                    {
                        Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription
                        {
                            Method = HttpMethod.Get.Method
                        }
                    }
                };

                // Create a dummy item in the db to find
                await _dataAccess.PutProduct(new Product(itemId, "forWarming", default));
                // Hit the path where the item is found
                await FunctionHandler(request, new FakeLambdaContext());
                // Clean up the dummy item in the db
                await _dataAccess.DeleteProduct(itemId);
            }
        });
    }

    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest apigProxyEvent,
        ILambdaContext context)
    {
        var id = apigProxyEvent.PathParameters["id"];

        var product = await _dataAccess.GetProduct(id);

        return new APIGatewayHttpApiV2ProxyResponse
        {
            StatusCode = (int)HttpStatusCode.OK,
            Body = JsonSerializer.Serialize(product),
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }
}

多次执行

细心的读者会注意到上面示例中的 RegisterBeforeSnapshot 委托是在 for 循环中执行的。通过实验,我们观察到运行 10-20 次预热可以带来额外的性能优势,因为这种执行模式向 .NET 运行时发出信号,要求其执行各种运行时优化,例如分层编译。

你需要尝试自己的应用程序,以找到优秀的调用次数,从而获得优秀的冷启动时间。但是请记住,Lambda 10 秒的 INIT_PHASE 时限确实适用。

Amazon.Lambda.Core.SnapshotRestore.RegisterBeforeSnapshot(async () =>
{
  // For max benefit, run warmup code multiple times
  for (int i = 1; i <= 10; i++)
  {
     // warmup code
  }
}

专门用于 ASP.NET Core

对于运行 ASP.NET Core 应用程序的 Lambda 函数,我们添加了两个扩展,用于在快照之前初始化您的应用程序代码和 ASP.NET Core 管道。

对于使用该 AddAWSLambdaHosting 方法将您的 ASP.NET Core 应用程序与 Amazon Lambda 集成的应用程序。您可以使用新 AddAWSLambdaBeforeSnapshotRequest 方法注册并在快照之前执行 HttpRequestMessage。Lambda 运行时将模拟执行此 HttpRequestMessage,以预热 ASP.NET Core 和 Lambda 管道以及您的应用程序代码。

可以多次调用该方法,以便注册多条路线进行预热。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);

// Add warm-up requests before snapshot
builder.Services.AddAWSLambdaBeforeSnapshotRequest(
    new HttpRequestMessage(HttpMethod.Get, "/warmup"));

var app = builder.Build();

app.MapGet("/warmup", () => "Warmup successful");

app.Run(); 

对于具有扩展其中一个函数基类 LambdaEntryPoint 的类的应用程序,例如 APIGatewayProxyFunction 来自 Amazon.Lambda.AspNetCoreServer,有一个新的虚拟方法 GetBeforeSnapshotRequests,它可以返回 HttpRequestMessage 对象的集合:

public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction
{
    protected override IEnumerable<HttpRequestMessage> GetBeforeSnapshotRequests =>
        [
            new HttpRequestMessage(HttpMethod.Get, "/warmup"),
            new HttpRequestMessage(HttpMethod.Get, "/another-warmup-route")
        ];
}

Lambda 运行时将模拟执行这些 HttpRequestMessage 对象,以预热 ASP.NET Core 和 Lambda 管道以及您的应用程序代码。

注意:使用 AddAWSLambdaBeforeSnapshotRequest 需要 Amazon.Lambda.AspNetCoreServer.Hosting 版本 8.0 或更高版本,而该 GetBeforeSnapshotRequests 方法需要 Amazon.Lambda.AspNetCoreServer 版本 9.1.0 或更高版本。

量化性能增益

我们将使用以下示例存储库来测量禁用、启用 SnapStart 然后进行优化的 Lambda 函数的冷启动时间:https://github.com/aws-samples/serverless-dotnet-demo/tree/main/src/NET8MinimalAPISnapSnart。此 Lambda 函数使用 ASP.NET Minimal API 来公开多个端点。README.md 文件中提供了完整的测试说明。

该存储库包括 artillery.io 负载测试工具的配置,因此我们可以生成大量的并行请求以导致多次冷启动。我们将测量预热时间(以启用 SnapStart 时的 restoreDuration 和其他方式的 initDuration 记录)的 p50、p90 和 p99 以及函数执行持续时间的 p50、p90 和 p99。

下表显示了结果:

恢复或初始化持续时间(毫秒) 函数持续时间(毫秒)
函数 p50 p90 p99 p50 p90 p99
已禁用 SnapStart 767.91 809.69 954.86 816.19 870.97 955.81
已启用 SnapStart 416.16 510.09 651.47 468.2 500.77 3006.89
SnapStart 优化 415.24 516.01 679.33 155.53 182.26 491.17

启用 SnapStart 后,还原持续时间始终比未启用 SnapStart 的 Lambda 的等效初始持续时间长几百毫秒。通过使用本博客中讨论的优化,90% 的冷启动功能持续时间低至 182 毫秒。

加上功能持续时间和恢复/初始化持续时间,我们可以更全面地了解消费者将如何体验冷启动。在 P90 上,客户端在未启用 SnapStart 的情况下调用 Lambda 时将等待 1,680.66 毫秒,而使用带优化的 SnapStart 调用 Lambda 的等待时间为 698.27 毫秒。注意:这两种计算都不包括网络传输时间。

摘要

为 .NET Lambda 函数启用和优化 SnapStart 是最大限度地缩短冷启动时间和减少应用程序延迟的重要工具。访问 Lambda 开发人员指南中的使用 Lambda SnapStart 提高启动性能,了解有关 SnapStart 的更多信息。

尝试在自己的应用程序中进行优化,或者访问我们的示例存储库查看使用 SnapStart 的示例:https://github.com/aws-samples/serverless-dotnet-demo/tree/main/src/NET8MinimalAPISnapSnart

发现了错误或者想推荐一个功能?你可以在 GitHub 上通过 https://github.com/aws/aws-lambda-dotnet 存储库与团队联系。



菲利普·皮特尔

Philip Pittle

Philip 'pj' Pittle 从 Framework 1.1 开始就一直在开发 .NET 应用程序。他是一名高级软件工程师,为亚马逊云科技开发人员构建 .NET 工具,专注于开源。你可以通过 https://github.com/ppittle 和 https://twitter.com/theCloudPhil 找到他。


*前述特定亚马逊云科技生成式人工智能相关的服务仅在亚马逊云科技海外区域可用,亚马逊云科技中国仅为帮助您发展海外业务和/或了解行业前沿技术选择推荐该服务。