亚马逊AWS官方博客

使用 C# 和 Rust 优化 Amazon Lambda 函数扩展

本博文作者为 Serverless 高级专家级解决方案架构师 Siarhei Kazhura

客户可使用 Amazon Lambda 扩展将监控、可观测性、安全性和治理工具与 Lambda 函数集成。亚马逊云科技与 Amazon Lambda Ready Partners(如 DatadogDynatraceNew Relic)共同提供可直接运行的扩展服务。此外,客户也可开发自己的扩展程序来解决特定需求。

外部 Lambda 扩展通常设计为伴生进程,与函数代码在同一执行环境中运行,这意味着 Lambda 函数会与扩展共享内存、CPU 和磁盘 I/O 等资源,设计不当的扩展会导致性能退化和额外成本。

本篇博文会展示如何使用 Amazon CloudWatch 仪表板上的关键性能指标衡量扩展对函数性能的影响。

本篇博文将重点介绍使用 C#Rust 语言编写的 Lambda 扩展,展示选择使用 Rust 编写 Lambda 扩展的优势。同时,还会解释如何优化 C# 编写的 Lambda 扩展,实现三倍的性能提升。该解决方案也适用于您选择的其他编程语言。

概述

C# 编写的 Lambda 函数(在.NET 6 中运行)为 HeaderCounter,常用作基线,用以计算请求标头的数量并返回响应数量。在函数代码中插入 500 毫秒的静态延迟来模拟额外的计算。该函数使用最小内存设置(128MB),可放大扩展对性能的影响。

使用 curl 命令执行负载测试,该命令针对 Lambda 函数支持的公共 Amazon API Gateway 端点发送 5000 个请求(同时运行 250 个请求)。CloudWatch 仪表板 lambda-performance-dashboard 显示了该函数的性能指标。

仪表板显示的指标:

  1. 最大持续时间和平均持续时间指标,用于评估扩展对函数执行时间的影响。
  2. PostRuntimeExtensionsDuration 指标,用于衡量函数调用后扩展所用的额外时间。
  3. 平均使用内存和分配内存指标,用于评估扩展对函数内存消耗的影响。
  4. 冷启动时间和冷启动指标,用于评估扩展对函数冷启动的影响。

扩展运行

C# 和 Rust 编写的扩展在运行方式上略有差别。

Rust 编写的扩展发布为可执行文件,其优势在于可编译为本地代码,直接运行。该扩展对运行环境无要求,因此可与另一运行时中编写的 Lambda 函数一起运行。

其劣势在于可执行文件的大小。扩展作为 Lambda 层占用部署包的大小空间,而 Lambda 的最大解压部署包为 250MB。

C# 编写的扩展发布为动态链接库(DLL)。DLL 包含通用中间语言(CIL),必须通过即时编译器(JIT)转换为本地代码。必须存在.NET 运行时,否则扩展不可运行。在解决方案提供的示例中,由 dotnet 命令运行 DLL。

Blank 扩展

HeaderCounter 函数部署的三个实例:

  1. 第一个实例通过 no-extension 端点实现,无扩展。
  2. 第二个实例通过 dotnet-extension 端点实现,使用 C# 编写的 Blank 扩展。除将收到的事件记录到 CloudWatch 外,该扩展不提供任何额外功能。
  3. 第三个实例通过 rust-extension 端点实现,使用 Rust 编写的 Blank 扩展。除将收到的事件记录到 CloudWatch 外,该扩展不提供任何额外功能。

仪表板显示,这些扩展对 Lambda 函数内存消耗的影响很小,而 C# 扩展在较高的百分位数指标上对 Lambda 函数内存消耗的影响较大,如最大冷启动时间和最大持续时间

EventCollector 扩展

