使用 Graal Vm Native构建 Springboot微服务后记

# 尝试了一下 Java 的 GraalVM Native / AOT:确实更省内存、更快启动,但现阶段不适合我 #

把一个 Spring Boot 多模块后端(AxT Dashboard Backend)尝试用 GraalVM Native Image / AOT 编译成原生可执行文件。

结论先放在前面:

  • 运行层面很香:内存占用能做到 20-40MB,启动也明显更快。
  • 但构建层面很劝退:单模块 Native 构建至少 10 分钟,并且对机器要求很高(我这次测试环境 8C / 8G4G 会 OOM)。

所以目前我的策略是:方案跑通,形成脚本与配置,先归档。等未来真要全量转 Native,再把这套东西拿出来继续推进。

在此之前我也查询过很多资料,比如这篇Reddit讨论: GraalVM 是首选吗


1. 背景:我为什么要折腾 Native Image #

我最开始的动机其实很朴素:

  • GraalVM Native Image 与传统 JVM 内存管理:云原生时代的技术选型指南
  • Spring Boot 服务跑起来后,JVM 进程常驻内存不低(尤其一堆模块拆开跑)。
  • 我的 6个微服务模块,实测 docker 上机后单个容器 430MB+,刚启动3G内存没了
  • 为了节省成本,目前生产机器 4C4G,可能还需要蓝绿部署,双倍内存成本暴增
  • 看看 Native Image 能不能让后端服务在测试/轻量环境里“更轻”。
  • 高并发场景下内存的使用量甚至可以比肩Go语言

参考 2024年各编程语言运行100万个并发任务需要多少内存?

GraalVM Native Image 的卖点也很明确:

  • 更快启动(省掉 JIT 预热)
  • 更低内存(很多场景能显著下降)

但它的代价同样出名:

  • 构建慢
  • 兼容性坑多(反射、动态代理、资源加载、Spring Cloud 特性……)

我这次做的不是“看文档跑 demo”,而是直接把它落到真实项目上:

  • 后端是 Maven 多模块
  • Spring Boot 3.5.x + Java 17
  • 用了 Spring Cloud Config(配置中心)

2. 在选 Native 之前,我做过什么 #

在决定上 Native 之前,我其实是按“正常工程思路”先评估/尝试了一圈:

  • 先接受 JVM 的现实:服务多模块拆开跑,单个服务不大,但整体常驻内存并不低。
  • 先把可观测性拉起来:关注的核心指标很明确:
    • 启动时间
    • 空闲内存占用
    • 压测/正常流量下的稳定性
  • 再看“跑得更省”的路线:我需要的是能在测试环境更轻量地跑起来,而不是极致吞吐。
  • 尝试通过JVM参数缩小占用: 使用了 -Xms,-Xss,-XX:MaxMetaspaceSize 等一系列限制,但内存占用仍然没有可观的减少,反而会导致启动慢的特性

最终我选择 Native 的原因也很简单:

  • 我更在意“启动更快更低内存”这两个点。
  • 我可以接受“先把路跑通、先归档”的策略,而不是一步到位全量迁移。

3. 环境与目标 #

3.1 构建环境 #

  • OS: Ubuntu 24.04
  • CPU: 8 Core
  • RAM: 8 GB(实践证明 4 GB 会 OOM)
  • GraalVM: 17.0.9-graalce
  • Maven: 3.9+

3.2 目标 #

我希望做到:

  • 在远程编译机上完成单个模块的 Native Image 构建
  • 编译成功后直接输出可执行文件路径

4. 首要问题: AOT 阶段会“启动一部分应用”,Config Server 连接直接炸 #

Native Image 构建阶段(更准确地说是 AOT processing)会触发 Spring Boot 的一系列初始化。

而我的服务用了 Spring Cloud Config:

  • 默认会去连 http://localhost:28610

构建机上当然没有 Config Server,于是报错:

  • ConfigClientFailFastException
  • 连接被拒绝

5.1 我当时的疑问 #

“是不是必须完整启动一遍服务才能构建?”

答案是:

  • 不需要完整启动
  • 但 AOT 阶段确实会做很多启动期事情,所以像 Config Client 这种“引导期外部依赖”,会在构建时就触发。

