GraalVM 与 Spring Native 初体验,一个让你的应用在 100ms 内启动的神器

本贴最后更新于 801 天前,其中的信息可能已经事过境迁

原文首发于菠萝的博客, 欢迎关注,获取最新更新。

7043346857590688133.PNG
先吹一波截图,当中 springboot 的启动只用了 0.036 秒,试问如果没有 Spring Native,谁还能做到。
即使是 M1 Mac Pro 启动也是需要 0.559 秒。两张图片的时间差距比较久是因为在写博客的时候,突发奇想想把 solo 博客也给做成 GraalVM 的,但是很可惜失败了,这里省略几百字的小作文,但是会提到为什么失败了。
image.png

1. 一些背景知识

1.1 GraalVM

GraalVM 在官方网站对自己的介绍是 High Performanсe. Cloud Native. Polyglot 意思就是 高性能,云原生,多语言。

GraalVM for Java 具有新的编译器优化的高性能 runtime,以加速 Java 应用程序性能和较低的基础设施成本以及云中的基础设施成本。Graalvm 是 Java 和其他 JVM 语言的高性能 runtime。它包含一个兼容的 JDK,并提供基于 Java 8(仅 GRAALVM Enterprise Edition),Java 11 和 Java 17 Graalvm 提供多个编译器优化的分布,旨在加速 Java 应用程序性能,同时消耗更少的资源。要开始使用 Graalvm,或从另一个 JDK 分发迁移,您不必更改任何源代码。在 Java Hotspot VM 上运行的任何应用程序将在 Graalvm 上运行。

很官方啊,这样的话。说的明白点就是 GraalVM 是一个共享运行时间的生态系统,无论是那些依赖于 JVM 的语言(Java、Scala、Groovy、Kotlin)还是说其他的编程语言例如(JavaScript、Ruby、Python、R)有性能上的优势。另外,GraalVM 能够通过一种前端的 LLVM 执行 JVM 上面的原生代码。

2. 安装 GraalVM

这里会说到 Windows, Mac,linux 下的安装过程。

2.1 下载

地址

找到你电脑安装的 Jdk 的版本进行下载

image.png

这里推荐安装 Java11,Java17 的版本我安装之后发现有问题,在我改成 Java11 后没有修改其他配置的情况下却又成功了。

2.2 安装

官方英文版

linux and mac

这里 linux 和 mac 下一起讲是因为差不多,会其中一个,另一个你也可以延展的去安装。

下载解压后,放在和你的 JDK 同一级目录下,如:

tar -xzf <graalvm-archive>.tar.gz

image.png

更改环境变量

linux,以 centos 为例就是更改 /etc/profile 文件,macOS 下就是更改 ~/.zshrc 文件,在这里需要把你之前安装的 JDK 时配置的 JAVA_HOME 进行修改为:GraalVM 的地址

export JAVA_HOME=/Users/asher/workspace/software/jdk/graalvm-ce-java11-21.3.0/Contents/Home

然后在 PATH 路径进行添加

export PATH=$PATH:$MAVEN_HOME/bin:$FFMPEG_HOME/bin:/Users/asher/workspace/software/jdk/graalvm-ce-java11-21.3.0/Contents/Home/bin:$JAVA_HOME:.

注意,我在上面添加了 /Users/asher/workspace/software/jdk/graalvm-ce-java11-21.3.0/Contents/Home/bin 的路径,这个是需要进行添加的

centos 下别忘了 source /etc/profile

Windows

解压下载的文件

然后 win+R 打开你的命令行

setx /M JAVA_HOME "C:\Progra~1\Java\<graalvm>"

配置环境变量

setx /M PATH "C:\Progra~1\Java\<graalvm>\bin;%PATH%"

当你安装配置完成之后,打开新的命令行窗口,执行 java -version

就会发现 JDK 已经改成了新安装的那个了,类似如下截图

image.png

3. 从 Hello World 开始

已经安装完成之后,我们从最简单的 Hello World 开始,体会一下 GraalVM 和 JVM 的区别

