从零放弃学习 Spring - Native Image 与 Spring
GraalVM Native Image
GraalVM 是 Oracle 开发的 JDK,本身就提供了更好性能的 JVM,GraalVM Native Image 可以通过 AOT 把 Java 代码直接编译成二进制可执行文件,编译后的二进制不再依赖 JVM,更快的启动速度,更小的运行内存,并且不会对性能有大影响。
Native Image 会执行静态分析,从应用入口(即 main)开始,找到所有会被访问的方法,并且只会编译找到的代码。完全按照 JVM 执行的逻辑去静态分析是不现实的,所以 Native Image 静态分析基于一个叫 封闭世界假设(closed-world assumption) 的约束:只考虑构建时的已知代码,不允许在运行时增加加载新代码。
下载安装 GraalVM 后你还需要安装必要的构建工具和 z-lib,例如 Ubuntu 下:
1 | apt-get install build-essential zlib1g-dev |
Native Image 构建 GRPC 应用
完整代码参考 native-grpc。
代码基本上就是 grpc-from-scratch,为了 Native Image 编译简单,去掉了 logback。
对于这种比较简单的项目,只需要简单修改 pom.xml 就可以了:
1 | <build> |
使用 Maven 构建可执行文件:
1 | mvn native:compile |
这里有一个参数 --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils
,如果不加上的话会出现 Unable to initialize BouncyCastleAlpnSslUtils
异常。不过因为我们代码里没有使用加密,这个异常不会影响代码正常运行。关于 run-time 和 build-time 概念我就不翻译文档了,参考 Build Time vs Run Time。
在日常开发时可以加入
-Ob
构建参数加速构建
Build Configuration
Native Image 可以通过 Build Configuration 指示正确的构建行为。
官方推荐的布局如下:
1 | META-INF/ |
Native Image 构建会扫描 META-INF/native-image
目录下的构建配置 native-image.properties
,使用 groupID 和 artifactID 是为了多个模块都可能生成各自的配置文件,构建时要避免它们产生冲突,但很扯的是,如果你按照这个方式放置你自己项目需要的构建配置,可能会和插件为你的项目生产的配置文件冲突。
元数据配置
Native Image 静态分析没办法处理 Java 动态特性,如果用到了这些动态特性,要在构建配置中添加对元数据的配置。
当然,手动编写这类配置是非常繁琐的,所以 GraalVM 提供了一个 Agent 自动收集元数据信息,并且生成对应的配置文件,使用方法如下:
1 | mvn package |
上面命令会把结果生成到 target/out-config 下,把这些文件复制到 META-INF/native-image
下就可以了。
官方文档 中建议结合
config-merge-dir
多次执行以覆盖到更全面的动态特性。
Native Image 构建 Spring Boot 应用
像 Spring 这样的框架往往就会用到很多动态特性,所以会涉及到大量构建配置,好在 Spring 已经支持 Native Image,只需要引入 GraalVM Native Support
构建插件,开箱即用:
1 | mvn -Pnative native:compile |
指定 native
Profile,Spring 构建插件会在构建时自动生成需要的元数据配置,无需任何额外配置。
除了上面这种方式构建 Spring 项目为可执行文件,Spring 构建插件还支持直接构建出含可执行文件的 docker 镜像:
1 | mvn -Pnative spring-boot:build-image |
使用这种方式构建(默认)会下载 paketobuildpacks/builder:tiny
镜像作为 builder 镜像,并且它自己会准备一套构建环境,例如 BellSoft Liberica JDK,native-image 等等,构建完成后还会基于 paketobuildpacks/run:tiny-cnb
镜像制作应用镜像,paketobuildpacks/run 是一个类似 distroless 的精简镜像,最后进程运行在用户 1000 上。
PGO
Native Image 支持 PGO,虽然官方文档中说社区版不可用。
下面是官方文档中示例代码的运行结果,运行环境是腾讯云 S6.2XLARGE16,SDK 是 graalvm-jdk-17.0.8+9.1(不用在意数据,我在后面总结了):
1 | $ javac Streams.java |
可以看到 GraalVM JIT 的性能极限是 22ms,PGO 前耗时 141-159ms,PGO 后耗时完全稳定在 30-31ms。我还测了使用 --gc=G1
和 -march=native
的情况,如果单用任何一个选项,性能是不会有大变化的,但是两个选项一起用以后,性能下降了一点,这点让我非常疑惑。
--pgo-instrument
为了生成 default.iprof,但是在一次测试中我发现 checksum 值变了,不知道是什么原因。
代码参考 native-pgo。
最后
根据我之前测试的观察,Native Image 构建后的可执行文件启动内测开销 60MiB,对应 JVM 上运行时 210MiB,执行过 bench 后 Native Image 版内存 80MiB,对应 JVM 上 600MiB,虽然性能会比 JIT 极限性能差,至少内存节约上时非常可观的。
不过 Native Image 在构建时 CPU 和内存开销非常大,云上 8 核 16GiB 机器构建一个简单的 GRPC 的应用,native-image 执行耗时 2min(其中大部分时间 CPU 处于满负荷),最高时内存开销 5GiB。如果想在 CI 中集成也必须考虑到构建机器的成本,并发任务的限制还有其他开销。
参考
- https://www.graalvm.org/latest/reference-manual/native-image/metadata/Compatibility/
- https://www.graalvm.org/latest/reference-manual/native-image/basics/#static-analysis
- https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/ClassInitialization/
- https://www.graalvm.org/latest/reference-manual/native-image/overview/BuildConfiguration/
- https://medium.com/graalvm/working-with-native-image-efficiently-c512ccdcd61b
- https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection
- https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Reflection/
- https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
- https://cn.dubbo.apache.org/zh-cn/blog/2023/06/28/%E8%B5%B0%E5%90%91-native-%E5%8C%96springdubbo-aot-%E6%8A%80%E6%9C%AF%E7%A4%BA%E4%BE%8B%E4%B8%8E%E5%8E%9F%E7%90%86%E8%AE%B2%E8%A7%A3/