Java 运行包精简探索(GraalVM)

2023-02-27 00:00:00 文件 运行 配置 扫描 打包

前言


在热心群友的提示下,入了 GraalVM 的坑。本来以为花个三两天应该就能搞定,没想到这一搞就是一个周,终于取得了阶段性的胜利。


编注:此文为运行包精简主题的一篇,其他精简方法请参见文末。

背景如下:

> 近由于某些原因,需要做一个自带运行环境的程序。由于各种原因,选定了 Java 和 Python 作为备选语言。但是 Java 由于 JRE 的臃肿(100M+)以及 Spring Boot 的日渐臃肿(helloworld 15M),需要在这两方面进行 size 的缩减。


环境


  • 系统:Ubuntu 16.04

  • GraalVM:GraalVM Community 21.3.0 (Based on OpenJDK 11.0.13)


https://www.graalvm.org/downloads/


环境准备


参考:https://www.graalvm.org/docs/getting-started/


  • 关键配置

  • 环境变量设置


export JAVA_HOME=/path/to/<graalvm>export PATH=/path/to/<graalvm>/bin:$PATH


安装 native-image


gu install native-image


安装 native-image 需要的组件


apt-get install build-essential libz-dev zlib1g-dev


注意:这里有个其他文章都没有提到的坑。ld 需要更新到 2.26+,不然在构建过程中会报告莫名其妙的异常(这里我耗了大半天)。


apt-get install binutils-2.26

另一个坑


开始我是在一台 aliyun 的机器上实验的,没有注意到内存问题,在实验过程中遇到异常中断。排查 syslog 发现是 OOMKiller。排查发现我的可用内存只有 4G。


后改为在本机机器 VMware 里的 Ubuntu 操作,内存分配到 8G。经观察,native-image 打包过程中会用到 5.2G 左右的内存,所以这里要注意一下。


至此,环境准备完成。


打包


helloworld 的尝试就跳过了,网上一搜一堆。先了解一下打包命令。



https://www.graalvm.org/uploads/quick-references/native-image-quick-reference-v2_A4.pdf


项目依赖



主要用到了 solon、solon-api、h2、weed3、logback、slf4j、jlhttp 等包。


首先,通过 Maven 把我的项目 solondemo 打包为可以运行的 jar,
确保通过。


java -jar solondemo.jar


可以正常运行并访问。然后把 solondemo.jar 上传到前面准备好的 GraalVM 环境。


首先,需要使用 GraalVM 提供的配置工具,对想要打包的程序的一些静态分析无法分析到的信息进行采集。


如果你确认项目没有使用任何反射、代理等特性,可以省略这一步。执行


java -agentlib:native-image-agent=config-output-dir=./config/ -jar solondemo.jar


执行后,好能跑一跑 testcase,尽量保证代码覆盖率 ,避免打包后遇到 classnotfound。


执行完成后,终止执行。


如果需要多次运行采集信息 可以使用如下命令再次执行,工具会自动合并采集结果而不是覆盖。


java -agentlib:native-image-agent=config-merge-dir=./config/ -jar solondemo.jar


执行结束,config 下生成如下 5 个文件:


jni-config.jsonpredefined-classes-config.jsonproxy-config.jsonreflect-config.jsonresource-config.jsonserialization-config.json


坑又来了


根据网上的 说法,可以通过指定 -H:ConfigurationFileDirectories=./config 的方式来使用前面生成的配置文件。


但是,后老是会把 -H:ConfigurationFileDirectories= 认为是指定的生成文件名,然而根据文档,-H:Name=xxx 才是指定输出文件名的参数。


注意:这个问题是开始在 CentOS 上操作遇到的,后我在 Ubuntu 上又尝试用这种方式指定配置文件的时候,它生效了,原因未知。


我采用了另外一种配置方式。把这些文件打包到 jar 包的 META-INF/native-image 目录下。



打包命令:


native-image  -jar solondemo.jar --allow-incomplete-classpath -H:+ReportExceptionStackTraces --enable-http


这个命令也是经过反复多次尝试终得出的可用命令,尝试的过程就略去3W字了。


注意:如果你的应用需要对外提供 HTTP 服务,必须加上配置 --enable-http。如果提供 HTTPS 服务,则必须加 --enable-https。否则哪怕运行起来了访问也永远是 500。打包成功,目录下生成 solondem 文件。

执行运行:运行成功了,但是插件没加载。翻阅 solon 源码,发现插件加载的流程大致如下:



通过反复添加日志排查,发现:


在 graalvm native-image 下运行,这里扫描到 META-INF/solon 这个目录的 type,不是 file/jar,而是 resource。



所以到了这里自然就无法遍历目录下的文件了。于是我尝试让 resource 类型也走 file 的方式去扫描。




调研搜索后发现,GraalVM 内部资源管理自己实现了一套 FileSystem,URL 描述符定义为 resource,有一套自定义的 API(由于时间有限,暂未深入研究)。


对本来是目录类型的 resource 使用 File 方式去处理,得到的结果是 file not exists!但是对于确定的文件,是可以正常读取的。


于是我考虑预处理,在 GraalVM 外面就先把能扫描到的文件清单提取出来,通过配置的方式,插件扫描的时候直接返回预置的文件清单。


