如何提高从 AWS Lambda (Java) 初始调用 AWS 服务的性能?

How to improve performance of initial calls to AWS services from an AWS Lambda (Java)?

我最近尝试分析 AWS Lambda 中托管的服务的一些性能问题。 分解这个问题,我意识到这只是在每个容器的第一次调用中。 在隔离问题时,我发现自己创建了一个新的测试项目来获得一个简单的示例。

Test project (您可以克隆它,构建它 mvn package,部署它 sls deploy,然后通过 AWS 管理控制台测试它。)

此项目有 2 个 AWS Lambda 函数:sourcetargettarget 函数只是 returns 一个空的 json {}source 函数使用 AWS Lambda SDK 调用 target 函数。

target 函数的大约持续时间在冷启动时为 300-350 毫秒,在热调用时为 1 毫秒。 source 函数的大约持续时间在冷启动时为 6000-6300 毫秒,在热调用时为 280 毫秒。

source 函数冷启动的 6 秒开销似乎是获取客户端的 3 秒和调用其他函数的 3 秒,在热调用中分别为 3 毫秒和 250 毫秒。 对于 AWS SNS 等其他服务,我得到了类似的时间。

我真的不明白它在那 6 秒内做了什么,也不知道我能做些什么来避免它。 在进行热身调用时,我可以获得客户端并存储引用以避免前几秒,但其他几秒来自实际使用其他服务(SNS、Lambda 等),我不能真正做到无操作。

那么,其他人是否经历过相同的冷启动持续时间,我可以做些什么来提高性能? (除了设置内存)

预配置的并发性有助于您拥有的代码初始化持续时间。除此之外,它针对的是来自函数代码的执行环境设置的另一项开销。

请参阅打开预配置并发部分 here

Java Lambda 冷启动时间慢的主要原因是需要加载 类 和初始化对象。对于简单的程序,这可能非常快:除了打印“Hello, World”之外什么都不做的 Lambda 将在 ~40 毫秒内 运行,这与 Python 运行 时间相似。另一方面,Spring 应用程序将花费更多时间来启动,因为即使是简单的 Spring 应用程序在执行任何有用的操作之前也会加载数千个 类。

虽然减少冷启动时间的明显方法是减少需要加载的 类 数量,但这很少容易做到,而且通常是不可能的。例如,如果您在 Spring 中编写 Web 应用程序,则无法在处理 Web 请求之前初始化 Spring 应用程序上下文。

如果这不是一个选项,并且您正在使用 Maven Shade 插件生成“uber-JAR”,您应该切换到我描述的 Assembly 插件 here。原因是 Lambda 会解压缩您的部署包,因此“uber-JAR”会变成许多必须单独打开的小类文件。

最后,增加内存分配。这毫无疑问是您可以为 Lambda 性能做的最好的事情,Java 或其他。首先,因为增加内存减少了 Java 垃圾收集器必须做的工作量。二是因为amount of CPU that your Lambda gets is dependent on the memory allotment。在 1,769 MB 之前,您不会获得完整的虚拟 CPU。我建议为 Java 应用分配 2 GB;更大分配的成本通常被减少的 CPU 需求所抵消。

不会做的一件事是为预配置的并发付费。如果你想要一台机器一直 运行ning,使用 ECS/EKS/EC2。并认识到,如果您的需求激增,您仍然会冷启动。


更新: 我在假期里花了一些时间量化各种性能改进技术。完整的文章是 here,但数字值得重复。

我的示例程序与 OP 一样,是一个“什么都不做”,它只是创建了一个 SDK 客户端并用它来调用 API:

public void handler(Object ignored, Context context)
{
    long start = System.currentTimeMillis();
    
    AWSLogs client = AWSLogsClientBuilder.defaultClient();
    
    long clientCreated = System.currentTimeMillis();
    
    client.describeLogGroups();
    
    long apiInvoked = System.currentTimeMillis();
    
    System.err.format("time to create SDK client = %6d\n", (clientCreated - start));
    System.err.format("time to make API call     = %6d\n", (apiInvoked - clientCreated));
}

我运行 这具有不同的内存大小,每次都强制冷启动。所有时间都以毫秒为单位:

|                   |  512 MB | 1024 MB | 2048 MB | 4096 MB |
|+++++++++++++++++++|+++++++++|+++++++++|+++++++++|+++++++++|
| Create client     |    5298 |    2493 |    1272 |    1019 |
| Invoke API call   |    3844 |    2023 |    1061 |     613 |
| Billed duration   |    9213 |    4555 |    2349 |    1648 |

正如我上面所说,增加内存的主要好处是同时增加了 CPU。创建和初始化 SDK 客户端是一项 CPU 密集型工作,因此您提供的 CPU 越多越好。


更新 2: 今天早上我尝试用 GraalVM 编译一个简单的 AWS 程序。构建独立的可执行文件需要几分钟时间,而且由于 AWS SDK 的依赖性,它甚至创建了一个“后备映像”(具有嵌入式 JDK)。当我比较运行次时,运行标准Java没有区别。

底线:将 Java 用于 运行 足以从 Hotspot 中获益的事物。使用不同的语言(Python、JavaScript,也许是 Go)来处理短 运行ning 和需要低延迟的事情。

