InfoQ

Presentation: Practical Performance Tuning for Serverless Java on AWS

8.5内容质量
Presentation: Practical Performance Tuning for Serverless Java on AWS

TL;DR · AI 摘要

AWS Lambda上Java性能调优需关注冷启动和内存占用问题,SnapStart和GraalVM是关键方案。

核心要点

  • AWS SnapStart通过预快照优化冷启动问题,适合需要快速响应的应用。
  • GraalVM的AOT编译可减少内存占用,但需权衡编译时间和兼容性。
  • Java 25和Project Leyden对Serverless架构有重要影响,需关注其最新特性。

结构提纲

按章节快速跳转。

  1. AWS Hero Vadym Kazulkin介绍Java在AWS Lambda上的性能调优挑战。

  2. Java在AWS Lambda上面临冷启动和内存占用的挑战,影响性能。

  3. AWS SnapStart通过预快照优化冷启动问题,提升应用响应速度。

  4. ·GraalVM AOT编译

    GraalVM的AOT编译减少内存占用,但需权衡编译时间和兼容性。

  5. Java 25和Project Leyden对Serverless架构有重要影响,需关注其最新特性。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • AWS Lambda Java性能调优
    • 冷启动与内存问题
      • AWS SnapStart
      • GraalVM AOT编译
    • Java 25与Project Leyden

金句 / Highlights

值得收藏与分享的关键句。

#AWS Lambda#Java#Serverless#性能优化
打开原文

AWS 上无服务器 Java 的实用性能调优 - InfoQ

InfoQ 首页 演讲 实用性能调优:AWS 上无服务器 Java 的实践

Java

AI 分析时代重新思考日志(网络研讨会 7 月 9 日)

实用性能调优:AWS 上无服务器 Java 的实践

Like

新下拉阅读列表

  • 阅读列表

查看演讲

  • 垂直
  • 水平
  • 全屏

速度:

  • 1x
  • 1.25x
  • 1.5x
  • 2x

49:16

总结

AWS 专家 Vadym Kazulkin 解释了如何克服 Java 在 AWS Lambda 上的企业障碍:冷启动和内存占用。他深入探讨了性能调优的技术细节,将完全托管的 AWS SnapStart(带有预快照准备钩子)与 GraalVM 提前编译进行比较,同时探讨了 Project Leyden 和 Java 25 的最新架构影响。

个人简介

Vadym Kazulkin 是 AWS 无服务器专家,也是 ip.labs GmbH 的开发负责人。他目前的关注点和兴趣包括在 AWS 云中设计和实现高可扩展性和高可用性的应用程序,特别热衷于无服务器架构。Vadym 还是 Java 用户组波恩聚会的联合组织者,并经常在各种聚会和会议上发表演讲。

关于会议

InfoQ Dev Summit 慕尼黑软件开发会议聚焦于高级开发团队目前面临的软件挑战。从 20 多位高级软件开发人员那里获得宝贵的现实技术见解,与演讲者和同行交流,并享受社交活动。

INFOQ 活动

  • 2026 年 6 月 25 日,上午 1 点 EDT 自动化可靠性架构:将 AI 嵌入您的可观测性堆栈 演讲者:Justin Griffin - NeuBird AI 产品负责人
  • 2026 年 7 月 9 日,上午 12 点 EDT AI 分析时代重新思考日志 演讲者:Nicolas Jung - Datadog 日志产品经理
  • 2026 年 7 月 16 日,上午 1 点 EDT 代理时代工程:如何规范、构建、测试和运营 AI 驱动的系统 演讲者:Juveria Kanodia - Harness 工程高级总监

演讲稿

Vadym Kazulkin:我的名字是 Vadym。无服务器架构和 Java 都是我的热情所在。我是 Java 用户组伯尔尼的组织者之一,我目前就住在那里。此外,我还是 AWS 专家。我经常就各种主题发表演讲和撰写博客,这只是其中之一。基本上,如果我们谈论 Java 的总体流行度,有很多来源试图以不同的方式衡量,比如职位发布的数量、包含 Java 代码的 GitHub 仓库数量、Stack Overflow 的问题数量。无论你选择什么,Java 几乎都是最受欢迎的编程语言之一。也许不是第一,但肯定是第二或第三。如果我们只关注 AWS 上的无服务器 Java 世界,目前我们看到,基本上在 AWS Lambda 上,亚马逊有自己的 Java 发行版,Amazon Corretto,他们对安全补丁的支持超过了长期支持生命周期,这是好事,但也许不是因为亚马逊将支持 Java 8 直到 2030 年。

这只是为了帮助那些难以跨越这座山的人。发生了什么?他们只支持 Lambda 的长期版本,而且在 Java 21 发布两个月后才开始支持 Java 21,因此我们现在距离 Java 25 发布已经过去三周了,所以希望在接下来的一个月或两个月内,Lambda 能够支持 Java 25。这不是由我决定的。目前,我们从 Java 8 到 Java 21 都有支持,这已经很好了。Java 25 也将会非常出色。Java 无疑是一种非常快速且成熟的编程语言,它在开源项目、Spring、Quarkus,甚至现在的 AI 项目中都有庞大的社区。这真的令人惊叹。如果你看看 Java 的支持情况,有多少人在使用 Java 在无服务器架构上编写带有业务逻辑的 Lambda 函数,我们目前有两项服务可以与之相关。

