近几年“原生”一词一直泛滥在云计算、边缘计算等领域中,而原生宠幸的语言也一直都是Golang,Rust等脱离Sandbox运行的开发语言。Java得益于上世纪流行的一次编译,到处执行的理念,流行至今,但也因为这个原因,导致Java程序脱离不了JVM运行环境,使得不那么受原生程序的青睐。在云原生泛滥的今天,臃肿的JVM使Java应用程序对比其他语言显得无比的庞大,各路大神也想了很多方式让Java变的更“原生”。最近Spring推出了Spring Native概念,并参考了其他大牛的文章后,今天我们就一探如何让用Spring Boot编写原生应用。
Spring Native借助GraalVM native-image编译器来编译Spring应用,所以我们需要先来了解一下GraalVM。大部分脚本语言或者有动态特效的语言都需要一个语言虚拟机运行,比如CPython,Lua,Erlang,Java,Ruby,R,JS,PHP,Perl,APL等等,但是这些语言的虚拟机水平参差不齐,例如JVM的HotSpotVM、JS的V8都是“艺术”级别的,但CPython的VM就不忍直视。那能不能用一个“艺术”级别的虚拟机跑所有的语言呢?GraalVM就是这么一个高性能的救世主,它使用运行在JVM上的Truffle语言框架,将AST节点编译为机器代码,使用户只需要实现具体语言AST解释器,就能实现性能足够好的虚拟机,而实现这个编译器也是一个Java写的即时编译器Graal,GraalVM也因此得名。
也许有同学会问了怎么用Java语言编译Java代码呢,而且还是这么高性能?这我们就要说说JEP 243的JVMCI。众所周知,HotSpot JVM内置了两个C++写的即时编译器(JIT)C1和C2,一般频繁的代码先用C1编译,如果热点继续,那么会使用C2编译。JVMCI相当于把本该交给C2编译的代码交给高级JIT:Graal编译,说到底就是将一段byte[]在运行时换成另一段byte[]。
那像Go和C/C++这类语言是否也能运行在JVM上呢?答案是肯定的。解决方案是将C/C++这些语言用一些工具(如Clang)转换为LLVM IR,然后使用基于Truffle的AST解释LLVM IR即可。(但,我们为啥要这么做??)
到目前为止,几乎所有的语言都能在以JVM为基础,以Graal即时编译器为核心的虚拟机上运行起来了,但大家已经一定疑惑了,程序运行需要依赖JVM,而JVM必须提前安装JDK环境,而且自身启动慢,内存负载高,就不能把程序直接打包成平台相关可执行文件吗?答案是SubstrateVM,它借助Graal编译器,可以将Java程序AOT编译为可执行程序。所以万能的Graal编译器不仅能JIT,还能AOT。
好了,我们这些“CRUD仔”们了解这些基础魔法就足够了,至于SVM如何解决反射、GC等问题的高级魔法还是交给大牛们吧。现在进入我们的正题:用Spring Boot来编写一个原生应用。
制作过程
Step 1:安装GraalVM和依赖工具
因为大家都比较熟悉JDK安装过程,所以本过程带过了一些细节,不做重点讲解。首先我们需要安装GraalVM,笔者以自己的macOS系统为例,其他系统请参考官方安装文档。比较遗憾的是,GraalVM并没有提供针对M1优化的AArch64平台的包,我们只能使用AMD64平台,下载地址点击这里[1],我们使用Java 17版本的darwin压缩包,解压至:
1/Library/Java/JavaVirtualMachines/
并且设置JAVA_HOME:
1
2export GRAALVM17_HOME=$(/usr/libexec/java_home -v 17) export JAVA_HOME=$GRAALVM17_HOME
为了使用方便也可以设置Alias:
1alias java17g='export JAVA_HOME=$GRAALVM17_HOME;java -version'
由于macOS的安全限制,需要删除quarantine:
1$ sudo xattr -r -d com.apple.quarantine $GRAALVM17_HOME
我们依然需要Maven作为本次探索的打包工具,请大家自行安装Maven,这里不再赘述。一切安装完成,我们可以运行java -version和mvn -v来验证一下安装是否成功。
1
2
3
4$ java -version openjdk version "17.0.1" 2021-10-19 OpenJDK Runtime Environment GraalVM CE 21.3.0 (build 17.0.1+12-jvmci-21.3-b05) OpenJDK 64-Bit Server VM GraalVM CE 21.3.0 (build 17.0.1+12-jvmci-21.3-b05, mixed mode, sharing)
最后,我们需要安装native-image作为原生代码编译工具:
1
2$ cd $GRAALVM_HOME/bin $ ./gu install native-image
当然,Xcode工具包因为包含GCC等工具,也必须安装,如已经安装可跳过。
1$ sudo xcode-select --install
Step 2:建立Spring Boot应用
按着官方的向导建立一个基于Spring Boot 2.6.2版本,Java版本使用1.8的Web应用。注意一定要使用最新的2.6.2+版本,否则不支持AOT功能。并且,Java版本也只支持1.8。目录如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20. ├── HELP.md ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── ajk │ │ │ └── testspringnative │ │ │ └── TestSpringNativeApplication.java │ │ └── resources │ │ ├── application.yml │ │ ├── static │ │ └── templates │ └── test │ └── java │ └── com │ └── ajk │ └── testspringnative │ └── TestSpringNativeApplicationTests.java
其中TestSpringNativeApplication代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class TestSpringNativeApplication { public static void main(String[] args) { SpringApplication.run(TestSpringNativeApplication.class, args); } @GetMapping("/hello") public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { return String.format("Hello %s!", name); } }
配置文件application.yml代码如下:
1
2
3
4
5
6
7
8
9
10
11server: port: 9000 shutdown: graceful spring: profiles: active: default logging: level: root: info
Step 3:配置Maven
为了方便演示,我们使用了最简单的代码和配置,重点是Maven的配置,以至于我需要用整个Step来说明。
由于使用了官方向导生成的项目,所以基础pom.xml文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38<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.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ajk</groupId> <artifactId>test-spring-native</artifactId> <version>0.0.1-SNAPSHOT</version> <name>test-spring-native</name> <description>test-spring-native</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
接下来我们开始配置Spring Boot Native,官方有两种方式实现编译原生应用:
用Spring Boot Buildpacks生成包含原生应用的OCI容器。
用GraalVM native image Maven plugin生成原生应用。
由于篇幅关系,这里只介绍第二种方式,即编译为原生应用。
首先增加包和插件依赖库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<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>
再次确认我们的Spring Boot版本为2.6.2(因为Spring Native 0.11.1版本支持此版本),并添加如下依赖:
1
2
3
4
5
6
7
8<dependencies> <!-- ... --> <dependency> <groupId>org.springframework.experimental</groupId> <artifactId>spring-native</artifactId> <version>0.11.1</version> </dependency> </dependencies>
添加Spring AOT部署插件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<build> <plugins> <!-- ... --> <plugin> <groupId>org.springframework.experimental</groupId> <artifactId>spring-aot-maven-plugin</artifactId> <version>0.11.1</version> <executions> <execution> <id>generate</id> <goals> <goal>generate</goal> </goals> </execution> <execution> <id>test-generate</id> <goals> <goal>test-generate</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
再添加原生编译插件,这里使用一个profile来更好的管理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35<profiles> <profile> <id>native</id> <build> <plugins> <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>0.9.9</version> <extensions>true</extensions> <executions> <execution> <id>build-native</id> <goals> <goal>build</goal> </goals> <phase>package</phase> </execution> </executions> <configuration> <!-- ... --> </configuration> </plugin> <!-- Avoid a clash between Spring Boot repackaging and native-maven-plugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <classifier>exec</classifier> </configuration> </plugin> </plugins> </build> </profile> </profiles>
一切妥当!开始编译吧!
1$ mvn clean -Pnative -DskipTests package
官方推荐编译的机器不能少于8核8G内存,否则编译工具会报错。在我的M1的机器上,编译大概需要10分钟左右,编译时CPU峰值使用率大概在50%,内存占用6.9GB。
简单评测
首先看一下编译文件大小对比:
fatjar包文件为17.8M(不包含JRE),原生可执行文件为68.2M。
使用spring-boot-maven-plugin生成包含JRE运行环境的容器镜像大小为270M,而使用Tiny Core Linux+原生应用的形式,镜像大小可以控制在100M以内,为96M。压缩比达到35%之多。
再来看看启动速度对比:
fatjar启动时间为8.2s
原生文件启动时间为5.6s
程序使用CPU和内存对比:
fatjar空载CPU 0.5%,内存使用528M
原生应用空载CPU 0.3%,内存使用85M
如下表格:
FatJar包 | Native包 | |
应用大小 | 17.8M | 68.2M |
容器镜像大小 | 270M | 96M |
启动速度 | 8.2s | 5.6s |
空载CPU | 0.5% | 0.3% |
空载内存 | 528M | 85M |
总体来讲,原生应用从产物大小,启动速度,运行负载来讲都优与Jar包应用,这还是在没有针对arm的指令集做优化的基础上的,但对比官方宣传的内存使用20M内存占用还有一定差距。
总结
经过几天折腾,GraalVM的性能即使不编译为原生应用也优于HotSpot VM,在编译为原生应用后,性能也有一定的提升。但目前Spring Native还不够成熟,笔者想用undertow代替Tomcat Web容器而编译后的原生应用,始终无法运行。相信后面版本应该会修复一些问题。
本文总结了一种编译原生的方式,另一种生成原生镜像的方式大家可以自行研究(注意,编译成原生镜像需要阅读大量文章)。另外,由于时间有限,在两者的压测过程中,原生应用GC回收内存速度快于jar包应用,大家也可以深入研究原生内存回收方式。
所有代码可在GitHub[2]上参考。
相关链接:
https://github.com/graalvm/graalvm-ce-builds/releases
https://github.com/huang-kai/test-spring-native
参考资料:
https://www.graalvm.org/docs/introduction/
https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/
https://openjdk.java.net/jeps/243
http://trufflesuite.com/truffle/
https://github.com/graalvm/labs-openjdk-17
作者:黄凯,58安居客新房技术部负责人。
Kubernetes线下实战与CKA培训
本次培训在深圳开班,基于最新考纲,理论结合实战,通过线下授课、考题解读、模拟演练等方式,帮助学员快速掌握Kubernetes的理论知识和专业技能,并针对考试做特别强化训练,让学员能从容面对CKA认证考试,使学员既能掌握Kubernetes相关知识,又能通过CKA认证考试,理论、实践、考证一网打尽,学员可多次参加培训,直到通过认证。点击下方图片或者阅读原文链接查看详情。
最后
以上就是耍酷羽毛最近收集整理的关于Spring Native初探的全部内容,更多相关Spring内容请搜索靠谱客的其他文章。
发表评论 取消回复