HeaderCounter 函数部署的三个实例:

  1. 第一个实例通过 no-extension 的端点实现,无扩展。
  2. 第二个实例通过 dotnet-extension 端点实现,使用 C# 编写的 EventCollector 扩展,该扩展会将所有扩展调用事件推送至 Amazon S3
  3. 第三个实例通过 rust-extension 端点实现,使用 Rust 编写的 EventCollector 扩展,该扩展会将所有扩展调用事件推送至 Amazon S3


持续时间、冷启动次数和平均 PostRuntimeExtensionDuration 指标看,Rust 扩展对函数内存消耗的影响较小。然而,使用 C# 扩展运行的函数却有明显的性能退化,平均持续时间几乎增加了三倍,最大持续时间增加了约六倍。

函数现在几乎消耗了所有分配内存。Lambda 函数的 CPU、网络和存储依据选定的内存量而进行分配。目前,内存设置为 128MB,是可设置内存的最小值。受限的资源影响了函数的性能。

将内存增加到 512MB,重新运行负载测试,函数性能得到改善。最大持续时间现在是 721 毫秒(包括静态 500 毫秒延迟)。

C# 函数的平均持续时间现在仅比基线高 59 毫秒,平均 PostRuntimeExtensionDuration 为 36.9 毫秒(之前为 584 毫秒)。这一性能提升归因于内存增加,无需改变任何代码。

也可使用 Lambda Power Tuning 来确定 Lambda 函数的最佳内存设置。

垃圾回收

与 C# 不同,Rust 并不是用于垃圾回收的编程语言。垃圾回收(GC)是个管理应用程序内存分配和释放的过程,资源密集,会影响到较高的百分位数指标。可利用 Blank 扩展和 EventCollector 扩展的指标查看 GC 的影响。

Rust 具备所有权借用特性,允许不依赖 GC 的安全内存释放,是 Lambda 函数扩展等工具的良好运行时选择。

EventCollector Native AOT 扩展

Native ahead-of-time(Native AOT)编译(.NET 7 和.NET 8 中可用)允许 C# 编写的扩展交付为可执行文件,与 Rust 编写的扩展类似。

Native AOT 不使用 JIT 编译器,可将应用程序编译为独立的(所有所需资源均被封装)可执行文件,在编译时指定的目标环境(例如 Linux x64)中运行。

使用 Native AOT 编译.NET 扩展并重新运行性能测试的结果如下(函数内存设定为 128MB):

C# 扩展的平均持续时间现在接近基线(DLL 是基线的三倍),平均 PostRuntimeExtensionDuration 现在是 0.77 毫秒(DLL 是 584 毫秒)。在最大 PostRuntimeExtensionDuration 指标方面,C# 扩展也优于 Rust 扩展,C# 扩展为 297 毫秒,而 Rust 扩展为 497 毫秒。

总体而言,Rust 扩展拥有更优的平均/最大持续时间、平均/最大冷启动时间和内存消耗,C# 扩展的 Lambda 函数占用几乎所有的分配内存。

要考虑的另一指标是二进制大小。Rust 扩展可编译为 12.3MB 的二进制文件,而 C# 扩展可编译为 36.4MB 的二进制文件。

示例演示

欲查看示例演示,请访问 GitHub 库。示例演示包含:

  1. 所需先决条件
  2. 详细解决方案部署演示
  3. 清理过程
  4. 成本考量

总结

本篇博文展示了可用于运行和剖析不同类型 Lambda 函数扩展的技术,重点介绍了使用 C# 和 Rust 编写的 Lambda 扩展,概述了用 Rust 编写 Lambda 扩展的优势,以及可用于提升 C# 编写的 Lambda 扩展性能的技术。

在使用 Amazon Lambda crate 运行时扩展的基础上,用 Rust 编写 Lambda 扩展,这是 Amazon Lambda Rust 运行时的一部分。

欲了解更多 Serverless 的学习资源,请访问 Serverless Land

Original URL:https://aws.amazon.com/blogs/compute/optimizing-aws-lambda-extensions-in-c-and-rust/