这两项服务都稍微有些过时了。我预计下一项将在今年推出,但你看到 Java 在 AWS Lambda 上的采用率大约是单数百分比。大多数 Lambda 函数是用 Python、Node.js 编写的,而 Java 落后了。我假设现在情况已经有所改善,但仍然存在差距。当然,问题是,为什么?因为 Java 在企业中的采用情况是完全不同的。

使用 Java 的无服务器架构 - 挑战

在这里,我们将主要讨论两个方面。我们将讨论所谓的冷启动时间或延迟启动时间,以及 AWS Lambda 上的内存占用情况,这是 AWS Lambda 的一个成本因素。让我们从一些基本且简单的内容开始,然后看看如何继续前进。这是我们工作中使用的应用程序的一个非常简化的版本,它有 200 个 Lambda 函数。我将其简化为两个。我们基本上有一个用于管理 API 的 API 网关,产品 API,为了简单起见,只有两个 Lambda 函数用于创建产品和通过 ID 获取产品。我将使用完全无服务器的数据库,DynamoDB。如果使用像管理的 Postgres 这样的东西,我会说一些话,但我在工作的公司更倾向于尽可能保持无服务器。

在使用关系型数据库时,有一些挑战我也将提到。基本上,如果请求通过 API 网关(即管理的 API)进入,那么最终 Lambda 接收到的是 JSON 格式的正文。你在这里会看到 JSON 的结构。基本上,你将看到所有查询参数、路径参数、如果有的话,POST 请求的正文、头部,所有这些内容基本上会以 JSON 的形式出现,并且大致会被反序列化为这种类型的内容。这就是你如何编写 Lambda 函数的方式。这就是我之前展示的 GetProductById,它基本上实现了 RequestHandler 接口,具有输入和输出,并且我们只有一个方法,即 handleRequest,它会将这个 JSON 反序列化为 API 网关代理请求事件,因为触发器是 API 网关。

还有其他触发器。基本上,从这个请求事件中,我们可以获取参数 ID,然后发送它,并与 DynamoDB 通信以获取具有该 ID 的产品。这就是你如何编写这样的 Lambda 函数,以及这个 JSON 将如何被简化为 API 网关代理请求事件,输出是代理响应事件,其中基本上放入 HTTP 状态码信息,然后将产品作为结果输出进行序列化。

冷启动

现在我们来谈谈最大的挑战,这个问题仍然存在,但我们将看到如何应对,这就是所谓的“冷启动”问题。首先,什么是冷启动?可能并不是每个人都熟悉这个概念。Lambda 实现的是所谓的函数即服务(Function as a Service)范式,这与我们所熟知的 Tomcat 等方式有很大不同。函数即服务的执行环境目前仅能处理单个请求。它不像 Tomcat 那样可以水平扩展。我们可以发送多个请求,如果无法处理,就需要水平扩展。一个 Lambda 环境只能处理一个请求。这意味着什么?基本上意味着我们需要启动这个环境,如果环境不足,或者环境正在被使用,它就会进入环境池。

如果环境不足,就需要启动其他环境。这让我想起数据库连接如何进入连接池。连接被使用后,会放回池中。当其他人需要连接时,就会从池中获取。当然,这还涉及到池的大小等其他因素。问题是,当我们需要新的池时,或者 AWS 负责启动新的池,例如当 Lambda 函数第一次被调用时。这可能发生在你为新函数编写了函数,或者更新了现有函数的情况下。这需要重新部署,当然,这也意味着新的代码。所有环境都需要被销毁。基本上,我们需要一个新的环境来运行这个新代码。可能你并没有修改代码,但你同时有五个请求,此时有五个执行环境,现在你有十个请求。

这时我们需要并行启动五个新的环境,因为原来的五个无法处理这些请求,而 AWS 不希望出现延迟,它们不会将请求排队。它们会尝试启动更多的环境,以增加并发处理能力。还有一种情况是,AWS 会销毁现有的环境。这可能是出于节省成本的原因。例如,它们启动了十个环境,因为你当时需要十个,但五分钟后你又只剩下五个请求。这时,它们会随着时间的推移关闭另外五个环境,因为这是它们的成本。它们不会对我们未使用的资源收费,但运行环境本身也有成本。它们会关闭这些环境。还有一种情况是,它们需要对这些环境进行补丁更新,以应对安全风险等。它们不会明确说明何时发生这种情况,但根据我的经验,没有任何一个环境能存活超过几个小时。

他们将关闭这些实例,仅仅是为了防止安全攻击等。这些工作者并没有一个固定的池。随着时间的推移,它们都会消失,因此我们需要启动它们。启动意味着什么?这个过程的流程是怎样的?首先,如果我们部署一个 Lambda 函数,无论它是 Java 还是其他语言,函数代码都会被上传到 Amazon 的 S3。如果我们启动环境,首先需要下载这段代码。AWS 有自己的技术,比如 Firecracker microVM,这基本上是一个执行环境,他们对其进行管理。你可以将其想象为管理 Kubernetes 环境。这是一种执行环境。我们不需要了解其细节。基本上,如果我们选择 Java,那么这个执行环境就需要启动 Java 运行时,也就是我们选择的 Corretto 版本。

接下来,他们会执行静态初始化代码块。这意味着 Lambda 函数中静态初始化块中的所有内容,以及从该块中可访问到的所有内容。例如,我在这里初始化了 ProductDao,它连接的是 DynamoDB,它本身也有初始化过程,因此所有内容都会从这个静态初始化块中加载。这意味着类加载、运行时依赖注入、即时编译可能会被触发。我在这里使用的是纯 Java,如果你使用的是 Spring,那么会进行注解处理。所有在静态阶段可访问的内容都会被执行,最后一步是调用处理方法。处理方法就是函数代码本身,即函数处理请求。前三步通常被称为冷启动,或启动时间、应用程序的启动时间。