新建一个 Java 文件,HelloWorld.java

然后输入

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, World!");
  }
}

3.1 JVM 版

我们需要 javac HelloWorld.java, 然后 java HelloWorld ,我们用记录一下
java HelloWorld 所需要的时间
image.png
总计 0.077 秒

3.2 GraalVM 版

先进行安装 native-image

gu install native-image

然后在刚刚编译 HelloWorld 的目录下进行执行

native-image HelloWorld

等待一段时间,此时会直接生成一个可执行文件

image.png

等待一段时间后,我们会发现文件生成了

image.png

让我们执行看看

image.png

完全没问题,再测试一下时间呢

image.png

0.063 秒!拿出之前和 JVM 执行的对比一下,它在执行的时候,用户态和系统态使用的时间都低于 JVM

image.png

虽说 GraalVM 确实快了,但是你也注意到了,当执行的 native-image HelloWorld 时候会有好几个阶段,而且都很耗时间跟内存。

4. 进阶版, Maven 插件编译

看完了上面的,你可能觉得差距不大,毕竟这几微秒的事,咱们都体会不出来。

这里会说到的是在我们常见的 Maven 项目如何进行使用 GraalVM。

让我们新建一个 Maven 项目, 整个程序的目录结构是这样的,只有一个 Application.java 和一个 Person.java 文件

image.png

4.1 pom.xml

因为这里我们要使用 maven plugin 进行打包,加入了 dependency graal-sdk ,然后引入了 native-image-maven-plugin

<properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <graal-sdk.version>21.3.0</graal-sdk.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.graalvm.sdk</groupId>
            <artifactId>graal-sdk</artifactId>
            <version>${graal-sdk.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.nativeimage</groupId>
                <artifactId>native-image-maven-plugin</artifactId>
                <version>21.2.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>native-image</goal>
                        </goals>
                        <phase>package</phase>
                    </execution>
                </executions>
                <configuration>
                    <skip>false</skip>
                    <imageName>graalvmMaven</imageName>
                    <mainClass>run.runnable.Application</mainClass>
                    <buildArgs>
                        --no-fallback
                    </buildArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

然后的话我们还需要在 IDEA 中进行配置编译的 Java 版本是下载的 GraalVM 下的 Java

image.png

修改之后我们在项目中加一些代码试试。这里的话,我新建了一个 Person 实体类和 Application 启动类。代码如下

Person.java

package run.runnable.entity;

/**
 * @author Asher
 * on 2021/12/23
 */
public class Person {

    private Integer id;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Application

package run.runnable;

import run.runnable.entity.Person;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author Asher
 * on 2021/12/23
 */
public class Application {

    public static void main(String[] args) {
        List<Person> personList = new ArrayList<>();
        for (int i = 0; i < 10_000; i++) {
            Person person = new Person();
            person.setId(i);
            person.setName("jack" + i);
            personList.add(person);
        }
        List<Person> collectPersonList = personList.stream()
                .filter(person -> person.getId() > 5000)
                .collect(Collectors.toList());
        System.out.println(collectPersonList);
    }

}

这里代码逻辑很简单,就是新建了一个 personList,然后对其进行添加 10000 个,添加进去之后,再对 id>5000 的进行过滤。

4.2 JVM 版

普通 maven 项目打成 jar 方法不再赘述,这里直接演示结果。

JVM 版用的是已经适配了 m1 的 zulu jdk,所以不用担心会由于转译引起的性能下降。

进行执行

time java -jar graalvmMaven-1.0-SNAPSHOT.jar

可以看到执行时间为 0.146

image.png

4.3 GraalVM 版本

直接点击 Maven 的 package 就可以进行打包

image.png

打包时间花了一分钟

image.png

接下来让我们执行打包生成的可执行文件

image.png

0.085 秒!和 JVM 版的 0.146 秒相比,花的时间差距也越来越明显了。

5. 高级版, SpringBoot 项目使用 Spring Native 打包成 image

在这个部分中,甚至你本地都不用安装 GraalVM。

5.1 新建 SpringBoot 项目

在这一部分里会说到,怎么将一个简单的 SpringBoot 项目进行打包成 docker 的 image,这里我推荐使用 window 下的 WSL2 进行,因为这个过程非常吃资源。在 mac 下即使我给 docker 设置了 10G 运存,4 核 CPU 仍然会莫名卡死在某一部分。

当然如果你是 linux 主机那就更好了,不用担心这个问题。

让我们新建一个 SpringBoot 的简单项目。

Spring Initializr 中我们选择 SpringBoot 的版本,以及在右侧我们选择 Spring Native 依赖,和 Spring

image.png

点击下面的生成会下载一个压缩包,在工作目录进行解压,然后导入到你的 IDEA 中。

5.2 稍微修改一下 pom 文件

在生成的 pom 文件中,Spring 已经贴心帮我们把配置都加好了。

所以我只添加了一个 spring-boot-starter-web 的 dependency

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.1</version>
		<relativePath/>
	</parent>
	<groupId>run.runnable</groupId>
	<artifactId>experience</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>experience</name>
	<description>Experience Spring Native</description>
	<properties>
		<java.version>11</java.version>
		<repackage.classifier/>
		<spring-native.version>0.11.0</spring-native.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.experimental</groupId>
			<artifactId>spring-native</artifactId>
			<version>${spring-native.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.graalvm.buildtools</groupId>
			<artifactId>native-maven-plugin</artifactId>
			<version>0.9.8</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-idea-plugin -->
		<dependency>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-idea-plugin</artifactId>
			<version>2.2.1</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-maven-plugin</artifactId>
			<version>2.5.0</version>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<classifier>${repackage.classifier}</classifier>
					<image>
						<builder>paketobuildpacks/builder:tiny</builder>
						<env>
							<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
						</env>
					</image>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.springframework.experimental</groupId>
				<artifactId>spring-aot-maven-plugin</artifactId>
				<version>${spring-native.version}</version>
				<executions>
					<execution>
						<id>test-generate</id>
						<goals>
							<goal>test-generate</goal>
						</goals>
					</execution>
					<execution>
						<id>generate</id>
						<goals>
							<goal>generate</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
	<repositories>
		<repository>
			<id>spring-releases</id>
			<name>Spring Releases</name>
			<url>https://repo.spring.io/release</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
	</repositories>
	<pluginRepositories>
		<pluginRepository>
			<id>spring-releases</id>
			<name>Spring Releases</name>
			<url>https://repo.spring.io/release</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</pluginRepository>
	</pluginRepositories>

	<profiles>
		<profile>
			<id>native</id>
			<properties>
				<repackage.classifier>exec</repackage.classifier>
				<native-buildtools.version>0.9.8</native-buildtools.version>
			</properties>
			<dependencies>
				<dependency>
					<groupId>org.junit.platform</groupId>
					<artifactId>junit-platform-launcher</artifactId>
					<scope>test</scope>
				</dependency>
			</dependencies>
			<build>
				<plugins>
					<plugin>
						<groupId>org.graalvm.buildtools</groupId>
						<artifactId>native-maven-plugin</artifactId>
						<version>0.9.8</version>
						<extensions>true</extensions>
						<executions>
							<execution>
								<id>test-native</id>
								<phase>test</phase>
								<goals>
									<goal>test</goal>
								</goals>
							</execution>
							<execution>
								<id>build-native</id>
								<phase>package</phase>
								<goals>
									<goal>build</goal>
								</goals>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>

</project>

然后我们在启动类上添加一个 endpoint 进行返回

@SpringBootApplication
@Controller
public class ExperienceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ExperienceApplication.class, args);
	}

	@GetMapping("hello")
	@ResponseBody
	private String hello(){
		return "hello world";
	}

}

现在你可以通过直接点击 IDEA 的 run,这样的话,是通过本地的 Java 进行运行的,获得一个 JVM 版的启动时间。