4.2 解决方案:让 Config Server 变成“可选” #

这里我踩到的关键点是:AOT 期间会触发引导期配置加载。如果引导期就强依赖外部服务(例如 Config Server),构建就会直接失败。

我的修复做法是两步:

  1. 把引导期导入改为可选:
spring:
  config:
    import: "optional:configserver:"
  1. 关闭 fail-fast(让“连不上配置中心”变成 warn,而不是直接终止):
spring:
  cloud:
    config:
      fail-fast: false

这样构建时即使 Config Server 不在线,也只会打印 WARN,不会把 AOT 直接打断。

  • 只改 fail-fast 不一定够
  • optional:configserver: 才是让引导期导入真正“可选”的开关

5. 下一波坑:@RefreshScope 这类 Spring Cloud 特性与 Native 不友好 #

Config Server 的连接问题解决后,AOT 继续往下跑,又遇到:

  • AotBeanProcessingException
  • 出错 bean:OAuthProperties

我定位到这个配置类上有:

  • @RefreshScope

@RefreshScope 本质是运行时动态刷新 + 代理增强(用于运行时动态刷新配置服务),在 Native Image 下经常会踩到 AOT/代理/反射相关坑。

5.1 解决方案 #

在这次“先把构建跑通”的阶段,我选择了最直接的处理:

  • 移除 @RefreshScope(至少先让 AOT 能过,再谈更优雅的方案)

6. 反射配置:reflect-config.json 还是要准备 #

即使 Spring Boot 3 对 AOT/Native 已经友好很多,但项目里只要有:

  • 反射
  • Jackson 序列化
  • 一些框架自动装配

就很容易遇到运行期找不到构造器/字段之类的问题。

这次为了快速跑通,我把本次验证过的一份反射配置也一起整理归档(方便后续复用/扩展)。


7. 最终结果:确实能跑,但构建成本太高 #

8.1 结果 #

  • ✅ Native Image 编译成功
  • ✅ 可执行文件能跑起来
  • ✅ 内存占用明显下降:20-40MB
  • ✅ 启动速度更快(非常明显,几乎全程加载完10s内,正常JVM则1m左右)

8.2 现实问题(也是我选择暂缓的原因) #

  • ❌ 构建时间太长:单模块至少 10 分钟
  • ❌ 构建资源消耗太大:
    • 8C/8G 才比较顺
    • 4G 直接 OOM(这点对我的测试/CI环境不友好)
    • 构建出的二进制文件比原jar文件大了3倍

因此对我当前阶段来说:

  • Native Image 不是不能用
  • 前期投入产出比不高,尤其我这里还是多模块(模块越多,构建成本线性甚至更糟)。

8. 归档:把“跑通所需的一切”统一收口 #

我最终决定把这套方案“归档而不是删除”,原因很简单:

  • 以后如果真的要全量转 Native,这些踩坑经验能直接复用

归档内容主要包括几类:

  • 构建脚本:远程构建、输出产物路径
  • POM 片段:如何启用 native profile / 插件
  • 反射配置示例:本次验证通过的基础版本
  • 关键配置改动清单:AOT/Native 相关的必要调整点

同时,为了不影响日常生产/开发体验,我也做了回滚/恢复:

  • 恢复 @RefreshScope
  • 恢复 application.yml 的 config server 导入方式
  • 移除根 POM 的 native profile
  • 移除模块 POM 的 native 插件
  • 清理模块内的 META-INF/native-image(避免长期污染)

9. 我对“是否要上 Native Image”的个人建议 #

如果是下面情况,Native Image 会很香:

  • 需要极致启动速度(例如 serverless / 弹性扩缩容特别频繁)
  • 机器内存特别紧张
  • 项目对 Spring Cloud 依赖不重(或者你愿意做适配/替换)

如果是下面情况,建议先观望:

  • 多模块 + 依赖复杂 + Spring Cloud 特性多
  • CI 资源一般(尤其 RAM < 8G)
  • 更在意“构建效率/迭代速度”而不是“运行时极致性能”

我这次的选择就是:

  • 现在先不全量上
  • 把路走通并归档

等未来时机成熟(比如:模块更少、配置中心方案调整、CI 机器更强、业务更需要低内存/快启动),再把它捡起来继续。