最后一步,AWS 称其为热启动,但基本上只是函数的执行。因为在 Java 领域,人们通常在 Java 达到峰值性能时称之为热启动,比如即时编译是否生效。但这并不是热启动的定义,它只是关于函数本身。热启动基本上只是函数的执行,当有热容器存在时就会发生。此时,只有最后一步会被执行。如果需要启动容器,那么前三步都需要被执行,这就是冷启动。现在我们来看看冷启动在性能方面意味着什么。基本上,我从一些基础开始,这在大多数情况下是足够使用的,可以作为起点。我给 Lambda 函数分配了 1 GB 的内存。

可能有人知道,在 AWS 中,你只能给 Lambda 函数分配内存,而 CPU 是根据内存推导出来的。你不能直接分配 CPU,只能分配内存,而且内存越多,CPU 也越多。使用 1 GB 内存,你将获得一半的虚拟 CPU,这在大多数情况下是足够的。x86 架构是默认的,也有 ARM。为了与 DynamoDB 通信,我们需要一个 HTTP 客户端。如果我们不做任何处理,默认使用的是 Apache。这个应用程序的大小是 14 兆字节,这是我测量的结果。我们使用了某些适合无服务器环境的 Java 编译选项,比如我们禁用了分层编译,使用了基本的停止编译,也就是第一级别。我之后会再详细说明这一点。

我做了一个类似压力测试的测试,持续了一个小时。我只是发送了一定数量的请求,然后等待了8到9分钟,再开始下一批请求。基本上,在这一个小时中,我经历了100次冷启动和100,000次热启动。稍后你们会看到冷启动和热启动的图表。这是一个刚刚部署的新应用,它也起到了一定作用。现在让我们来看看结果如何。先从积极的一面开始,这是热启动,你看到的数值是以毫秒为单位的。基本上,如果你忽略这个最大值(我会解释为什么它如此大),你会发现Java是一种非常快速的编程语言。我总是关注p90,也就是90%的情况下,数值都是这样的,或者稍微低一点。

从DynamoDB获取一个项目,这当然是一个非常简单的Lambda函数,但7毫秒已经是一个不错的成绩了。它非常快,你几乎不会注意到。如果你查看冷启动的情况,当发生冷启动时,你会看到这些数值,对于Java来说,冷启动时间大约是3秒。首先,也许现在我们回顾一下,你看到冷启动发生的次数不到0.1%。这是一个好消息,说明冷启动并不频繁发生。你不会经常受到这些3秒冷启动的影响,但我只是测试了一个Lambda函数。你的应用可能需要使用多个Lambda函数来加载页面,如果只有一个Lambda函数发生冷启动,而你需要整个链路,那么概率就会增加。

一般来说,冷启动的发生率在2%到3%之间是正常的。如果你有2%或3%的冷启动,每次冷启动需要3秒,你可能知道Google的研究,他们指出,如果某个功能的响应时间超过1秒,用户可能会流失,尤其是登录功能。这种情况可能发生在2%到3%的时间里。这会对业务产生影响,或者可能不会。如果是同步应用,你可能并不在意,但如果是面向公众的应用,你就会在意。

减少冷启动时间的选项

让我们谈谈优化技术。我将介绍其中的两种:SnapStart和GraalVM。SnapStart是AWS自己的技术。我们先从它开始。基本上,SnapStart的目标是AWS看到了Java的采用情况,他们看到了冷启动的原因,于是说,我们需要做点什么。我们正在失去市场份额。Java开发者不使用无服务器架构,或者使用得不频繁,这是一个挑战。是的,他们开发了SnapStart,目标是提高启动性能。它是完全托管的。我会向你展示这意味着什么。它从Java运行时开始。它从Java 11版本开始可用,但他们在去年年底又增加了Python和.NET。Python显然有其原因,用于加载LLMs并减少启动时间,而.NET基本上也存在同样的问题。它只适用于托管运行时,如Java、Python和.NET。

你也可以在 AWS Lambda 和运行时上部署 Docker 容器镜像。这将是我们部署 GraalVM 的方式。但这些技术对其他人来说并不适用。你无法改进这一点。什么是 SnapStart?人们知道 CRaC 和 CRI-O 技术吗?这些技术对你来说意味着什么?目前有一些技术可以将容器移植,比如 CRI-O。这些技术早在 12、13 年前就已经存在,当时只是为了将一个 Docker 容器中的应用程序移植到另一个容器中。CRaC 是用于恢复 JVM 的技术。AWS 基本上是自己开发了类似的技术。发生了什么?他们将 Lambda 的部署流程分成了两个阶段。首先,在没有启用 SnapStart 的情况下,部署时什么都不会发生。我们只是将 Lambda 函数部署到调用之前,什么都不会发生。如果我们为 Lambda 函数启用了 SnapStart,就会有额外的步骤。在部署阶段,AWS 会启动一个 Java 容器,下载代码并执行应用程序的冷启动阶段。他们会下载代码,启动 JVM,并加载静态初始化块。当然,他们无法执行你的 Lambda 函数,因为他们没有有效载荷,不知道你想要什么,比如 ID 为 1、2、3 的产品,但他们可以在这个冷启动阶段进行到这一步,目标是创建应用程序的快照。快照包含所有内容,只需加载 JVM,静态加载类,以及所有这些内容。快照是微虚拟机的完整快照。它包含所有内容:作为操作系统的 Linux、JVM,以及到那时为止从你的代码中加载的所有内容。在调用阶段,如果你启用了 SnapStart,你将体验到这个快照的恢复。快照将被恢复,然后执行流程将恢复继续。

