将GraalVM应用接入ARMS

GraalVM技术通过为Java应用进行静态编译,帮助应用消除了冷启动和运行时内存占用高的问题。针对GraalVM应用,ARMS提供了静态插装方案,将应用运行时使用Java探针对字节码的改写逻辑调整到静态编译中,实现静态增强,提供开箱即用的可观测能力。

重要

GraalVM作为一项前沿技术,如果之前未在生产场景使用过,建议您在测试环境充分验证后再考虑生产使用。此外,您在使用该方案接入ARMS过程中有任何问题,欢迎通过钉钉答疑群(群号:80805000690)与我们联系。

使用限制

  • 应用本身已完成GraalVM静态编译适配,如果是Spring Boot应用,可以参考相关文档进行应用适配改造。

  • 该方案需要调整GraalVM JDK实现,因此使用时需确保所使用的GraalVM JDK均为ARMS提供。

  • 如果要使用GraalVM静态编译,其对环境有一些要求,具体要求请参见GraalVM官方文档

  • 目前针对GraalVM场景,ARMS仅支持核心的TracesMetrics功能,暂不支持Arthas、持续剖析和内存快照等功能,另外由于GraalVM场景下,应用内存形态有变化,因此JVM监控中的元空间详情、非堆内存和直接缓冲区无数据是正常的。

接入操作

步骤一:安装依赖

GraalVM场景下,首先需要在环境中安装以下依赖:

  1. 根据自身应用所在地域,下载对应的GraalVM版本的ARMS探针。

    当前仅支持以下地域,如有更多需求,请通过钉钉答疑群(群号:80805000690)与我们联系。

    地域

    公网地址

    VPC地址

    华东1(杭州)

    wget "http://arms-apm-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip
    wget "http://arms-apm-cn-hangzhou.oss-cn-hangzhou-internal.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip

    华东2(上海)

    wget "http://arms-apm-cn-shanghai.oss-cn-shanghai.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip
    wget "http://arms-apm-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip

    华北2(北京)

    wget "http://arms-apm-cn-beijing.oss-cn-beijing.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip
    wget "http://arms-apm-cn-beijing.oss-cn-beijing-internal.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip

    华北3(张家口)

    wget "http://arms-apm-cn-zhangjiakou.oss-cn-zhangjiakou.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip
    wget "http://arms-apm-cn-zhangjiakou.oss-cn-zhangjiakou-internal.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip

    华南1(深圳)

    wget "http://arms-apm-cn-shenzhen.oss-cn-shenzhen.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip
    wget "http://arms-apm-cn-shenzhen.oss-cn-shenzhen-internal.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip

    华南1金融云

    wget "http://arms-apm-cn-shenzhen-finance-1.oss-cn-shenzhen-finance-1-internal.aliyuncs.com/ArmsAgentNative.zip" -O ArmsAgentNative.zip

    解压文件后,进入ArmsAgentNative目录,执行以下命令在本地环境中安装探针。

    sh install.sh
  2. 下载支持可观测能力的GraalVM JDK版本:graalvm-java17-23.0.4-ali-1.2b.tar.gz

    解压后在对应目录中执行以下命令:

    graalvm-java17-23.0.4-ali-1.2b/bin/native-image --version

    返回结果如下表示安装成功:

    image

  3. 下载Maven(如果环境已有Maven,无需再次安装):apache-maven-3.8.4-bin.tar.gz

    解压后,将环境变量JAVA_HOMEMAVEN_HOME设置对应解压文件后的路径。

    例如:

    请将/xxx/换成实际路径。

    export MAVEN_HOME=/xxx/apache-maven-3.8.4
    export PATH=$PATH:$MAVEN_HOME/bin
    export JAVA_HOME=/xxx/graalvm-java17-23.0.4-ali-1.2b
    export PATH=$PATH:$JAVA_HOME/bin

步骤二:引入依赖

为应用添加如下依赖:

请将代码中的路径/xxx/dynamic-configs指向应用原始的动态配置文件地址。

<dependencies>
  <dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>arms-javaagent-native</artifactId>
   <version>4.1.11</version>
   <type>pom</type>
 </dependency>
</dependencies>
<profiles>
  <profile>
    <id>native</id>
    <build>
      <plugins>
        <plugin>
          <groupId>org.graalvm.buildtools</groupId>
          <artifactId>native-maven-plugin</artifactId>
          <extensions>true</extensions>
          <executions>
            <execution>
              <id>build-native</id>
              <goals>
                <goal>compile-no-fork</goal>
              </goals>
              <phase>package</phase>
            </execution>
          </executions>
          <configuration>
            <fallback>false</fallback>
            <buildArgs>
              <arg>-H:ConfigurationFileDirectories=native-configs,/xxx/dynamic-configs</arg>
            </buildArgs>
          </configuration>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>