因为本地执行是可以正常扫描的,所以我在扫描结束的时候,增加一个输出:


然后在配置中添加:


scan 流程做如下修改


插件扫描成功并运行。



扫描注解也有同样的问题,排查过程与配置文件扫描类似,解决方案已与配置文件扫描的解决方案合并,略去 3万字。


至此,主框架已经可以 run 起来了,但是嵌入式数据库 h2 还在作妖。


调研发现 Github 已经有人提过这个问题:


https://github.com/h2database/h2database/issues/2207


但是我尝试按照他们说的 使用 1.4.199 版本,却仍然各种异常。没办法,下载 h2 源码 加 log 排查吧。



首先,这里报空指针,那么的可能就是 defaultProvider 为空。分析 defaultProvider 初始化过程:



发现了 Class.forName,以及吃掉了异常:


e.printStackTrace();


打包,再来运行:



Caused by: java.lang.ClassNotFoundException......org.h2.store.fs.disk.FilePathDisk


好嘛。native-image 的 agent 居然没有把这个扫出来。手动把这些添加到 reflect-config.json 里面,再打包运行。又报了个别的 class not found。再添加,再打包。


[........,{    "name": "org.h2.store.fs.FilePathDisk",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathMem",    "methods":[{"name":"<init>","parameter*":[] }]  }  ,  {    "name": "org.h2.store.fs.FilePathMemLZF",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathNioMem",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathNioMemLZF",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathNioMapped",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathAsync",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathZip",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathRetryOnInterrupt",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathNio",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.store.fs.FilePathSplit",    "methods":[{"name":"<init>","parameter*":[] }]  },  {    "name": "org.h2.mvstore.db.MVTableEngine",    "methods":[{"name":"<init>","parameter*":[] }],    "allDeclaredFields":true  }]


成了!



访问接口、增删改查、静态页面 、日志都 OK 了。


 




幸福来得如此突然。


总结


终可用的打包命令


native-image  -jar solondemo.jar --allow-incomplete-classpath -H:+ReportExceptionStackTraces --enable-http


ld 需要升级到 2.26+



root@ubuntu:/home/hx/graalvm/demo3# ld -version
GNU ld (GNU Binutils for Ubuntu) 2.26.1
Copyright (C) 2015 Free Software Foundation, Inc.This program is free software; you may redistribute it under the terms ofthe GNU General Public License version 3 or (at your option) a later version.This program has absolutely no warranty.


其他注意事项


  • -agentlib:native-image-agent 不一定能检查出所有的反射;

  • GraalVM 有自己的文件系统实现,暂未找到遍历目录的方法;

  • 第三方包不能运行时,大概率是由于反射没有检查到导致的 class not found;

  • 排查第三方包问题时,一定要注意被吃掉的 Exception;

  • native-image 打包时需要 5G+ 内存。


补充

1. native-image后序列化失败问题(比如 JSON.toJSONString(JavaBean))


fastjson1.2.68 版本下在程序启动时增加如下代码:


ParserConfig.getGlobalInstance().setAsmEnable(false);SerializeConfig.getGlobalInstance().setAsmEnable(false);


2. 反射方法报错


需将反射类手动配置到 reflect-config.json 文件中,也可在编译打包成 jar 时添加配置


-agentlib:native-image-agent=config-output-dir=../META-INF/native-image


后打包,然后 java -jar 或 java -cp 运行起来后,执行对应测试用例后,会自动将反射类信息生成到。


reflect-config.json 文件中(但真的不一定)。配置文件样例:

[    {        "name":"com.test.A",        "allDeclaredFields":true,        "allPublicFields":true,        "queryAllPublicMethods":true,        "methods":[            {"name":"getA","parameter*":[] },             {"name":"getD","parameter*":[] },             {"name":"getF","parameter*":[] },             {"name":"getI","parameter*":[] },             {"name":"getQ","parameter*":[] },             {"name":"getR","parameter*":[] },             {"name":"getT","parameter*":[] },             {"name":"getY","parameter*":[] },             {"name":"getU","parameter*":[] },             {"name":"getV","parameter*":[] }        ]    }]


3. GraalVM 有自己的文件系统实现


暂未找到遍历目录的方法(即上文说说的).


如果你的程序中有涉及 ClassLoad.getResource("com.org") 这样的代码并打算对齐返回的结果以 File 或 jar 文件的方式扫描 com.org 下的所有类文件时会报错。


解决方式如上文所说,手动配置需要扫描的类文件,然后读取该配置(替代 getResource 方式)。


4. 控制 native 化后的二进制程序内存大小(配置参数不多说,一看就明白)


样例:


./solondemo -Xmx16m -Xms16m -XX:MaxDirectMemorySize=8m


上文提到的 solon scan 方法 支持预配置代码已提交到:


https://gitee.com/noear/solon


文中提到的 solondemo 项目,已经提交到:


https://gitee.com/mantouchong/solondemo


其他探索


  • java运行包精简探索(一):https://www.jianshu.com/p/f1da836fb4d6

  • java运行包精简探索(二):https://www.jianshu.com/p/5e3b1913cf3b


转自:馒头虫,

链接:jianshu.com/p/f78a016b30fe



相关文章