| 导语:由于笔者工作项目上的平台产品特性设计原因,用户在平台产品使用过程中会产生数量不少的 Java Spring Boot 微服务,加上 Java 服务本身资源消耗大(尤其内存消耗),造成资源成本很高。因此考虑调研将 Java Spring Boot 服务包编译为本地可运行程序的方式,记录亦供参考。
参考:
- Announcing Spring Native Beta! ,官宣 Beta 版;
- New AOT Engine Brings Spring Native to the Next Level , 最新进展, 当前 0.11 版本了,而且在 Spring Boot 3 中会有更好的支持;见 https://github.com/spring-projects/spring-boot/milestones 这里,Spring Boot 3 会在 2022 年 11 月正式发布;
- Spring Native documentation, 官方文档,本文体验例子来自该文档;
- Spring Native 中文文档 ,内容比较多,但也称不上“中文文档”
1. 概述
Spring Native 是 Spring 团队和 GraalVM 团队合作的成果,可以将 Spring 应用通过 AOT(Ahead-of-Time,预先编译)技术编译为 Native Image(本地可执行程序,不是指容器镜像),从而获得快速启动、低内存消耗、即时峰值性能等特性,这样的特性在云原生时代显得尤为重要,但相应代价是编译构建时间更长。
Spring Native 的相关特性以及 GraalVM 的介绍网上已有不少,详见参考文档,本文主要记录体验过程的一些细节以及效果对比。代码示例见附件。
2. Spring Native 体验过程记录
2.1 环境
体验测试都在 MacBook Pro 上,
芯片: M1 Pro,16c,
内存: 32g,
系统: macOS Monterey,Version 12.3.1,
GraalVM: 22.0.1 版本
JDK:openlogic-openjdk-11.jdk,JDK 11 版本
(补充:注意,如果是 Mac M1 芯片,GraalVM、JDK 使用 amd64 版本和 aarch64 版本性能会相差很多,aarch64 芯片架构版本原生支持 M1。本文一开始使用 amd64 的版本,发现出来的数据比之前在旧 MacBook Intel 芯片下的数据要差,后来改使用 aarch64 版本,各项数据要好很多。openlogic-openjdk-11.jdk 找不到 aarch64 版本的,改使用 zulu 构建版本,传送。)
2.2 GraalVM 安装
需要先安装 GraalVM 和配置 GRAALVM_HOME 环境变量(如 macOS 下 GRAALVM_HOME=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-22.0.0.2/Contents/Home),否则编译 Spring Native 应用时会提示出错:
代码语言:javascript复制[ERROR] Failed to execute goal org.graalvm.buildtools:native-maven-plugin:0.9.10:build (build-native) on project rest-service-complete: Execution build-native of goal org.graalvm.buildtools:native-maven-plugin:0.9.10:build failed: GraalVM native-image is missing from your system.
[ERROR] Make sure that GRAALVM_HOME environment variable is present.
按照 https://www.graalvm.org/22.0/docs/getting-started/macos/ 指引进行安装,下载传送。
注意将解压包 mv 到对应位置后需要先执行 sudo xattr -r -d com.apple.quarantine /path/to/GRAALVM,注意路径后面不需要 Contents/Home 一截,否则如果先使用 GRAALVM 会提示程序损坏,即使后面补执行 xattr 也一样。
2.3 OpenJDK 11 安装
GraalVM 支持 Java 11、Java 15 或 Kotlin 1.5 , 不支持 Java 8。
使用了 OpenLogic Build 版本 OpenJDK 11,下载传送。 同样解压到对应位置,设置 JAVA_HOME(如 macOS 下 JAVA_HOME=/Library/Java/JavaVirtualMachines/openlogic-openjdk-11.jdk/Contents/Home),否则如果使用了低版本 Java,会提示错误:
代码语言:javascript复制org/springframework/aot/maven/TestGenerateMojo has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0
2.4 Spring Native 依赖及编译工具
体验测试代码见,只是简单的尝试,所以没有包含反射等特性,只测试简单的 Hello 接口和 OpenFeign 服务互调。
所以相比原本的 SpringBoot 应用不需要改动代码,只是在 pom.xml 文件中多加一个名为 native 的 profile。且当前 spring-native 0.11.3 版本只支持 Spring Boot 2.6.4 版本,所以需要注意 Spring Boot 和 Spring Cloud 版本的设置。如下:
代码语言:html复制 <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<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>
<profiles>
<profile>
<id>native</id>
<dependencies>
<!-- 运行Spring Native所需的运行时依赖,还提供了Native hints API-->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.4</version>
</dependency>
<!-- Required with Maven Surefire 2.x -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- AOT 转换的 Maven 插件-->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.4</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>
<!-- 提供编译和测试 native image 的支持,
see: https://graalvm.github.io/native-build-tools/latest/maven-plugin.html -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.11</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>build</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</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>
2.5 编译打包及对比
指定 -Pnative 使用 native profile 进行编译
代码语言:shell复制GRAALVM_HOME='/Library/Java/JavaVirtualMachines/graalvm-ce-java11-22.0.0.2/Contents/Home' JAVA_HOME='/Library/Java/JavaVirtualMachines/openlogic-openjdk-11.jdk/Contents/Home/' mvn -Pnative -DskipTests package
编译过程长达 2min43s,主要时间消耗在 native-maven-plugin:0.9.10:build 即构建本地镜像过程中,且内存消耗也很大,最高达 8.15GB。 因此需要注意构建时分配多一些内存,防止出现 OOM。
native-maven-plugin:0.9.10:build 过程输出也很有意思,可以看到分为 7 个步骤,Performing analysis 分析阶段耗时最长,Compiling methods 编译方法次之。
代码语言:shell复制GraalVM Native Image: Generating 'rest-service-complete'...
========================================================================================================================
[1/7] Initializing... (14.5s @ 0.44GB)
Warning: Could not register org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer: allDeclaredConstructors for reflection. Reason: java.lang.NoClassDefFoundError: org/springframework/jdbc/CannotGetJdbcConnectionException.
Warning: Could not register org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer: allDeclaredConstructors for reflection. Reason: java.lang.NoClassDefFoundError: javax/validation/ValidationException.
Warning: Could not register org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer: allDeclaredConstructors for reflection. Reason: java.lang.NoClassDefFoundError: liquibase/exception/ChangeLogParseException.
Version info: 'GraalVM 22.0.0.2 Java 11 CE'
The bundle named: org.apache.tomcat.util.threads.res.LocalStrings, has not been found. If the bundle is part of a module, verify the bundle name is a fully qualified class name. Otherwise verify the bundle path is accessible in the classpath.
[2/7] Performing analysis... [**********] (68.0s @ 2.82GB)
Warning: Could not register complete reflection metadata for org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer. Reason(s): java.lang.NoClassDefFoundError: org/springframework/jdbc/CannotGetJdbcConnectionException
Warning: Could not register complete reflection metadata for org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer. Reason(s): java.lang.NoClassDefFoundError: liquibase/exception/ChangeLogParseException
Warning: Could not register complete reflection metadata for org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer. Reason(s): java.lang.NoClassDefFoundError: javax/validation/ValidationException
Warning: Could not register complete reflection metadata for org.springframework.validation.beanvalidation.SpringValidatorAdapter$ViolationFieldError. Reason(s): java.lang.NoClassDefFoundError: javax/validation/Validator, java.lang.NoClassDefFoundError: javax/validation/ConstraintViolation
14,554 (90.03%) of 16,165 classes reachable
24,076 (75.73%) of 31,792 fields reachable
71,665 (62.74%) of 114,217 methods reachable
734 classes, 186 fields, and 3,373 methods registered for reflection
68 classes, 89 fields, and 55 methods registered for JNI access
[3/7] Building universe... (4.8s @ 3.84GB)
[4/7] Parsing methods... [**] (2.9s @ 5.21GB)
[5/7] Inlining methods... [*****] (5.4s @ 3.52GB)
[6/7] Compiling methods... [******] (33.0s @ 3.95GB)
[7/7] Creating image... (6.1s @ 3.11GB)
29.39MB (42.70%) for code area: 47,456 compilation units
34.04MB (49.46%) for image heap: 10,153 classes and 430,771 objects
5.39MB ( 7.83%) for other data
68.82MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area: Top 10 object types in image heap:
1.59MB sun.security.ssl 12.88MB byte[] for general heap data
1005.81KB java.util 3.56MB java.lang.Class
941.17KB com.oracle.svm.core.reflect 2.94MB java.lang.String
673.61KB com.sun.crypto.provider 2.50MB byte[] for java.lang.String
608.80KB org.apache.tomcat.util.net 1.52MB java.util.LinkedHashMap
529.34KB org.apache.catalina.core 1.10MB java.lang.reflect.Method
495.82KB sun.security.x509 598.50KB java.util.HashMap$Node
474.78KB org.apache.coyote.http2 565.13KB java.lang.String[]
451.74KB org.aspectj.weaver.patterns 554.10KB com.oracle.svm.core.util.LazyFinalReference
428.94KB java.util.concurrent 463.83KB byte[] for method metadata
... 627 additional packages ... 2962 additional object types
(use GraalVM Dashboard to see all)
------------------------------------------------------------------------------------------------------------------------
9.8s (6.9% of total time) in 42 GCs | Peak RSS: 8.15GB | CPU load: 3.78
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
/Users/schelling/IdeaProjects/spring-boot-native-image/target/rest-service-complete (executable)
/Users/schelling/IdeaProjects/spring-boot-native-image/target/rest-service-complete.build_artifacts.txt
========================================================================================================================
Finished generating 'rest-service-complete' in 2m 19s.
[INFO]
[INFO] --- spring-boot-maven-plugin:2.6.4:repackage (repackage) @ rest-service-complete ---
[INFO] Attaching repackaged archive /Users/schelling/IdeaProjects/spring-boot-native-image/target/rest-service-complete-0.0.1-SNAPSHOT-exec.jar with classifier exec
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 02:43 min
[INFO] Finished at: 2022-05-04T18:01:13 08:00
[INFO] Final Memory: 41M/174M
[INFO] ------------------------------------------------------------------------
与之相比,如果不使用 native 方式编译,耗时在 4s 左右。
代码语言:shell复制~ JAVA_HOME='/Library/Java/JavaVirtualMachines/openlogic-openjdk-11.jdk/Contents/Home/' mvn -DskipTests package
...
[INFO] Total time: 4.140 s
...
编译完同时生成本地可执行程序和 jar 包,可以看到本地可执行镜像也不小,有 64MB, jar 包反而要小些,这也正常,毕竟 jar 包还需要 jvm。
2.6 运行对比
(1) Native Image 运行
直接运行可执行文件,启动提示为 0.663s ,启动后内存占用 71.4MB。
(很奇怪的是,之前在 intel 芯片的 MacBook Pro 上测试的数值没有这么高,猜测跟 M1 芯片下 Rosetta 转化有关系,待确认。之前在 intel 芯片上 ,启动完差不多 21MB,调用了几次接口后是 23.7MB。启动非常快,几毫秒。)
(2) Java 包运行
与之相比,直接 java -jar 运行 jar 包方式的话,启动提示为 4.295s , 启动后内存占用 513.3MB,对比差别挺大。
(之前在 intel 芯片上 使用 jar 包启动的,启动完是 191.3MB,而且启动接近 1s。 同样待确认)
(3) 测试服务间互调-使用 OpenFeign
尝试写了一个 Provider 和 一个 Comsumer,直接 @FeignClient
中指定 url,可以调通。
且发现 java -jar -Dspring.profiles.active=8090 rest-service-complete-0.0.1-SNAPSHOT.jar 这样的启动中传入参数,在 native image 方式下,也可以传入参数,即 ./rest-service-complete -Dspring.profiles.active=8090 可运行成功。
(4) Mac M1 芯片下使用 aarch64 版本 GraalVM 和 JDK
(补充) 上面猜测可能由于 M1 芯片下使用非 M1 芯片版本的 GraalVM 和 JDK,影响构建和运行性能,因为查找下载了针对 M1 芯片的 aarch64 版本,各项目数据相比非 aarch64 版本下的要好很多,补充记录于“对比总览”中。
2.7 对比总览
原 Java (非 arrch64 版本) | Spring Native (非 arrch64 版本) | 原 Java (aarch64 版本) | Spring Native (aarch64 版本) | |
---|---|---|---|---|
编译时间 | 4.140s | 163s | 1.403s | 60s |
包/可执行文件大小 | 27MB | 64MB | 27MB | 64MB |
启动时间 | 4.295s | 0.663s | 1.157s | 0.051s |
启动后内存占用 | 513.3MB | 71.4MB | 227.1MB | 36.5MB |