Spring 运行时效率(现在和未来)

工程 | Sébastien Deleuze | 2023年10月16日 | ...

随着 Spring Framework 6.1 和 Spring Boot 3.2 正式发布的临近,我们想分享一下 Spring 团队正在努力的方向,以帮助开发者优化其应用程序的运行时效率。

我们将涵盖以下技术和用例

  • 在 JDK 21 上使用精简的虚拟线程 Web 栈的 Spring MVC
  • 使用 Spring 和 GraalVM 原生镜像优化的容器部署
  • JVM 检查点恢复:使用 Spring 和 Project CRaC 实现零扩展
  • Spring AOT 和 Project Leyden 对 OpenJDK 未来发展的一瞥

如果您更喜欢观看视频而不是阅读博文,我们推荐 Devoxx Belgium 2023 的“Spring Framework 6:战略主题”演示文稿

上下文

让我们从最重要的问题开始:为什么我们要关心改进云工作负载的运行时效率?第一个原因可能是成本优化。我们都想以更便宜的方式运行我们的应用程序。更便宜的托管通常意味着使用更少的 CPU、更少的内存、更少的资源,这使得我们的工作负载更可持续。我们也身处一个运行应用程序可能或多或少会涉及 Kubernetes 和容器的世界,这通常需要额外注意 Java 虚拟机的启动时间、预热时间和内存管理。

Spring 团队的目标是提供各种选项(其中一些可以组合使用)来优化数百万个生产环境中 Spring 工作负载的运行时资源占用和可扩展性。我们的目标是尽可能减少在您的 Spring 应用程序中进行更改以利用这些改进所需的更改量,但当然,通常会涉及权衡,我们将尽可能明确地说明这些权衡。希望这能为您提供足够的信息,让您更清楚地了解这如何应用于您的组织、您的应用程序,并了解哪些权衡对您的环境来说是值得的。

为了利用这些运行时效率改进,一个共同的要求是升级到基于 Spring Framework 6 的 Spring Boot 3,它具有 Java 17 基线,并且需要从 Java EE(`javax` 包)迁移到 Jakarta EE(`jakarta` 包)。当您进行此类升级时,将为您提供一组新的运行时效率功能。

在 JDK 21 上使用精简的虚拟线程 Web 栈的 Spring MVC

让我们从一项刚刚发布的技术开始,它从 Java 21 开始可用。虚拟线程旨在降低用简单且流行的每请求一个线程样式编写的服务器应用程序的成本,以接近最佳的硬件利用率进行扩展。

虚拟线程使 I/O 阻塞变得廉价,因此非常适合 Servlet 栈上的 Spring Web MVC 应用程序。Spring MVC 可以充分利用 Tomcat 或 Jetty 上的这些新的运行时特性以及虚拟线程设置。在大多数情况下,这不需要代码更改,并且可以自然地适应以提供最佳性能,而无需微调线程池配置。

我们还听取了 Spring 社区的反馈,他们要求我们不仅要提供对处于维护模式的 RestTemplate 和响应式 WebClient 之间的选择。因此,我们决定在 Spring Framework 6.1 中引入一个名为 RestClient 的“虚拟线程友好的现代 HTTP 客户端”(当然,在没有虚拟线程的情况下也是一个有吸引力的选择)。Spring Cloud Gateway 和 Spring 产品组合中的相关基础设施同样可以从虚拟线程设置以及 Spring MVC 中受益,从而提供一致的整体体验。

那么,这对 WebFlux 和响应式栈意味着什么呢?

我们故意选择具有不同的阻塞和响应式栈,以便在 WebFlux 服务器中充分利用响应式编程,并使 Spring Web MVC 栈(到目前为止,在 start.spring.io 上最常用的 Web 栈)尽可能精简,并采用常规的阻塞线程架构。在 Servlet 容器上的 Spring MVC 非常适合虚拟线程,作为改进传统 Web 应用程序可扩展性的有吸引力的解决方案。另一方面,WebFlux 服务器提供了一个优化的响应式栈,非常适合 Netty I/O 设置,通过不同的编程模型提供同等的运行时优势。