这里的承诺是,恢复阶段比冷启动更快。否则,这完全没有意义。你可以测量并决定是否适用于你的操作。他们认为,这个快照是完整的微虚拟机快照,会更快,他们会测量是否属实。基本上,你只需要进入 Lambda 函数,决定是否启用 SnapStart。这基本上是一个真或假的选项。它只是在基础设施即代码中多了一行,而已。这是一个标志,由你决定。在展示测量结果之前,我想再介绍一种技术,称为预热(priming),你可以使用 SnapStart 来实现。再次强调,我们展示了 AWS 基本上启动容器,加载这个处理类,但不会执行函数。

有一种可能,尽可能多地加载类或预先初始化一些内容,使它们成为快照的一部分。尽可能多地将这些内容包含在快照中,因为恢复会更高效。问题是,我们将展示 Java 是如何部分地、懒加载地加载类的。有些事情是常识,比如我们能做什么,特别是当我们谈到像 DynamoDB 这样的东西时。再次强调,DynamoDB 调用时,背后有一个 HTTP 客户端。默认的是 Apache。如果你只是打开你的编辑器并测量初始化一次 HTTP 客户端所需的时间,你会发现需要大约半秒钟。因为 Apache 会执行大量的实例化、缓存等操作,Apache 更适合像 Tomcat 这样的长期运行的应用程序。有机会预先初始化这个 Apache 客户端,使预初始化成为快照的一部分。

与 DynamoDB 进行交互时,同样需要使用 JSON Marshaller 或 Unmarshaller,因为你处理的是 Java 对象,而 DynamoDB 是 NoSQL 数据库。根据你是在写入还是读取数据,你需要将 Java 对象序列化为 JSON 或反序列化为 Java 对象。默认的序列化器是 Jackson。如果你在 IDE 中第一次执行 new ObjectMapper,执行时间会根据你的 CPU 性能,大约在 300 到 500 毫秒之间。如果你第二次执行,只需要 1 毫秒。如果你查看下面的代码,你会发现有很多单例对象,它们只会在生命周期的第一次加载时被初始化。一个重要的问题是,我们能否提前初始化这些对象?如果我们能够提前初始化,我们该如何实现?你看到这些橙色的框,它们提供了通过钩子实现提前初始化的可能性。我们暂时忽略这个 post-snapshot 钩子,我不会使用它,

但这个 pre-snapshot 钩子非常重要。这是额外的一个方法。我将向你展示如何实现它。在这里,我们可以添加额外的逻辑,提前初始化将成为快照一部分的内容。预初始化就是这个意思。我们假装提前初始化这些内容。这可能不是很方便,但这是加快速度的一种好方法。再重申一次,你看到的是 warm start,即使在 warm start 的情况下,最大值也是一秒半,我之前展示过,即使在 warm start 的情况下,也需要 1500 毫秒。现在我将解释原因。即使我们执行函数本身,即 handler,getProductById,并且它第一次请求 DynamoDB,Java 会在第一次需要时加载这些类。例如,这个 getProductById 在 DynamoDB ProductDao 中,它将初始化 getItemResponse。

这是与 DynamoDB 交互的方式。即使在 warm 执行阶段,这条链中的所有类都会在第一次被初始化。因为在静态块中,我们只初始化 DynamoDB 类,所以只有静态初始化块中的类会被加载。这个 getProductById,所有这些逻辑,包括 ObjectMapper,我们只在与 DAO 交互时使用,这些都会在 warm start 时间发生。即使我们看到 warm start 阶段,第一次执行可能仍然很慢,因为需要加载类。如何处理这个问题?基本上,你需要添加一个额外的依赖项,即这个 org-crac。这个 org-crac 提供了实现资源接口的可能性,这个接口来自 org-crac。基本上,你需要通过复制这一行代码,将这个 Lambda 函数或其他 Lambda 函数注册为 CRaC 资源,

GetCore,getGlobalContext,注册这个。你只需复制这一行。最重要的方法是在 checkpoint 之前,它来自你需要实现的资源接口。你可以在该方法中执行加载操作,它将成为快照的一部分。我在这里所做的,基本上是执行 ProductDao,getProductById,ID 为 0。我不关心结果。通过执行这个操作,HTTP 客户端将被加载,这是与 DynamoDB 通信所必需的,JSON Marshaller 也将被加载,用于将 JSON 序列化为 Java 对象,即这个产品。这就够了。我不关心结果。我已经预加载了这些内容。类已经被加载。我初始化的内容将成为快照的一部分。恢复后,我不会执行这个操作。好消息是,使用 SnapStart 与 DynamoDB 通信涉及一个 HTTP 连接。

它将为您自动恢复,您不需要编写任何代码。即使在快照和恢复过程中,它也能保持运行。就像您通过 JDBC 与 Postgres 通信一样,它同样可以存活下来。您不需要编写任何代码。这就是 SnapStart 的魔力。当然,这需要一些常识,有时您可能不会使用 DynamoDB。您需要思考,应该预加载什么?如果您做的是类似 A 加 B 的操作,可能无法预加载任何内容。在这种情况下,您应该预加载什么?这很简单。您可能根本不需要 SnapStart,也不会出现冷启动。AWS 提供了一份 SnapStart 预加载指南,他们描述了您可以采取的措施。您能做的最好的事情是尽可能预加载那些将被延迟加载、不属于静态初始化块的类。这可能会节省一些资源。使用 DynamoDB 时,情况会更复杂一些。

