GraalVM build Native Image 踩坑

前言 #

GraalVM 我們可以粗暴理解為一種新生代的 JVM,擁有更好的處理效率
並且可以透過 native-image 工具去構建 Native Image
Native Image 可以想做是可以在 OS 上原生執行的可執行檔,不需要 JDK 參與
基本上在什麼環境 build,就只能在什麼環境上執行

踩坑 #

參考 Spring 官方文件,推薦使用 gradle 或 maven 來幫助構建:
GraalVM Native Image Support

因為原生的 native-image 指令基本上要給太多的 Hint
使用 Spring 的話天生就有 gradle 或 maven plugin 的整合,使用 plugin 及對應的指令去執行構建,會自動在構建時產出相對應的 hint
為什麼需要有 Hint 上述官方文件當中也有說明:

除了生成源文件,Spring AOT引擎还将生成hint文件,供GraalVM使用。hint文件包含JSON数据,描述了GraalVM应该如何处理它无法通过直接检查代码来理解的东西。
例如,你可能在一个私有方法上使用Spring注解。为了调用私有方法,Spring需要使用反射,即使是在GraalVM上。当出现这种情况时,Spring可以编写一个反射hint,这样GraalVM就知道即使没有直接调用该私有方法,它仍然需要在本地镜像中可用。
Hint文件是在 META-INF/native-image 下生成的,GraalVM会自动接收这些文件。

以 maven 為例,在 Spring boot 專案中一行以 mvn clean -Pnative native:compile 做到的事,native-image 指令應該如何處理呢?
必須透過 -cp 指令傳入自己專案和依賴專案的 classpath
傳入 main 入口讓 native-image 知道
提供給 native-image hint,傳入反射 config、動態代理 config、JNI config、resource…

native-image \
-cp BOOT-INF/classes:`find BOOT-INF/lib | tr '\n' ':'` \
com.cherry.graal.GraalApplication [-H:+ReportExceptionStackTraces] \
-H:DynamicProxyConfigurationFiles=dynamic-proxy.json
-H:ReflectionConfigurationFiles=/path/to/reflectconfig
...

不只是指定幾行 options 這麼簡單,我們可以看看要人工去做這件事有多可怕
動態代理的檔案長這樣
Configure Dynamic Proxies Manually (graalvm.org)
反射的設定檔長這樣
Reflection in Native Image (graalvm.org)

自己寫的程式還算容易
要如何判斷 library 當中哪些是需要動態代理配置、哪些是需要反射配置呢?
如果不依靠 Spring 整合的工具,要做到這些事情不僅是麻煩且是困難的

那麼既然使用 Spring 就可以一行搞定,是否可以大大推廣使用呢?
在簡單的專案當中這樣當然是簡單可行的
但在大型專案上,即使是使用這樣的方式仍然造成不少的麻煩
在 docker 和 k8s 的不同環境中,在不違背 12 factor app 的前提下
我們常常會使用相同 codebase,透過修改並動態傳入配置的部分差異處
來達成對不同環境的差異部署

因此需要額外映射 K8S ConfigMap 進入 app 路徑中,即使簡單的 @Value 能被讀取使用
本人未做詳盡測試,整體的支援度還未明(例如:能否與 auto-configuration 與 jasypt 共同使用等等)

再來是在構建上,其實還是有些額外小麻煩要處理
在測試期間,我是使用 docker 來做構建
除了不需要安裝 GraalVM 及 gu native-image 在自己電腦上,還有個重要原因

那麼在客戶環境中使用 docker image,以 Spring maven plugin build 方式來說
目前已知會使用到的至少有

GraalVM + Native 構建的方式其實還不是非常主流,image 的選擇上不是非常多
除了適應性問題,不確定非官方的 image 會不會哪天被棄養之外
還必須要考慮若干的資安規定
除非要自己利用官方提供的為基底下去包
使用 GraalVM native image 的好處是顯而易見的
但構建的成本是相對高出許多

結論 #

在採納時我們應該考慮本身的情況是否有必要使用?(例如:頻繁的擴縮,需要毫秒級啟動)
繁雜和犧牲的部分我們可以容忍嗎?
才能做出合理的決定

最後 #

補上自己嘗試的 source code,包含兩個 branches 提供參考:
https://github.com/qoosdd89382/native-graal/branches

🙏🙏🙏

感謝你的閱讀 💖!
歡迎將本篇文章 分享 📋 出去,也歡迎到 我的 LinkedIn 聊聊。

Published