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 原生应用
  • 将此类应用部署到 Cloud Run

所需条件

调查问卷

您将如何使用本教程?

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

您如何评价自己使用 Java 的体验?

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

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

新手 中级 熟练

2. 背景

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

若要充分了解 Spring Native,了解一些组件技术、它们为我们实现的功能以及它们如何协同工作会很有帮助。

AOT 编译

当开发者在编译时正常运行 javac 时,我们的 .java 源代码会编译成以字节码编写的 .class 文件。该字节码仅供 Java 虚拟机理解,因此 JVM 必须在其他计算机上解释此代码,这样我们才能运行我们的代码。

该进程赋予了 Java 特征码可移植性,使我们能够“编写一次即可在任何位置运行”,但与运行原生代码相比,它的成本很高。

幸运的是,大多数 JVM 实现都使用即时编译来降低这种解释开销。这是通过统计对函数的调用次数来实现的,如果调用函数的频率足够高,可以达到阈值(默认为 10,000 次),那么系统会在运行时将其编译为原生代码,以防止执行大量的解译。

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

5042e8e62a05a27

当然,这需要权衡,并不总是值得的。不过,AOT 编译可能在某些用例中大放异彩,例如:

  • 启动时间非常重要的短时效应用
  • 内存极度受限的环境,其中 JIT 可能成本过高

有趣的是,AOT 编译是在 JDK 9 中作为一项实验性功能引入的,尽管此实现维护成本高昂,并且从未得到广泛关注,因此在 Java 17 中静默地移除了它,以支持仅使用 GraalVM 的开发者。

GraalVM

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

GraalVM 处于积极开发中,一直在获得新功能并改进现有功能,因此我鼓励开发者持续关注。

近期的一些里程碑如下:

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

春季原生广告

简而言之,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

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

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-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 构建器映像只是多个选项之一。对于我们的用例而言,这是一个不错的选择,因为它几乎没有额外的库和实用程序,这有助于最大程度地减少受攻击面。

例如,如果您正在构建一个需要访问一些常见 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 原生应用!

希望本教程可以帮助您更熟悉 Spring Native 项目,并在将来满足您的需求时注意它。

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

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

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

8. 其他资源

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

其他资源

以下是可能与本教程相关的在线资源:

许可

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