步骤三:添加access-filter-file.json文件

在应用目录下,添加一个名为access-filter-file.json的文件,其中内容为:

{ "rules": [
  {"excludeClasses": "sun.launcher.LauncherHelper"}
]
}

添加access-filter-file.json文件用于确保GraalVM收集动态特性的探针不要收集sun.launcher.LauncherHelper中的反射。sun.launcher.LauncherHelperJVM启动的类,其中用到的反射对于静态编译后的Native Image是不需要的,否则会在编译时产生错误。

步骤四:预执行

为了让ARMS探针对应用的动态增强代码被静态编译到最终的Native Image文件中,因此需要预先挂载ARMS探针对应用进行预执行,预执行需要确保应用核心代码分支都被执行,当前已提供一个相关脚本供您完成该过程。注意将应用的所有RESTful接口都按提示声明在脚本中,以便执行过程中的相关接口被正常调用触发业务执行。

  1. 参考以下脚本,按照注释完成自定义内容补充。

    ######## 请根据实际修改一下参数
    # ARMS接入参数, 可调用DescribeTraceLicenseKey接口获取到对应的LicenseKey。AppName代表接入到ARMS的哪一个应用中,您可以根据需要自定义您的应用名,在分布式架构中,同一个应用内可以包含多个对等的实例。
    export ARMS_LICENSEKEY=
    export ARMS_APPNAME=
    # 应用接口列表,例如PS=(interface1 interface2 interface3 interface4)
    export PS=
    # 应用端口,例如PORT="8080"
    export PORT=
    # Native image文件路径,静态编译后会在应用target目录下生成一个native image文件,例如NATIVE_IMAGE_FILE="target/graalvm-demo"
    export NATIVE_IMAGE_FILE=
    # 运行ARMS Native Agent的命令,例如JAVA_CMD="-javaagent:./arms-native/aliyun-java-agent-native.jar -jar target/graalvm-demo-1.0.0.jar"
    export JAVA_CMD=
    ########
  2. 挂载ARMS Java探针,进入预执行,搜集静态编译的配置项。

    sh ArmsAgentNative/run.sh --collect --jvm --Carms

步骤五:静态编译

完成依赖添加后,按照如下步骤对应用进行静态编译:

  1. 开始静态编译。

    mvn -Pnative package
  2. 运行静态编译后的项目。

    sh ArmsAgentNative/run.sh --native --Carms

其他操作

构建Docker镜像

如果要针对完成GraalVM静态编译后的应用构建Docker镜像,由于其中已经包含了JDK等运行时必要的信息,直接将最终的Native Image文件作为一个可执行文件放到镜像中,按照正常构建Docker镜像流程进行镜像构建即可。

示例Dockerfile:

说明

-Darms.licenseKey-Darms.appName参数需调整为实际值。

FROM centos:latest
WORKDIR /app
COPY ./target/graalvm-demo /app

CMD ["/app/graalvm-demo","-Darms.licenseKey=xxx","-Darms.appName=xxx"]

压缩Native Image

在比较Native Image与原Java程序所占的空间大小时,我们会将JDK的大小也加在Java程序侧一起计算。因为Java程序必须要JDK支持,而Native Image是自举的,它已经包含了所有的依赖。

但是因为Native Image中为汇编代码,相比Java应用的字节码信息密度更低,表达相同的语义所需的代码量更大。因此随着应用规模的增大,Native Image的大小会逐渐超越Java程序 + JDK的大小,导致部署、传输的压力增大。此时我们可以借助压缩工具UPX来降低Native Image的大小。UPX可以将二进制可执行文件仍然压缩为二进制可执行文件,压缩后的文件无需解压即可直接运行,并且基本不会影响运行时的性能。

下图展示了压缩Native Image的效果:

image

将静态编译后的graalvm-demo文件压缩为graalvm-demo-compressed文件,压缩后的大小仅有之前的28.4%。

Fat Jar包的比较:

image

原先包含了Spring Boot、RocketMQ等所有依赖的Fat Jar包大小为216MB,而压缩后的Native Image仅有47MB。

UPX使用方法:

  1. 下载并解压UPX工具

  2. 假设UPX被解压到$UPX_HOME目录,执行以下命令完成压缩。

    $UPX_HOME/upx -9 -o path/to/output-file path/to/original-file

    • -9:压缩程度为1~9,值越大压缩率越高,压缩所需时间越长。

    • -o path/to/output-file:压缩输出文件路径,path/to/output-file需要换成实际的文件路径。

    • path/to/original-file:被压缩的原始文件路径,path/to/original-file需要换成实际的文件路径。