当您需要应用程序级并发(例如,发送多个远程 HTTP 请求,可能流式传输,并组合结果)时,Project Loom 结构化并发 未来可能会提供一个有趣的底层构建块,但这并不是开发者通常在 Spring 应用程序中需要的 API 类型(它仍在预览中)。对于这样的用例,WebFlux 和响应式 API(如 Reactor)目前具有无与伦比的附加值,以及 Kotlin 协程 及其 Flow 类型,它提供了命令式和声明式编程模型的有趣组合。RSocket 是响应式交互模型另一个极好的附加值的例子。

请注意,您不必选择其中一个,因为 Spring MVC 也提供可选的响应式支持。因此,如果您只需要处理服务器应用程序中的一些用例的并发,您可以简单地使用具有虚拟线程设置的 Spring MVC 栈,并在您的 Web 控制器中无缝地包含例如响应式 WebClient 交互,Spring MVC 将响应式返回值适配到 Servlet 异步响应。Spring MVC 中的这种响应式支持是完全可选的,只有在实际使用响应式端点时才需要 Reactor 和 Reactive Streams,并且 HTTP 栈基于 Tomcat 或 Jetty(而不是 Netty)等 Servlet 容器。

对于典型的 Web 场景,我们预计虚拟线程将成为 Java 21+ 上 Spring 开发人员使用 Spring MVC 作为精简 Web 服务器栈的常见选择。更广泛的 Java 生态系统仍然必须完全适应虚拟线程,避免任何线程绑定(例如在常见的 JDBC 驱动程序实现中),但即使这也有望很快得到解决。确保使用 Spring Boot 3.2 或更高版本,将属性 spring.threads.virtual.enabled 设置为 true,并使用最新的库和驱动程序版本来评估虚拟线程。

使用 Spring 和 GraalVM 原生镜像优化的容器部署

我们继续改进 Spring Boot 3 中引入的 GraalVM 原生支持。主要用例是使用 Buildpacks 构建一个优化的容器镜像,其中包含一个微小的操作系统基础层和您的应用程序,感谢 Spring AOT(提前)转换和 GraalVM 原生镜像编译器,这些应用程序被编译成原生可执行文件。无需 JVM 发行版。

GraalVM native image build and execution

这允许部署启动时间为几十毫秒(通常比常规 JVM 上的启动时间快 50 倍)的微型容器,并降低应用程序基础设施的内存消耗,并立即提供峰值性能。

GraalVM 非常紧密地跟踪新的 Java 功能,例如,已经提供了对虚拟线程的一流支持:请参阅 Josh Long 最近的 All together now 博文。

GraalVM trade-offs

GraalVM 的出色运行时特性是由于与 JVM 相比采用了不同的权衡。原生镜像编译需要几分钟而不是几秒钟。它需要附加元数据才能正确处理反射、代理和 JVM 的其他动态行为。Spring 推断了很多这些元数据,但是任何真实的项目都可能需要一些额外的提示才能正常工作(例如,对于您的组织依赖项)。最后,Spring AOT 转换和 GraalVM 原生镜像的组合要求我们在构建时冻结类路径和 Spring Boot bean 条件。您通常能够在运行时配置中更改数据库的 URL 或密码,但不能更改数据库类型或执行更改 Spring bean 结构的操作。

历史上,另一个缺点是由于缺乏即时编译而导致的峰值性能有限,但是根据 GraalVM 免费条款和条件许可证发布的 Oracle GraalVM(请参阅 相关限制)挑战了这一假设。您可以订阅 此相关的 Buildpacks RFC 以跟踪其潜在的即将推出的支持,并且您已经可以使用 这个简单的 Dockerfile 作为起点来尝试使用您的 Spring Boot 工作负载。

借助即时启动和立即可用的峰值性能,Spring Boot 原生应用程序可以实现零扩展。让我们来探讨一下这意味着什么。

零扩展

