Google Cloud 上的 Spring Native

1. 概览

在此 Codelab 中,我们将学习 Spring Native 项目、构建使用 Spring 项目的应用,并将其部署到 Google Cloud 上。

我们将介绍该计划的组件、项目的最新历史记录、一些用例,以及您在项目中使用它所需的步骤。

Spring Native 项目目前处于实验阶段,因此需要一些特定的配置才能开始。不过,正如 2021 年 SpringOne 大会上宣布的那样,Spring Native 已设置为与具有一流支持的 Spring Framework 6.0 和 Spring Boot 3.0 集成,因此这是深入了解项目的最佳时机。

虽然即时编译已经针对长时间运行进程等方面进行了充分优化,但在某些用例中,预先编译的应用的性能可能更好,我们将在 Codelab 中讨论这些用例。

您将了解如何

  • 使用 Cloud Shell
  • 启用 Cloud Run API
  • 创建和部署 Spring Native 应用
  • 将此类应用部署到 Cloud Run

所需条件

调查问卷

您将如何使用本教程?

仅阅读教程内容 阅读并完成练习

您如何评价自己在使用 Java 方面的经验水平?

新手水平 中等水平 熟练水平

您如何评价自己在使用 Google Cloud 服务方面的经验水平?

新手水平 中等水平 熟练水平

2. 背景

Spring Native 项目使用多种技术为开发者提供原生应用性能。

要全面了解 Spring Native,了解其中的一些组件技术、它们为我们带来的益处以及它们如何协同工作会很有帮助。

AOT 编译

如果开发者在编译时正常运行 javac,我们的 .java 源代码将被编译为以字节码编写的 .class 文件。此字节码只能供 Java 虚拟机理解,因此 JVM 必须在其他机器上解读此代码,以便我们运行代码。

此过程为我们提供了 Java 的签名可移植性 - 让我们可以“只编写一次并在所有位置运行”,但与运行原生代码相比,它成本高昂。

幸运的是,大多数 JVM 实现都使用即时编译来减轻这种解释成本。这一目标是通过计算函数的调用次数来实现的,如果调用次数经常超过阈值(默认设置为 10000),则系统会在运行时将其编译为原生代码,以防止解释成本高昂。

预先编译采取另一种方式,即在编译时将所有可到达的代码编译到原生可执行文件中。这会在运行时对可移植性换取内存效率和其他性能提升。

5042e8e62a05a27.png

当然,这是取舍权衡,并不总是值得。不过,AOT 编译在某些用例中非常突出,例如:

  • 启动时间很重要的短期应用
  • 内存密集型环境,JIT 可能代价过高

有趣的是,在 JDK 9 中,AOT 编译是一项实验性功能。尽管这种实现维护成本高昂,而且从未深入开发,因此它已被静默移除改为使用 GraalVM

GraalVM

GraalVM 是一种高度优化的开源 JDK 发行版,具有非常快的启动时间、AOT 原生映像编译和多语言功能,使开发者可以将多个语言混合到一个应用中。

GraalVM 正处于积极开发阶段,不断推出新功能并改进现有功能,因此我鼓励开发者保持关注。

近期的一些里程碑包括:

  • 直观易用的全新原生映像构建输出 ( 2021-01-18)
  • 支持 Java 17 ( 2022-01-18)
  • 默认启用多层编译以缩短多语种编译时间 ( 2021-04-20)

Spring Native

简而言之,Spring Native 支持使用 GraalVM 的原生映像编译器将 Spring 应用转换为原生可执行文件。

此过程涉及在编译时对应用进行静态分析,以找出应用中所有可通过入口点执行的方法。

这本质上是为应用创建一个“封闭”概念,其中所有代码在编译时都假定为已知,并且不允许在运行时加载任何新代码。

请务必注意,原生图像生成是一个内存密集型过程,所需时间比编译常规应用更长,并且对 Java 的某些方面施加了限制。

在某些情况下,应用无需更改代码即可使用 Spring Native。但某些情况下,特定的原生配置才能正常工作。在这些情况下,Spring Native 通常会提供原生提示以简化此流程。

3.设置/准备工作

在开始实现 Spring Native 之前,我们需要创建并部署应用,以便建立性能基准,以便稍后与原生版本进行比较。

1. 创建项目

首先,从 start.spring.io 获取我们的应用:

curl https://start.spring.io/starter.zip -d dependencies=web \
           -d javaVersion=11 \
           -d bootVersion=2.6.4 -o io-native-starter.zip

此起始应用使用 Spring Boot 2.6.4,这是在编写时 spring 原生项目支持的最新版本。

请注意,从 GraalVM 21.0.3 版本开始,此示例也可以使用 Java 17。在本教程中,我们将继续使用 Java 11,以尽量减少相关配置。

在命令行中使用 zip 文件后,我们可以为项目创建一个子目录,然后在文件夹中解压缩文件夹:

mkdir spring-native

cd spring-native

unzip ../io-native-starter.zip

2.代码更改

项目打开后,我们将快速添加生命标志,并在运行后显示 Spring Native 性能。

请修改 DemoApplication.java 以匹配以下代码:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.Instant;

@RestController
@SpringBootApplication
public class DemoApplication {
    private static Instant startTime;
    private static Instant readyTime;