让我们来看一下测量结果。在左侧,这是我们的观察结果。这是冷启动,耗时 3 到 4 秒。这是百分位数的统计。第二个是启用了 SnapStart 但没有预加载的情况。只需勾选一个复选框,表示我想启用 SnapStart。第三个是启用了 SnapStart 并进行了预加载的情况。在这种情况下,这个产品,getProductById,只需要一行代码。您看到的,我总是关注这个黄色的 p90。这已经足够好了。仅仅通过激活 SnapStart,这基本上只是一个复选框,冷启动时间从 3.2 秒下降到 2 秒。大约减少了 40%。通过进行预加载,您可以看到冷启动时间降至 1 秒。从缓存中恢复通常更快,这是很常见的。至少在这一场景中,它不需要太多代码。

这是暖启动,只到 p99。SnapStart 本质上并不是用于提升暖启动执行时间的技术。您看到的,最大值现在非常低,因为我已经预初始化了所有内容。我预初始化了一切。我预加载了类。即使暖启动的最大值也大幅下降。我们基本上可以通过伪造 getProductById 而不使用 SnapStart 和静态初始化块来实现相同的效果。我们也可以这样做。我们只能在暖启动时达到相同的效果。对于冷启动,我们需要像 SnapStart 这样的技术。

Lambda 性能调优方法

这就是我向您展示的内容。有趣的是,我们使用的是这种默认配置。还有可能对这些内容进行调优。我将简要地向您展示应该怎么做,应该测试什么。我们为 Lambda 分配了 1GB 的内存。您可以进行调整。您可以分配更多或更少的内存,并查看冷启动的表现。更少的内存可能意味着成本更低,因为内存是成本因素之一,但您将获得更少的 CPU。您需要进行一些尝试。多少内存是足够的?您可以分配更多内存,基本上最多到 1.8GB,因为在此之后,您将获得第二个核心,如果应用程序没有从中受益,那么这并不重要。然后您可以分配更多内存。这可能会增加成本,但性能会更好。这就是您需要测试的内容。

我向你展示了这个大编译选项,设置为在级别 1 停止。为什么这是个好的选择?因为 Lambda 函数生命周期很短,而 Java 默认使用分层编译,基本上要等到函数执行了 10,000 次才会通过 JIT 进行优化。这在 Lambda 函数中不会发生。容器会一直运行,直到优化开始。即时编译器需要线程来进行优化,这会占用你的 CPU。因此,通过使用另一个 Java 编译选项,我们不希望进行过于激进的优化,这释放了 CPU,效果更好。HTTP 客户端实现不仅限于 Apache。AWS 也有自己的原生实现。你可以进行测试。还有 URL 连接、HTTP 客户端,不仅限于 Apache 的实现。

根据我的经验,如果你使用预加载,那么这些都不再重要,因为一切都会被预先加载。如果你能够进行预加载,那么结果会非常接近。架构方面,你可以测试 ARM 是否更好。如果你选择 ARM 架构,ARM 更便宜,但我在使用它时遇到了更大的冷启动问题。这仍然是一个性价比的问题。它更便宜,但性能会下降,等等。或者你可能需要考虑你自己的应用程序。有没有预加载技巧等等?

最佳实践和建议

无论如何,还要注意你的应用程序的大小,特别是如果你使用像 Tomcat 这样的东西,我们推送依赖项,因为它只启动一次,可能持续几周。在无服务器世界中,它并不像这样工作。我们有这样一个应用程序,中等大小,14 兆字节。这是我们测试过的。我编写了一个应用程序,是“Hello World”。没有人使用它,它没有任何依赖项。大小是 130 千字节。我们的应用程序是 14 兆字节,而我只是编写了一个使用追踪等功能的应用程序。它的大小是 50 兆字节。你可以在这里看到冷启动时间的差异,即使使用了 SnapStart 和预加载,大小仍然很重要。大小越大,冷启动时间越长,因为有更多的依赖项,它们需要快照和恢复,这总是需要更多时间。

注意你的依赖项。如果是测试依赖项,就不要包含它,因为它会被加载。它需要从 S3 下载等。真的只放你需要的东西,其他什么都不要放,否则你会受到惩罚。这里还有另一件事。我测量了性能,我向你展示的是所有 100 次执行的结果。我只是重新部署了应用程序。关键在于,这个快照是如何工作的。AWS 将其存储在所谓的分层低延迟缓存中的三个可用性区域中。我们不需要关心它,但看到这些信息很有趣。这是分块缓存,所以他们将整个快照分成 512 千字节的块进行存储。当然,如果你第一次部署应用程序,缓存是空的。

问题是,他们试图弄清楚如何为你优化快照,因为这也会增加他们的成本。例如,如果你频繁调用 Lambda 函数,这个缓存将部署在 Lambda 实例上。如果你的 Lambda 函数执行频率较低,那么会有多个缓存实例,但距离会增加。最后的真相来源是 S3。如果你有一个较少被调用的函数,它将存储在 S3 上。当然,这里的逻辑是,缓存越接近实例,恢复速度就越快。事情就是这样。和内存一样,你可能知道 L1、L3、RAM,它们本质上都是缓存。越接近核心,性能越好。你基本上无法控制这些,事情就是这样。

