在《Spring Boot 3 应用程序在 AWS Lambda 上》系列文章中,我们在第二部分(第二部分)中介绍了 AWS 无服务器 Java 容器。在第三部分(第三部分)中,我们解释了如何使用 Java 21 和 Spring Boot 3.2 通过 AWS 无服务器 Java 容器实现 AWS Lambda 函数。在第四部分(第四部分)中,我们测量了 Lambda 函数的启动性能(包括冷启动和热启动时间),包括启用 Lambda SnapStart 并介绍了各种基于 SnapStart 的预热技术。
由于示例中使用的 Spring Boot 3.2 版本已经发布超过一年,而本文撰写时(2024年底)的当前版本已是 3.4,我决定更新所有示例并重新测量 Lambda 性能。此外,示例应用程序中使用的 Spring Boot 3 无服务器 Java 容器及其他许多依赖项的版本也已更新。我还决定使用不同的 Java 编译选项进行更详细的 Lambda 性能测量,并更好地可视化 Lambda SnapStart 快照分层缓存的效果。
如何使用 AWS Serverless Java Container 和 Java 21 管理运行时以及 Spring Boot 3.4 编写 AWS Lambda 函数AWS Serverless Java Container 的概念在[部分 2]中介绍的,以及如何在 AWS Lambda 中使用 AWS Serverless Java Container 的方法在[部分 3]中解释的,仍然适用。
这个简单的样本应用程序也保持不变,请参见下面的架构图。
但我将所有依赖项更新到了撰写本文时的最新版本(即2024年底),并将源代码发布在spring-boot-3.4-with-aws-serverless-java-container仓库中。我们使用的是Spring Boot 3.4.0版本和AWS Serverless Java Container for Spring Boot 3版本2.1.0。当然,从那时起,库中的其他较小或较大版本更新已经发布,但我认为除了在pom.xml中更新版本外,无需对应用程序代码进行任何其他更改即可使其正常运行。据我记忆,在一年后的这种大规模版本更新中,我没有更改应用程序代码。
以下内容需要被安装以搭建并运行示例:
- Java 21,比如Amazon Corretto 21
- Apache Maven
- AWS CLI
- AWS SAM
构建应用程序,执行 mvn clean package
。
执行 sam deploy -g
(部署应用)。
为了创建id为1的产品,请执行
curl -X PUT -d '{ "id": 1, "name": "Print 10x13", "price": 0.15 }‘ -H "X-API-Key: a6ZbcDefQW12BN56WEI34 " https://{$API_GATEWAY_URL}/prod/products
为了找到id为1的产品,请这样做:
curl -H "X-API-Key: a6ZbcDefQW12BN56WEI34" https://{$API_GATEWAY_URL}/prod/products/1
我发现,使用 Spring Boot 3.4 版本编译出来的工件在部署到 AWS Lambda 上时,其大小比使用 3.2 版本时更大(25.500 KB 对比 22.000 KB)。我猜测所有依赖项更新版本的工件变大了,导致了这种情况。我们知道,工件越大,Lambda 函数的冷启动时间就越长。所以,咱们来看看。
使用Java 21托管运行时和Spring Boot 3.4,通过AWS无服务器Java容器来测量AWS Lambda函数的冷启动和暖启动时间在第 4 部分中介绍的所有衡量 AWS Lambda 性能的技术仍然适用。我们将应用这些技术,包括 SnapStart 以及额外的 DynamoDB 和 API Gateway 请求预调用。
以下实验的结果也是基于超过100次冷启动和大约10万次热启动,使用内存设置为1024 MB的Lambda函数GetProductByIdFunction持续1小时。实验中使用了Java Corretto版本java:21.v27。你可以使用任何你想要的工具,例如Serverless-artillery、Postman。
我还利用了两种不同的Java编译选项来测量Lambda的性能:分层编译(这是Java 21中的默认编译选项,即tiered),和编译选项XX:TieredStopAtLevel=1。对于后者,您需要在Lambda函数的JAVA_TOOL_OPTIONS环境变量中设置以下内容:"-XX:+TieredCompilation -XX:TieredStopAtLevel=1",如所示。
全局设置:
功能:
运行时环境: java21
环境:
环境变量:
JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
切换到全屏 退出全屏
我也希望更好地可视化Lambda SnapStart快照分层缓存策略的效果,展示所有100个冷启动的性能测量,但只展示最后70个较快速的冷启动,忽略初始较慢的30个冷启动。根据Lambda函数的更新频率以及缓存层失效的情况,Lambda函数可能经历数千或数万个冷启动,因此,最初的较慢冷启动不再显著影响整体性能。您可以在Mike Danilov的文章《AWS Lambda Under the Hood》中了解更多关于Lambda SnapStart快照分层缓存策略的效果。在我的文章《AWS SnapStart - Part 17:Java 21在AWS Lambda中的快照分层缓存对冷启动的影响》中也探讨了这一效果。
所以让我们来提供测量结果。缩写 c 代表冷启动,w 代表热启动。
冷启动(c)和暖启动(w)时间(以毫秒为单位),(使用了分层编译):
场景编号 | c p50 | c p75 | c p90 | c p99 | c p99.9 | c max | w p50 | w p75 | w p90 | w p99 | w p99.9 | w max |
---|---|---|---|---|---|---|---|---|---|---|---|---|
未启用 SnapStart 功能 | 6611.74 | 6725.05 | 6799.39 | 6978.40 | 6999.36 | 7003.54 | 6.45 | 7.21 | 8.33 | 20.01 | 49.23 | 1741.84 |
启用 SnapStart 但未应用预热机制,全部 | 2141.30 | 2197.67 | 3511.92 | 3543.66 | 3547.20 | 3549.65 | 6.61 | 7.39 | 8.60 | 19.53 | 53.31 | 2742.84 |
启用 SnapStart 但未应用预热机制,最后 70 次调用 | 2127.51 | 2176.16 | 2236.17 | 2360.17 | 2360.17 | 2360.17 | 6.61 | 7.39 | 8.67 | 19.53 | 49.23 | 1796.23 |
启用 SnapStart 并应用 DynamoDB 调用预热机制,全部 | 925.73 | 966.39 | 2296.48 | 2331.17 | 2342.84 | 2343.82 | 6.40 | 7.16 | 8.53 | 18.92 | 42.99 | 1565.13 |
启用 SnapStart 并应用 DynamoDB 调用预热机制,最后 70 次调用 | 910.57 | 942.65 | 973.39 | 1047.29 | 1047.29 | 1047.29 | 6.30 | 7.10 | 8.39 | 18.47 | 41.64 | 474.53 |
启用 SnapStart 并应用 API Gateway 请求调用预热机制,全部 | 668.31 | 710.32 | 1528.95 | 1555.15 | 1558.26 | 1558.57 | 6.35 | 7.16 | 8.53 | 19.22 | 42.99 | 715.99 |
启用 SnapStart 并应用 API Gateway 请求调用预热机制,最后 70 次调用 | 658.01 | 685.51 | 720.31 | 910.7 | 907.7 | 910.7 | 6.35 | 7.16 | 8.53 | 19.22 | 41.97 | 213.41 |
冷启动 (c) 和热启动 (w) 时间(毫秒),使用 -XX:+TieredCompilation -XX:TieredStopAtLevel=1 编译选项:
场景编号 | c p50 | c p75 | c p90 | c p99 | c p99.9 | c max | w p50 | w p75 | w p90 | w p99 | w p99.9 | w max |
---|---|---|---|---|---|---|---|---|---|---|---|---|
SnapStart未启用 | 6799.39 | 6964.47 | 7083.82 | 7604.78 | 8164.06 | 8165.55 | 6.61 | 7.51 | 8.80 | 20.41 | 62.99 | 1843 |
SnapStart启用但未应用预置,全部 | 2191.09 | 2248.78 | 3755.16 | 3789.09 | 3792.88 | 3793.42 | 6.61 | 7.51 | 8.80 | 20.09 | 52.90 | 2883 |
SnapStart启用但未应用预置,最后70次 | 2151 | 2176.16 | 2202.21 | 2260.85 | 2432.66 | 2432.66 | 6.51 | 7.39 | 8.66 | 20.09 | 46.59 | 1906 |
SnapStart启用并应用了DynamoDB调用预置,全部 | 926.66 | 972.20 | 2716.36 | 2738.16 | 2749.13 | 2749.51 | 6.40 | 7.21 | 8.60 | 20.17 | 76.30 | 1812 |
SnapStart启用并应用了DynamoDB调用预置,最后70次 | 913.04 | 935.13 | 1003.67 | 1453.38 | 1453.38 | 1453.38 | 6.30 | 7.10 | 8.46 | 19.38 | 65.58 | 484 |
SnapStart启用并应用了API Gateway请求调用预置,全部 | 678.40 | 737.82 | 1244.44 | 1267.03 | 1302.99 | 1303.27 | 6.30 | 7.04 | 8.33 | 18.92 | 44.03 | 456.75 |
SnapStart启用并应用了API Gateway请求调用预置,最后70次 | 665.72 | 685.75 | 754.55 | 849.83 | 849.83 | 849.83 | 6.25 | 6.99 | 8.20 | 18.62 | 41.64 | 218.7 |
在这篇文章中,我们将示例应用程序更新为使用Spring Boot 3.4、AWS Serverless Java Container Spring Boot 3版本2.1.0以及其他依赖项的最新版本(截至2024年底)。我们还通过不同的方法和Java编译选项来测试Lambda的性能。我的总体感觉是,分层编译器在减少冷启动时间方面表现更佳。
与仅针对 Spring Boot 3.2 的 -XX:+TieredCompilation -XX:TieredStopAtLevel=1 编译选项的 Lambda 性能测量相比,我们发现 Spring Boot 3.4 的冷启动时间较长(这是由于更大的部署文件大小),但热启动时间较短。值得研究哪些不必要的依赖项是否可以进一步从 pom.xml 中排除,正如我们已经对 spring-boot-starter-logging 和 spring-boot-starter-tomcat 两个依赖项所做的那样,参见以下来自 pom.xml 的代码片段:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
全屏 | 退出全屏
当我们使用 Amazon API Gateway 时,我们的应用程序不需要 Apache Tomcat,这本身就会让部署包的大小增加约 4 到 5 MB。也许还有其他依赖项可以排除,比如 JPA 实现,因为我们使用了 DynamoDB NoSQL 数据库。这样,我们还可以进一步减小 Lambda 部署包的大小,从而缩短冷启动时间。
我们还清楚地注意到Lambda SnapStart快照分层缓存的效果。所以,不要只测量最初的几次冷启动时间,因为这些冷启动确实比较慢,但随着后续调用会明显变快。最初的几次长时间冷启动可能不会显著影响你应用程序的整体性能。
在系列中的下一部分,我们将基于 AWS Lambda Web Adapter 的示例 Spring Boot 应用更新,来使用最新版本的依赖库。
如果你喜欢我写的文章内容,请支持我,关注我的GitHub账户,并给我仓库点个星。
OOP:面向对象编程
CRUD:创建、读取、更新和删除
JVM:Java虚拟机
SUT:系统被测