零扩展是无服务器架构的一种泛化。工作负载不仅可以部署到无服务器云平台,还可以部署到任何 Kubernetes 或云平台,这些平台在没有请求需要处理时能够实现零扩展。使用 Kubernetes,您可以使用 KnativeKEDA 等解决方案实现零扩展。而且您不限于函数,您可以使用任何类型的应用程序、任何类型的编程模型(包括传统的 Web 应用程序)来实现零扩展。无服务器架构最重要的特征不是技术上的,而是它所支持的按使用付费计费模式。

Scale to zero use cases

零扩展在各种用例中都很有意义。JVM 在开发高流量网站方面非常出色,但说实话,我们也开发了很多小型后台应用程序,这些应用程序通常不会一直使用。为什么在没有人使用它们时还要付费呢?还有一些暂存环境,通常只需要运行很短的时间,以及微服务,其中缓存允许大多数时间关闭其中一些服务。我们也不能忘记高可用性,它迫使我们始终保持每个服务的两个实例处于运行状态,以防万一发生紧急情况,因为我们的应用程序启动时间太长而无法从故障中恢复。

但是,对于无法接受 GraalVM 原生镜像所需权衡的项目,如何实现零扩展呢?

JVM 检查点恢复:使用 Spring 和 Project CRaC 实现零扩展

CRaC 是一个 OpenJDK 项目,它定义了一个新的 Java API,允许您在 HotSpot JVM 上检查点和恢复应用程序,该项目由 Azul Systems 开发,目前也得到 AWS Lambda 和 IBM OpenLiberty 的支持。它基于 CRIU,这是一个在 Linux 上实现检查点/恢复功能的项目。

其原理如下:您几乎照常启动应用程序,但使用的是启用了 CRaC 的 JDK 版本。然后,在某个时刻,可能是在一些工作负载之后(这些工作负载将通过执行所有常用代码路径使您的 JVM 变热),您可以使用 API 调用、jcmd 命令、HTTP 端点或其他机制触发检查点。

然后,运行中 JVM 的内存表示(包括其热度)将被序列化到磁盘,允许稍后在另一个机器上(具有类似的操作系统和 CPU 架构)非常快速地恢复。恢复后的进程保留了 HotSpot JVM 的所有功能,包括运行时的进一步 JIT 优化。

Spring lifecycle with CRaC

值得注意的是,“检查点”和“恢复”与 Spring 应用程序上下文生命周期的停止和启动阶段非常匹配。Spring Framework 6.1 的 CRaC 支持主要是关于将 CRaC 和 Spring 生命周期结合起来,其余的支持与 CRaC 无关,主要涉及旨在正确关闭和重新创建套接字、文件和池的 Spring 生命周期改进。这里的目标是除了常规的启动和停止生命周期之外,还要支持多个停止和重新启动周期。

Instant restoration of a Spring Boot application

与 GraalVM 一样,Project CRaC 允许应用程序实现零扩展,即使在小型服务器上也能实现几毫秒的即时启动。这比常规 JVM 冷启动快 50 倍,与 GraalVM 原生镜像类似。但是让我们来探讨一下相关的权衡。

Project CRaC trade-offs

第一个权衡是 CRaC 要求您提前启动应用程序,然后才能投入生产。那么您应该在您的 CI/CD 平台上启动它吗?是否要使用或不使用您的生产远程服务?这引发了一系列非平凡的问题。

第二个权衡是需要关闭涉及套接字、文件和池的任何功能,然后根据 CRaC 生命周期正确地重新创建这些资源。Spring Boot 会为您处理 支持的范围 内的工作。但是有些库目前不支持这一点,因此可能需要一些时间才能使您的整个堆栈完全支持。

第三个权衡在我们看来是最棘手的。创建自包含的、随时可恢复的容器镜像可能很诱人。但是,在检查点启动期间加载到内存中的任何秘密都将被序列化到快照文件,从而可能泄漏敏感信息,例如您的生产数据库密码。

