Google Cloud 上的 Spring Native

1. 概览

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

我们将介绍其组件、该项目的近期历史记录、一些用例,当然还有在项目中使用该库所需的步骤。

Spring Native 项目目前处于实验阶段,因此需要进行一些特定的配置才能开始使用。不过,正如在 SpringOne 2021 大会上宣布的那样,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 实现都使用即时编译来减少这种解释开销。为此,系统会统计函数的调用次数,如果调用次数足够多,超过阈值(默认为 10,000 次),系统会在运行时将其编译为原生代码,以防止进一步进行耗时的解释。

而预编译则采用相反的方法,即在编译时将所有可访问的代码编译为原生可执行文件。这会以可移植性为代价,在运行时实现内存效率和其他性能提升。

5042e8e62a05a27.png

当然,这是一种权衡,并不总是值得这样做。不过,在某些用例(例如以下用例)中,AOT 编译可以发挥出色作用:

  • 启动时间至关重要的短时应用
  • 内存极其受限的环境,JIT 的开销可能过高

有趣的是,AOT 编译在 JDK 9 中作为实验性功能引入,但此实现的维护成本很高,而且从未真正流行起来,因此在 Java 17 中被悄悄移除,以便开发者只使用 GraalVM

GraalVM

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

GraalVM 正在积极开发中,不断获得新功能并改进现有功能,因此我们鼓励开发者密切关注。

近期的一些里程碑包括:

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-native 项目支持的最新版本

请注意,自 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,您可以在自己偏好的编辑器中执行此操作。

将以下 repositories 和 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-native 依赖项,这是将 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>

请注意,tiny builder 映像只是多种选项之一。它非常适合我们的用例,因为它只有很少的额外库和实用程序,这有助于最大限度地缩小攻击面。

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

5. 构建和运行原生应用

完成所有这些操作后,我们应该就可以构建映像并运行经过编译的原生应用了。

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

  • 此过程比常规 build 花费的时间要长(几分钟)d420322893640701.png
  • 此构建过程可能会占用大量内存(几 GB)cda24e1eb11fdbea.png
  • 此构建过程需要 Docker 守护程序可访问
  • 虽然在此示例中,我们将手动完成此流程,但您也可以配置 build 阶段以自动触发原生 build 配置文件

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

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 通用许可授权。