aws-lightweight-client-java 是一个独立的 jar(无依赖)并且小于 60K。构建它的确切目的是减少 Java Lambda 冷启动时间,它的作用相当大且易于使用(尽管您可能需要查看 AWS API 文档来完成您的任务)。我发现使用 AWS SDK S3 jar 我的冷启动时间大约是 10 秒,而使用这个轻量级客户端它下降到 4 秒(分配了 512MB 内存)。为 Lambda 分配 2GB 内存,AWS SDK 的冷启动时间为 3.6 秒,轻量级客户端的冷启动时间降至 1 秒。

库进行 https 调用这一事实确实带来了 2000 左右的加载 类 所以很难比 1s 快很多(除非那里有一些很酷的 https 库在这方面更有效率)。

基本上,每次我必须优化 lambda 性能时,我都会使用一组建议作为作弊 sheet。

  1. 使用 SDKv2。 我见过很多次 AWS SDKv1 和 v2 完全不兼容。从 v1 迁移到 v2 可能很容易,但有时 API 变化太大,以至于您根本无法在 V2 中找到相应的方法。但是,如果可以,那么您最好这样做。 V2 引入了很多性能改进,所以这是一条经验法则“如果可能,请尽可能使用 V2”

  2. 使用定义的凭据提供程序。 AWS SDK 有一种非常有趣的检测凭证的方法。它通过多个步骤尝试找出正确的凭据,直到找到或找不到任何凭据。

  • Java 系统属性
  • 环境变量
  • 来自 AWS STS 的 Web 身份令牌
  • 共享凭据和配置文件
  • Amazon ECS 容器凭证
  • Amazon EC2 实例配置文件凭据

所有这些步骤都需要时间,您可以通过指定确切的凭据提供程序来节省几毫秒。像这样:

S3Client client = S3Client.builder()
       .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
       .build();

这样 SDK 就不会遍历所有可能的信用来源并立即检测到正确的来源。

  1. 执行前初始化一切 遵循简单的建议。您可能希望简化事情并将所有初始化放入处理程序方法中。最好不要这样做,而是尽量将尽可能多的初始化放入构造函数中。它可以减少重复 lambda 调用的延迟。

  2. 减小 jar 大小 要减少 lambda 冷启动,一个不明显的建议是减小 jar 的大小。 Java 开发人员通常不关心包含更多的库以避免重新发明轮子。但是对于 lambda,你最好仔细看看你的 pom.xml 并清除所有不必要的东西。因为更大的罐子意味着更长的冷启动时间。

  3. 避免使用任何 DI 我不认为你想使用任何类型的 DI。但如果你确实试图避免它。 Lambda 的目的是小而轻。并且 DI 将显着增加冷启动并且连接 2-3 没有多大意义 类。

  4. 使用分层编译 Java 即时编译具有自 Java 8 发布以来引入的分层编译这样一个很酷的功能。 JIT的目的是运行代码,最终达到原生代码的性能。它不能立即完成。但是 运行 整理代码并分析热点 JIT 最终解释代码的效果几乎与原生代码一样好。这可以通过在后台收集分析信息来实现。这对于您的整体式应用程序 运行 在 servlet 容器中长期使用是有意义的。但是 short-lived Lambda 无法从这些优化中受益,最好将其完全关闭。为此,请放置这些环境变量: how to put env vars

为了更好的理解,我会参考oracle文档:https://docs.oracle.com/javacomponents/jrockit-hotspot/migration-guide/comp-opt.htm#JRHMG119

  1. 明确指定区域和 HttpClient 默认情况下,AWS SDK 支持 3 个不同的 HTTP 库,它们是 apache、netty 和 built-in JDK HTTP 客户端。 Apache 和 Netty 有很多标准 built-in 没有的特性,但我们必须减少冷启动,所以更喜欢使用 built-in 并排除另外两个以减少对生成的 jar 的依赖。
<dependency>
            <groupId>software.amazon.awssdk</groupId>
            <artifactId>s3</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>software.amazon.awssdk</groupId>
                    <artifactId>netty-nio-client</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>software.amazon.awssdk</groupId>
                    <artifactId>apache-client</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

该地区的情况几乎相同。确定部署了 lambda 的区域需要一些时间,可以通过明确指定区域来减少这个时间。 总体而言,生成的配置应如下所示:

       .region(Region.US_WEST_2)
       .httpClient(UrlConnectionHttpClient.builder().build())
       .build();
  1. 使用 RDS Proxy 进行连接池 如果您计划将 Lambda 与 RDS 结合使用,那么此建议可能会对您有所帮助,否则请跳过它。在“正常”Java 应用程序中,通常使用连接池来重用现有连接并节省一些建立新连接的时间。当您使用 Lambda 时,RDS Proxy 服务将助您一臂之力。

  2. 增加分配的内存 简单而有力的建议。可能是您的 Lambda 可以 运行 分配标准的 128 Mb 内存。在这种情况下增加内存似乎是正确的。隐藏而不明显的是,增加分配的内存会使您的 lambda CPU 可用。因此,增加分配的内存和更多虚拟 CPU 可用功率的组合当然会减少执行时间。给你的 lambda 更多内存和 CPU 意味着增加成本。但执行时间更少。与其猜测哪种组合更好,我建议使用这个工具:https://github.com/alexcasalboni/aws-lambda-power-tuning

  3. 使用预配置的并发 其他人已经提到了这一点。可能是解决问题最简单易行的方法。但它会产生额外的费用。规定的骗局urrency 意味着 AWS 将保持执行上下文准备好供您使用,从而减少 lambda 冷启动。您可以指定预配实例的数量并享受为您预热的 lambda。

有使用 Graal VM 的奇特建议,但我认为我的回答足够长。