当然,执行次数越多,函数代码出现在 L1 缓存中的可能性就越大。AWS 还会跟踪执行路径。如果你的 Lambda 函数中包含 if 语句,他们会根据你的请求负载,尝试预加载相应的内容。例如,如果 ID 小于 10,就执行这个操作;如果 ID 大于 10,就执行另一个操作。他们会监控这些情况,并尝试异步为你预加载这些代码块,因为通常你不需要一开始就加载整个代码块。这就是整个“魔法”所在。你甚至不应该意识到这一点。重要的是,有时你可能会因为 Lambda 函数的不同而得到不同的结果,这很重要。

影响这一点的因素是执行频率和镜像的大小。一个函数可能有更多的依赖项,因此镜像的大小更大,因此它的性能可能会稍差一些。你无法配置这些,但你需要知道的是,较小的函数和执行频率更高的函数会更受青睐。事情就是这样。是的,访问频率较低的函数会进入 L2 缓存,甚至 S3。这就是我之前说的。我写了一系列文章。我会提供我幻灯片的链接,你可以阅读相关内容。我推荐 Mike Danilov 在 InfoQ 旧金山举办的演讲,他现在是前工程师,演讲题目是《AWS Lambda under the Hood》。他不仅解释了 Lambda 的工作原理,还解释了 SnapStart 的工作原理。这值得专门做一个演讲,特别是关于缓存如何工作的部分。再次强调,了解这一点是有益的,你无法配置任何东西。

这个缓存最重要的特性是,随着时间的推移,它会变得越来越快。当你开始执行 Lambda 函数时,它们会将函数的代码块放入缓存中,而缓存一开始是空的。它们会重新排列这些内容,因此你执行得越频繁,缓存填充得越多,速度也会越快。在这里,我比较了重新部署后所有 100 次执行,只与最后 70 次进行比较。我排除了前 30 次执行,因此这个比较只在你激活了 SnapStart 时才有效。你在这里看到的是,没有预加载的 SnapStart 对所有 100 次执行的比较。这是没有预加载的 SnapStart 对最后 70 次执行的比较,而这是预加载的 SnapStart 的比较。

如果你仔细看这里,如果你可以进行预热,你会发现预热 p90 的全部 100 次执行时间为 1.2 秒,而这里只有 650 毫秒。基本上,它说明的是,最后 70 次执行更快,而前 30 次稍微慢一些。现在问题是,你的 Lambda 函数能存活多久?你多久重新部署一次?如果你几周都不去碰它,你可能会经历多达 10,000 次甚至 100,000 次冷启动。前 30 次虽然慢,但只是稍微慢一点,它们并不算数。这可能甚至是一个更好的衡量方式。这个信息是,不要在几次测量后就停止使用 SnapStart 和预热。继续测量直到结束。你将看到缓存带来的好处。当然,从某个点开始,缓存满了之后,速度也不会再提升。但基本上,最后 70 次执行,p90 是 700 毫秒,冷启动时间是可以接受的。对于 JavaScript 和 Python,你可能会看到大约 300 毫秒到 400 毫秒的冷启动时间。在这里,我们可以达到 700 毫秒,但不是比 3 秒多出那么多。这一点很重要。

AWS Lambda Profiler Extension for Java

你可以查看你的 Lambda 函数中发生了什么。AWS 发布了异步分析器用于此目的。我写过一篇文章,现在我将展示给大家。这是一个所谓的分析器扩展,但它是基于开源的异步分析器。你可以查看,是否可以进行优化?这些是火焰图,你可以看到时间都花在哪里了。什么是在什么时候加载的?如果你看到这些,你可以进行预加载和预热这些内容。你可以“游戏”一下这个机制。这就是我在这个分析器发布后所做的。我问自己,我是否可以在这里进行预热?因为以前我对此没有影响,也没有控制权。AWS 将有效载荷序列化为这个对象,但我是否可以做些什么?这个火焰图帮助了我,因为通过它我看到了在我处理程序执行之前发生了什么。

所有内容都是公开的,但只有这个 Lambda 序列化器 serializeFor 是公开的库,我才能对它做些事情。我扩展了我的代码。这涉及很多代码。我并不是说你必须使用它,但基本上,我使用了这个 serializeFor。我看到了他们做了什么,他们做了很多工作。我只是预热了它。我在检查点之前使用了它,进行了预热。我模拟了一个最小的 API 网关请求,只设置了参数 ID 等于 0,然后进行了预热。我再次将冷启动时间提升了 20%。当然,这又需要写更多的代码,这取决于你。你是否愿意编写这些自定义代码?当然,如果你有类似“put product”的操作,你可以在这里设置 post,并发送请求体,你可以做任何事情。这需要更多的额外代码,但这个火焰图帮助了我很多。甚至因为这个,我实现了 25% 的性能提升。

AWS SnapStart Pricing

定价。问题是,当 Java SnapStart 发布时,它是免费的,但 AWS 认识到每个人都会在 staging 环境中为每个函数使用 SnapStart,而这是他们的存储和 CPU 资源用于恢复。随后,他们将其扩展到 Python 和 .NET,并为缓存和恢复阶段引入了收费。目前 Java 仍然是免费的,但预计 AWS 可能会改变这一政策。他们不想激怒那些一直免费使用 Java SnapStart 的开发者,但可能会观察到并表示,由于这是他们的资源,如果有人滥用,价格可能会统一。再次强调,你需要考虑唯一性,因为从快照恢复时,不要在静态初始化块中使用像当前时间毫秒这样的东西,因为这样恢复时间就会和你代码中的时间一致。