    public static void main(String[] args) {
        startTime = Instant.now();
                SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/")
    public String index() {
        return "Time between start and ApplicationReadyEvent: "
                + Duration.between(startTime, readyTime).toMillis()
                + "ms";
    }

    @EventListener(ApplicationReadyEvent.class)
    public void ready() {
                readyTime = Instant.now();
    }
}

此时,我们的基准应用已经准备就绪,因此您可以随意构建映像并在本地运行它,以便在启动它之前将其转换为原生应用。

如需构建映像,请执行以下操作:

mvn spring-boot:build-image

您还可以使用 docker images demo 了解基准图片的大小:6ecb403e9af1475e.png

如需运行应用,请执行以下操作:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

3.部署基准应用

现在,我们有了应用,就可以部署并记录时间,稍后我们会将其与原生应用启动时间进行比较。

根据您正在构建的应用的类型,有几种不同的托管资料

不过,由于我们的示例是一个非常简单直接的 Web 应用,因此我们可以简化操作并依赖 Cloud Run。

如果您要在自己的机器上进行操作,请务必安装并更新 gcloud CLI 工具。

如果您使用的 Cloud Shell 将被处理好,您只需在源目录中运行以下命令即可:

gcloud run deploy

4.应用配置

1. 配置 Maven 代码库

由于此项目仍处于实验阶段,因此我们必须配置应用,使其能够找到 Maven 中央代码库中未提供的实验工件。

这将需要在 pom.xml 中添加以下元素,您可以在您所选的编辑器中执行此操作。

将以下代码库和 pluginRepositories 部分添加到我们的 pom 中:

<repositories>
    <repository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>spring-release</id>
        <name>Spring release</name>
        <url>https://repo.spring.io/release</url>
    </pluginRepository>
</pluginRepositories>

2. 添加依赖项

接下来,添加运行原生 Spring 应用所需的 Spring 原生依赖项。注意:如果您使用的是 Gradle,则无需执行此步骤

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>0.11.2</version>
    </dependency>
</dependencies>

3.添加/启用我们的插件

现在添加 AOT 插件,以提高原生映像的兼容性并减少占用空间(了解详情):

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-aot-maven-plugin</artifactId>
        <version>0.11.2</version>
        <executions>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

现在,我们将更新 Spring-boot-maven-plugin 以启用原生映像支持,并使用 paketo 构建器构建我们的原生映像:

<plugins>
    <!-- ... -->
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                    <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
            </image>
        </configuration>
    </plugin>
</plugins>

请注意,小型构建器映像只是多个选项之一。这是一个非常好的用例,因为它只有极少量的额外库和实用程序,有助于最大限度地减少受攻击面。

例如,如果您构建的应用需要访问一些常用 C 库,或者您尚未确定应用的要求,则完整构建器可能更适合。

5. 构建和运行原生应用

完成上述所有操作后,我们就应该能够构建映像并运行我们的原生编译应用。

在运行 build 之前,请注意以下几点:

  • 这将比常规构建(几分钟)多一些d420322893640701.png
  • 此构建过程可能需要大量的内存(几 GB)cda24e1eb11fdbea.png
  • 此构建过程需要能够访问 Docker 守护程序
  • 在本例中,我们会手动执行此流程,不过您也可以将构建阶段配置为自动触发原生构建配置文件

如需构建映像,请执行以下操作:

mvn spring-boot:build-image

构建完成后,我们就可以看到原生应用的运行情况了!

如需运行应用,请执行以下操作:

docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT

此时,我们可以非常方便地看到原生应用程序方程的两边。

我们在编译时放弃了一些时间和额外的内存用量,但作为交换,我们可以获得一个可以更快地启动,并显著减少内存(具体取决于工作负载)的应用。

如果我们运行 docker images demo 将原生图片的大小与原始图片的大小进行比较,就会发现大幅减小:

e667f65a011c1328.png

我们还应注意,在更复杂的用例中,需要进行其他修改,以告知 AOT 编译器您的应用将在运行时执行的操作。因此,某些可预测的工作负载(例如批处理作业)可能非常适合使用,而其他可预测的工作负载则可能是更大的提升幅度。

6.部署我们的原生应用

为了将应用部署到 Cloud Run,我们需要将原生映像放入 Artifact Registry 等软件包管理器中。

1. 准备 Docker 代码库

我们可以通过创建一个代码库来启动此过程:

gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"

接下来,我们需要确保通过身份验证推送到新的注册表。

gcloud CLI 可以大大简化该过程:

gcloud auth configure-docker us-central1-docker.pkg.dev

2.将我们的映像推送到 Artifact Registry

接下来,我们将标记图片:

export PROJECT_ID=$(gcloud config list --format 'value(core.project)')

docker tag  demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

然后,我们可以使用 docker push 将其发送到 Artifact Registry:

docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

3.部署到 Cloud Run

我们现已可以将存储在 Artifact Registry 中的映像部署到 Cloud Run:

gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2

因为我们是以原生映像的形式构建和部署应用的,所以我们可以确信,我们的应用在运行期间会充分利用我们的基础架构成本。

您可以随时将我们的基准应用的启动时间与这个新的原生应用进行比较!

6dde63d35959b1bb.png

7. 摘要/清理

恭喜您在 Google Cloud 上构建和部署 Spring Native 应用!

希望本教程让您更熟悉 Spring Native 项目,并在今后能满足自己的需求时牢记在心。

可选:清理和/或停用服务

无论您是为此 Codelab 创建 Google Cloud 项目,还是重复使用现有项目,都请务必避免我们使用的资源产生不必要的费用。

您可以删除或停用我们创建的 Cloud Run 服务,删除我们托管的映像,也可以关停整个项目。

8. 其他资源

虽然 Spring Native 项目目前是一个全新的实验性项目,但已经有大量良好的资源可帮助尝鲜者排查问题并参与进来:

其他资源

以下是可能适用于本教程的在线资源:

许可

此作品已获得 Creative Commons Attribution 2.0 通用许可授权。