领先一步
VMware 提供培训和认证,助您快速提升。
了解更多随着 Spring Framework 6.1 和 Spring Boot 3.2 的正式发布临近,我们希望分享 Spring 团队为优化应用运行时效率所做的几项努力的概述。
我们将涵盖以下技术和用例
如果您更喜欢观看视频而不是阅读博文,我们推荐观看 Devoxx Belgium 2023 的“Spring Framework 6: Strategic Themes”演示文稿
让我们从最重要的问题开始:为什么我们应该关心改进云工作负载的运行时效率?第一个原因可能是成本优化。我们都希望以更低的成本运行我们的应用。更便宜的托管通常意味着使用更少的 CPU、更少的内存、更少的资源,这使得我们的工作负载更具可持续性。我们还生活在一个应用运行很可能以某种方式涉及 Kubernetes 和容器的世界,这通常需要特别关注 Java 虚拟机的启动时间、预热时间和内存管理。
Spring 团队的目标是提供多种选项(其中一些可以组合使用),以优化生产环境中数百万 Spring 工作负载的运行时占用空间和可伸缩性。我们的目标是尽可能减少您的 Spring 应用所需进行的更改,以便利用这些改进,但当然通常会涉及到权衡,我们将尽可能明确地说明这些权衡。希望这能为您提供足够的信息,以便更清楚地了解这如何适用于您的组织和应用,并判断哪些权衡在您的特定环境中是值得的。
利用这些运行时效率改进的一个普遍要求是升级到基于 Spring Framework 6 的 Spring Boot 3,该版本基于 Java 17,并且需要从 Java EE (javax
包) 过渡到 Jakarta EE (jakarta
包)。当您进行此类升级时,将为您提供一套新的运行时效率功能。
让我们从一项刚刚发布、在 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 和 Reactor 等响应式 API 具有无与伦比的附加价值,以及 Kotlin Coroutines 及其 Flow
类型,它提供了命令式和声明式编程模型的有趣组合。RSocket 是响应式交互模型的另一个重要附加价值示例。
请注意,您不必非此即彼,因为 Spring MVC 也提供了可选的响应式支持。因此,如果您只需要在服务器应用中处理少数并发用例,您可以简单地使用带有虚拟线程设置的 Spring MVC 堆栈,并在 Web 控制器中无缝包含响应式 WebClient
交互等,Spring MVC 会将响应式返回值适配为 Servlet 异步响应。Spring MVC 中的这种响应式支持是完全可选的,只有在实际使用响应式端点时才需要在堆栈中包含 Reactor 和 Reactive Streams,并且 HTTP 堆栈基于 Tomcat 或 Jetty 等 Servlet 容器(而非 Netty)。
对于典型的 Web 场景,我们预计虚拟线程将成为 Spring 开发者在 Java 21+ 上使用 Spring MVC 构建精简 Web 服务器堆栈的常见选择。更广泛的 Java 生态系统仍然需要完全适应虚拟线程,避免任何线程固定(例如在常见的 JDBC 驱动实现中),但即使如此,预计也很快会得到解决。请确保使用 Spring Boot 3.2 或更高版本,将属性 spring.threads.virtual.enabled
设置为 true
,并使用可用的最新库和驱动程序版本来评估虚拟线程。
我们继续完善 Spring Boot 3 中引入的 GraalVM 原生支持。主要用例是使用 Buildpacks 构建优化的容器镜像,其中包含一个微小的操作系统基础层,并且通过 Spring AOT(Ahead Of Time,预编译)转换和 GraalVM native image 编译器将您的应用编译成原生可执行文件。不需要 JVM 分发包。
这使得可以部署微型容器,它们可以在几十毫秒内启动(通常比普通 JVM 上的启动时间快 50 倍),应用基础设施的内存消耗更低,并且峰值性能立即可用。
GraalVM 非常紧密地跟进新的 Java 特性,例如已经提供了对虚拟线程的一等公民支持:请参阅 Josh Long 最近的博文 All together now。
与 JVM 相比,GraalVM 卓越的运行时特性得益于不同的权衡。原生镜像编译需要几分钟而不是几秒钟。为了正确处理反射、代理和 JVM 的其他动态行为,它需要额外的元数据。Spring 推断了大部分这些元数据,但任何实际项目可能都需要一些额外的提示才能正常工作(例如针对您组织的依赖项)。最后,Spring AOT 转换和 GraalVM 原生镜像的组合要求我们在构建时冻结类路径和 Spring Boot Bean 条件。通常您可以在运行时配置中更改数据库的 URL 或密码,但不能更改数据库类型或执行会改变 Spring Bean 结构的操作。
历史上,另一个缺点是由于缺乏即时编译而导致的有限的峰值性能,但在 GraalVM Free Terms and Conditions 许可下发布的 Oracle GraalVM(请参阅相关限制)挑战了这一假设。您可以订阅此相关的 Buildpacks RFC 以关注其潜在的未来支持,并且您可以使用这个简单的 Dockerfile
作为起点,将其用于您的 Spring Boot 工作负载进行尝试。
凭借即时启动和立即可用的峰值性能,Spring Boot 原生应用可以实现零伸缩。让我们探究这意味着什么。
零伸缩是无服务器的一种泛化。工作负载不仅可以部署到无服务器云平台,还可以部署到任何提供在没有请求处理时能够伸缩到零的 Kubernetes 或云平台。在 Kubernetes 中,您可以使用 Knative 或 KEDA 等解决方案来实现零伸缩。您不限于函数,您可以对任何类型的应用、任何编程模型(包括传统 Web 应用)进行零伸缩。无服务器最重要的特征并非技术上的,而是它实现的按使用付费的计费模式。
零伸缩在各种用例中都很有吸引力。JVM 在开发高流量网站方面表现出色,但老实说,我们也开发了许多小型后台应用,通常不会一直使用。当没人使用它们时,我们为什么要为此付费?还有一些通常只需要在很短的时间内运行的预生产环境,以及可以通过缓存机制在大部分时间关闭部分实例的微服务。另外,不要忘了高可用性,它迫使我们为每个服务保持两个实例始终运行,以备紧急情况,因为我们的应用启动时间太长,无法从故障中快速恢复。
但是,对于无法接受 GraalVM native image 所需权衡的项目,如何实现零伸缩呢?
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 应用上下文生命周期的停止和启动阶段非常契合。Spring Framework 6.1 的 CRaC 支持主要是将 CRaC 和 Spring 生命周期的映射结合起来,其余的支持不依赖于 CRaC,主要涉及 Spring 生命周期的一些改进,旨在正确关闭和重新创建 socket、文件和连接池。这里的目标是,除了常规的启动和停止生命周期之外,还支持多次停止和重新启动循环。
与 GraalVM 一样,Project CRaC 允许应用实现零伸缩,即使在小型服务器上也能在几十毫秒内即时启动。这比常规 JVM 冷启动快 50 倍,与 GraalVM native image 类似。但让我们探讨一下其中涉及的权衡。
第一个权衡是 CRaC 要求您在投入生产之前预先启动您的应用。那么您应该在 CI/CD 平台上启动它吗?是包含还是不包含您的生产远程服务?这引发了一系列不容忽视的问题。
第二个权衡是任何涉及 socket、文件和连接池的功能都需要关闭,然后根据 CRaC 生命周期正确重新创建这些资源。Spring Boot 会为您处理受支持的范围。但有些库尚不支持此功能,因此可能需要一段时间才能完全支持您的整个技术栈。
在我们看来,第三个权衡是最棘手的。创建一个包含所有内容、可直接恢复的容器镜像可能很诱人。但检查点启动期间加载到内存中的任何秘密信息都将被序列化到快照文件中,从而可能泄露生产数据库密码等敏感信息。
一个潜在的解决方案是在没有生产环境配置的情况下执行检查点启动,并在恢复时更新应用配置。这可以通过使用 Spring Cloud Context 和 @RefreshScope 注解来实现。Spring 团队未来可能会探索这个主题,看看是否有必要提供更多内置支持。您还可以采取策略,直接在 Kubernetes 平台上加密卷上创建和存储快照文件,即使这需要更深入的平台集成。
最后一个关键特征是 CRaC 是 Linux 特定的,并且需要进行一些Linux capability 微调才能在非特权模式下工作。
请记住,我们正处于 Project CRaC 的早期阶段,Spring Boot 3.2 是第一个支持它的版本。随着检查点恢复技术的演进以及 Spring 的支持,其中一些限制可能会被解除。如果您想亲自尝试这项技术,请查阅Spring Framework 相关文档和https://github.com/sdeleuze/spring-boot-crac-demo。
我们已经看到了使用 GraalVM 和 CRaC 使您的 Spring 工作负载实现零伸缩的两种方式,但这涉及一些非平凡的权衡。如果有一种限制更少的方法来改进您的 Spring Boot 运行时特性呢?
您可能听说过 Project Leyden,这是一个新的 OpenJDK 项目,旨在改进 Java 程序的启动时间、达到峰值性能所需的时间以及占用空间。如果您想了解更多信息,我们推荐观看 Brian Goetz 亲自进行的这场相关演讲。
Project Leyden 最近引入了“premain”优化(基本上是类数据共享 + 超强 AOT),有趣的是,Java 平台团队发现它与 Spring 预编译(Ahead-Of-Time,AOT)优化具有极好的协同效应,Spring AOT 最初是为了支持 GraalVM native image 而创建的,但已能够使 JVM 启动时间加快 15%。
虽然“premain”优化仍处于高度实验阶段(目前它是Leyden GitHub 仓库的一个实验分支),但 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 native image 提供的内存消耗降低对您来说是一个关键优势吗?您认为 JVM 上的 Spring AOT 与 Project Leyden 结合具有很高潜力吗?您对虚拟线程有何看法?请告诉我们!