这里我的电脑配置是

处理器	Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz   2.30 GHz
机带 RAM	32.0 GB (31.9 GB 可用)

启动的时间花费了 1.479 秒

image.png

打开浏览器,也是可以访问的

image.png

5.3 Spring Native 打包

接下来让我们进行 Spring Native 的打包工作。打开你的 win 下的 docker。

点击设置,将你的配置调高一点,等下打包的时候就会快一点

image.png

然后回到你的 IDEA,使用 cmd 窗口并进入到你的项目的目录

使用

mvn spring-boot:build-image

或者指定 maven 的路径

D:\maven\apache-maven-3.8.4-bin\apache-maven-3.8.4\bin\mvn clean -U -DskipTests spring-boot:build-image

进行构建,接下来就是漫长的等待过程了。当中可能会出现一些错误,比如

5.4 Execution default-cli of goal org.springframework.boot:spring-boot-maven-plugin:2.6.1:build-image failed: Builder lifecycle 'creator' failed with status code 145

image.png

此时你需要检查

  • 你的本地运行环境的 JDK 版本,要和项目一致。
  • 检查你的 IDEA 的项目设置的 JDK 是否正确。
  • 使用的 mvn 命令调用的 JDK 是正确的 JDK 版本
  • 使用了正确的 maven 版本,太低的是不行的

如果没有问题的话,你应该可以看到类似输出

image.png

都是正常的

这里要说明一下为啥需要把 Docker 的内存设置大点,因为你会发现输出内容中,占用的空间都是几个 G 的,如图

image.png

当看到 build success 的时候就是成功了

image.png

使用 docker images 可以看到刚刚打包好的镜像,让我们启动试试

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

image.png

0.045, 这启动速度如果是 JVM 真的打不了,到此为止就完成 Spring Native 的简单使用,如果想要深入体验还得看看他们的文档 Announcing Spring Native Beta!

6. 局限性

但是,什么事物都是有两面性的,那么对于 GraalVM 来说,好的一方面就是打包出来的体积更小,启动更快,占用的内存更小,让我不禁在想,以前一台 1 核 2G 的服务器部署一个应用就差不多,照 GraalVM,运行时占用才 50M。那我不就可以部署很多应用?而且性能还这么棒

可惜的是,

  • GraalVM 在打包的配置要求上挺高,Mac 上没一次打包成功的
  • 对于使用了反射的项目来说,需要在使用 GraalVm 构建 native image 前需要通过配置列出反射可见的所有类型
  • 对于 Spring Native 来说,现在任然是测试版,还没有能应用到生产环境的稳定版

但是我感觉这仍然是之后发展的一个趋势,在现在微服务大行其道的局面,Java 也需要一些东西来破局。说不定再过一两年,这个成熟稳定之后,我们在树莓派上都能部署起来企业级项目。

7. 参考内容

Announcing Spring Native Beta!

Oracle GraalVM Enterprise Edition

使用 graalvm 打包 maven 项目为 exe

如何评价 GraalVM 这个项目? - kelthuzadx 的回答 - 知乎

GraalVM native-image doesn't compile with Netty

8. 改造 Solo

当发现这个 GraalVM 之后让我挺兴奋的,马上想改造 Solo 博客,这样会让博客在服务器上的占用更低,顺便体验一下新玩意儿。可惜的是到现在为止仍然是卡在打包的时候,netty 中大量使用的反射的代码,导致打包失败。

然后我想 dubbo 底层不也是使用的 netty 吗,他们都可以打包成功,那我应该也可以,参考了他们的 guideline,给了一点想法,尝试之后仍然不行。

dubbo 项目支持 native-image

或许还需要再研究一阵子才能解决这个问题

类似这种配置加了接近一百多行

image.png

image.png

  • GraalVM
    3 引用
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    944 引用 • 1459 回帖 • 17 关注
1 操作
MingGH 在 2022-09-12 18:39:17 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...