一个可能的解决方案是在不使用生产环境配置的情况下执行检查点启动,并在恢复时更新您的应用程序配置。可以使用 Spring Cloud Context 和 @RefreshScope 注解 来做到这一点。Spring 团队将来可能会探讨这个主题,看看是否需要更多内置的支持。您还可以采用策略,直接在您的 Kubernetes 平台上创建和存储快照文件到加密卷上,即使这需要更深入的平台集成。

最后一个关键特征是 CRaC 是特定于 Linux 的,并且需要一些 Linux 功能的微调 才能在非特权模式下工作。

请记住,我们正处于 Project CRaC 的早期阶段,Spring Boot 3.2 是第一个支持它的版本。随着检查点恢复技术的不断发展以及 Spring 的支持,这些限制中的一些可能会被解除。如果您想自己尝试这项技术,请查看 Spring Framework 相关文档https://github.com/sdeleuze/spring-boot-crac-demo

Spring AOT 和 Project Leyden 带来的 OpenJDK 未来一瞥

我们已经看到了两种方法可以让您的 Spring 工作负载使用 GraalVM 和 CRaC 实现零扩展,但都涉及非平凡的权衡。如果还有另一种方法可以在限制更少的情况下改进 Spring Boot 的运行时特性呢?

您可能听说过 Project Leyden,这是一个新的 OpenJDK 项目,旨在改进 Java 程序的启动时间、达到峰值性能的时间和占用空间。如果您想了解更多信息,我们建议您观看 Brian Goetz 本人发表的 相关演讲

Project Leyden 最近引入了“premain”优化(基本上是 类数据共享 + AOT 加强版),有趣的是,Java 平台团队发现它与 Spring Ahead-Of-Time 优化(最初是为了支持 GraalVM 原生镜像而创建的,但已经在 JVM 上实现了 15% 的启动时间提升)具有很好的协同作用。

Project Leyden with Spring AOT data points

虽然“premain”优化处于高度实验阶段(目前它是 GitHub 上 Leyden 存储库的一个实验分支),但 Spring 团队最近能够通过结合 JVM 上的 Spring AOT 和 Project Leyden 的这些优化来测量 Spring Petclinic 示例应用程序的启动时间提高了 2 到 4 倍,并且预热速度更快,几乎没有任何权衡。

目前,与 GraalVM 和 CRaC 不同,这些优化并不能实现零扩展,因为它们不允许应用程序在生产环境中以几十毫秒的速度启动。但是,如果我们能够在几乎没有任何限制的情况下显著改进 JVM 的启动和预热时间,它就有可能成为主流,并与您可以按需选择的其他即将推出的 Leyden 功能相结合。

我们很高兴地分享,我们已经开始在 Java 平台组和 Spring 团队之间进行合作,以了解我们可以使用 Project Leyden 的 premain 方法将可能性推向多远。结合专门为 JVM 设计的 Spring AOT 改进,我们预计将有更多适用于各种 Spring 应用程序的优化。我们将在未来几个月分享更多信息。

如果您想自己尝试,请查看 https://github.com/sdeleuze/spring-boot-leyden-demo 存储库。

总结

倾听来自世界各地 Spring 社区的反馈已被证明是 Spring 团队灵感的关键来源,以及与 Oracle、Bellsoft、Azul 和许多其他公司进行务实合作的关键。

我们正在努力支持这些新功能,同时最大限度地减少对 Spring 应用程序开发的影响,为各种类型的应用程序提供直接的升级路径。这是我们战略基础设施工作中最具挑战性但也最令人欣慰的方面。

最后但并非最不重要的是,我们正在寻求关于您对您的组织和项目最兴奋的方面的反馈。您认为零扩展和按使用付费计费模式是否值得 GraalVM 或 CRaC 所需的权衡?GraalVM 原生镜像提供的内存消耗减少对您来说是否是一个关键优势?您认为 JVM 上的 Spring AOT 与 Project Leyden 相结合是否具有很高的潜力?您对虚拟线程有什么看法?请告诉我们!

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以提升您的进度。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

查看Spring社区中所有即将举行的活动。

查看全部