同样,不要使用基于时间的缓存,因为你无法确定何时会被恢复。当快照恢复时,缓存可能为空。请保存 8 小时的数据,如果快照在 8 小时内恢复,你将拥有这些值,否则将没有。不要依赖这一点。要思考如何缓解这个问题,基本上,不要使用这种缓存。还有其他类似的缓存方式。

AWS SnapStart,挑战与限制

存在一些限制。最重要的一点是,当你使用 SnapStart 部署 Lambda 函数时,会创建一个快照。它需要被安全地存储在所有可用区中,创建一个 Lambda 函数的快照需要大约两到两分半钟。这会影响开发者的体验,这是权衡的代价。如果你并行部署多个函数,快照也会并行创建。部署包含 10 个 Lambda 函数的应用程序不会受到惩罚,但需要额外的两到两分半钟。如果你希望快速测试,可能不要在 staging 环境中这样做。这不是性能测试的问题,这是基本的限制。如果 14 天内没有调用 Lambda 函数,快照将被删除。他们节省了存储成本。他们不希望这样。如果他们引入收费,他们将为 Java 开发者移除这一限制,因为那时是否付费将取决于你。目前,他们保护了自己,因为每个人都激活了 staging 环境中的功能,然后他们不做任何测试,AWS 来支付。当然,这不是这样做的原因。

GraalVM

现在让我们与 GraalVM 进行比较。这里重要的是,他们提供了提前编译(AOT),即封闭世界,构建一个包含所有可达内容的原生镜像,包括方法、类、字段等一切内容。AOT 对于无服务器环境非常有用,比如启动速度快、内存占用低、打包体积小,因为只打包可达的内容。这些是无服务器世界的基本特性。AOT 是非常有用的技术。当然,原生可执行文件体积更小,不需要太多内存,因为没有即时编译器。它是原生镜像。其承诺是,通过使用原生镜像,你可以同时优化启动时间和内存使用。AWS 并不提供托管的 GraalVM,但你可以部署自定义运行时。通过自定义运行时,你可以部署一个 GraalVM 镜像,基本上是一个包含原生镜像的 ZIP 文件。有一个格式要求,你需要将其命名为 function_zip,然后启动文件应为 GraalVM 原生镜像。

再一次强调,这不是关于如何构建它的演讲。你可以参考其他资料。这是用于比较的。我现在正在测试 GraalVM 25。你在这里看到的是 SnapStart,我们之前提到的 GraalVM 23 Native Image。你看到冷启动时间甚至更少,但这些不是最后的 70 个,而是全部 100 个。你在这里还可以看到可能性,即可预测的本地冷启动。由于提到的特性,它甚至提供了缓慢的热启动。这是一种有效的技术,而且还有最大值,p99.9。真的很酷,但你需要确认你的依赖项是否已准备好使用 GraalVM。GraalVM 提供了这样的信息,他们发布了一页面,你可以查看,例如我的框架,如 Quarkus、Helidon、Spring、Micronaut,它们都在其中,但它们也会发布依赖项,因为有时候你使用某些依赖项时,它们又会使用其他依赖项,这些依赖项也必须准备好使用 GraalVM。

为什么?因为你无法开箱即用地进行任何运行时实例化。这是一个封闭的世界,原生镜像。如果你没有将类放入原生镜像中,它将无法被找到。你无法像加载类那样做任何事情。有一种可能性是提前声明这些类,然后它们将被打包。这些类只能在反射之前被访问到。例如,Lambda 函数本身,它将被 AWS 动态实例化。他们查看基础设施即代码,看到这个类,然后实例化它,其他东西也是如此。如果你使用 GraalVM,你需要一些训练来找到所有这些类。否则,你将遇到运行时异常,如 ClassNotFound。这在使用日志记录器时尤其痛苦。你需要声明所有这些内容。我在这里使用的是 SLF4J。这只是额外的声明。有时候你只需要做一次,但还是很痛苦。

幸运的是,使用 Log4j 的人,从几个月前开始,GraalVM 已经支持版本 2.25.0。在此之前,几乎不可能声明所有内容。我仅仅在 3 小时后就放弃了。现在,GraalVM 为你带来了所有这些内容。最终,你可以做到这一点。有一种可能性是使用 Assistant GraalVM Tracing Agent。你需要将这个运行注入到你的测试运行中,他们通过观察你的测试类执行情况,找出你的应用程序在运行时将执行的内容,并为你生成这些代码。有时候仍然会有一些遗漏。如果你的测试不完整,没有经过这些路径,你仍然可能会遗漏一些内容。GraalVM 是一种强大的技术,具有巨大的潜力。它确实改善了冷启动和内存占用。再一次强调,你需要管理构建 GraalVM 原生镜像的流水线。这是你的工作。

这就是我喜欢 SnapStart 的原因,它是托管的。你不需要管理这些,只需进行快照,存储快照,然后恢复。你不需要管理这些。在这里,构建它的流水线则落在你的肩上。在我的示例中,它需要一个 Lambda 函数,6GB 的内存来构建这样的原生镜像,而且还需要最多 3 分钟。你可能在云中使用 CI/CD 并且它会大规模扩展,但无论如何这仍然是你的成本。开发者体验方面,3 分钟仍然存在。它与 SnapStart 的 2 分半钟相当。事情就是这样。你需要小心运行时错误。你可能更新了一个依赖项,它引入了一个不再支持 GraalVM 的依赖项,从而导致代码崩溃。你需要仔细关注你部署的内容。

