Spring Native 初体验及对比

| 导语:由于笔者工作项目上的平台产品特性设计原因,用户在平台产品使用过程中会产生数量不少的 Java Spring Boot 微服务,加上 Java 服务本身资源消耗大(尤其内存消耗),造成资源成本很高。因此考虑调研将 Java Spring Boot 服务包编译为本地可运行程序的方式,记录亦供参考。

参考:

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 应用时会提示出错:

[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,会提示错误:

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 版本的设置。如下:

    <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 进行编译

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 编译方法次之。

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 左右。

~ 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

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
Spring Native 初体验及对比
| 导语:由于笔者工作项目上的平台产品特性设计原因,用户在平台产品使用过程中会产生数量不少的 Java Spring Boot 微服务,加上 Java 服务本身...
<<上一篇
下一篇>>