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

1,494 阅读7分钟

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的回答 - 知乎

Native Build Tools

GraalVM native-image doesn't compile with Netty

8. 改造Solo

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

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

dubbo项目支持native-image

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

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

image.png

image.png

文章首发于菠萝的博客