总结和建议

这些基本上是我的建议。两者都是有效的技术,但如果你愿意接受稍微高一点的冷启动和热启动时间,我可能会以 SnapStart 作为起点,因为它是一个完全托管的服务。如果对每毫秒都斤斤计较,那就选择 GraalVM。这也没问题。你可能需要多做一些工作,有一些边缘情况。或者至少,这是我在 9 月 17 日之前的建议,我会解释原因。再次强调,Java 有 Powertools 这个库。它提供了一些辅助方法,用于处理幂等性、日志记录和追踪,但这是一个非常庞大的库。请测量冷启动和热启动时间。我之后会发布一系列关于这方面的文章。如果你使用它,那只是额外增加几个兆字节。这是 Java 25 发布当天发生的事情。GraalVM 宣布了与 Java 生态系统列车的分离,

并表示他们将专注于非 Java 的 GraalVM 语言,而不是 GraalVM 原生镜像。没人理解这个意思。我也在问,这意味着什么?他们还会支持 GraalVM 吗?我们还应该继续使用它吗?他们基本上指向了来自 OpenJDK 的 Project Leyden,并且它随 Java 25 一起发布。Project Leyden 的目标是提高 Java 程序的启动速度、达到峰值性能的时间,甚至减小程序的内存占用。你可以进行测试。它与 GraalVM 原生镜像并不相同。它们基本上有 AOT 缓存和类数据共享缓存。有了这些,你将无法获得 GraalVM 的性能。反而更差。这就是为什么我们目前感到困惑。GraalVM 原生镜像意味着什么?它会被支持吗?我不认为发布一个声明,说“最后一个完全支持的版本是在 Java 25 发布之后。”这是很不酷的。你需要提前知道这些,但我们无法改变。我们都希望得到澄清。

在幻灯片中,你会看到指向 GitHub 仓库的链接。你可以获取所有内容并重新测量,但我是用 Java 21 进行的测量。你之后可以用 Java 24 再做一次。AWS 会更新版本,所以次版本已经改变,希望有所改进。SnapStart 可能会改进,Firecracker 也可能改进,因此你可能会得到不同的结果。我真正看到的是,我使用了很多 AWS 依赖项,如果你升级版本,我几个月前测试过,新版本的趋势是依赖项的大小增加。你看到的大小增加意味着冷启动时间增加。特别是当我测量过某些东西时,这尤其明显。这又是一个关于 Spring Boot 3.2 的话题,然后我切换到 Spring Boot 3.4,仅仅通过升级,整体大小增加了 10%。它变得越来越大。

你可以在 dev.to 上找到我。我详细地介绍了所有这些调优的内容,以及你如何更深入地进行这些操作。我发布了一系列关于 Spring Boot、Quarkus 和 Micronaut 的文章,如果你使用它们的话。我仍在添加内容,并且我们会用 Java 25 和 Spring Boot 3.6 再次进行测量,你可以关注这些内容。我还发布了其他文章,关于如果你不想使用 DynamoDB,而是想使用 Postgres 的情况,比如如何处理这些变化。AWS 几个月前发布了 DSQL 数据库,它非常适合无服务器世界。它与 Postgres 兼容,但并非 100% 兼容。我会用它进行测量,因为它最终提供了在不考虑连接管理的情况下处理关系数据库和 Lambda 的可能性,而这是一个大问题。

我们有一个更小的函数,可以从数据库中获取所有连接,这个 Aurora DSQL 至少解决了这个问题,所以请注意。我会进行测量。基本上,信息是,冷启动时性能可以显著下降,但只需使用 SnapStart 即可。如果你深入了解这一点,它会为你带来很多机会。我希望它还能进一步改进。你将看到响应时间低于 1 秒,如果这种情况只发生在 2% 和 3% 的时间里,我认为这是合理的。如果你的公司有扎实的 Java 技能,并且想要转向无服务器架构或在 AWS Lambda 上使用 Java,为什么不试试看呢?基本上这就是问题所在。你无需过于担心。

查看更多附有讲稿的演讲

录制时间:

2026 年 6 月 15 日

  • Vadym Kazulkin

#### 相关赞助商

  • 基于 AWS 的智能代理 AI 运维团队

#### 相关赞助商

通过智能代理 AI 提升 AWS 效率 —— 统一遥测数据,减少噪音,更快解决故障。了解更多。

#### 本内容属于 Java 主题

##### 相关主题:

  • 开发
  • InfoQ 开发者峰会慕尼黑 2025
  • Java
  • InfoQ 开发者峰会
  • AWS
  • 亚马逊
  • 无服务器
  • QCon 软件开发会议
  • 讲稿
  • InfoQ
  • 云计算
  • 相关编辑
  • InfoQ 上的热门内容
  • 微软开源用于数据库内持久执行的 PostgreSQL 扩展
  • Slack 在 EMR 管道中淘汰 SSH,将 700 多个作业迁移至基于 REST 的架构
  • 使用项目即服务构建和扩展平台
  • AWS 引入 CDK Mixins 以实现可组合的基础设施抽象
  • AWS 为 Valkey 的 ElastiCache 引入持久存储选项
  • Craig McLuckie 在人工智能时代关于文化作为团队操作系统的话题