作者:铖朴、层风

GraalVM静态编译

布景介绍

跟着云原生浪潮的蓬勃发展,使用云原生技能为企业使用供给极致的弹功才能是企业数字化升级的中心诉求。但 Java 作为一种解说履行 运转时实时编译的言语,比较于其他静态编译型言语天生具有如下不足,严重影响了其快速发动与扩缩容作用。

冷发动问题

Java 程序发动运转具体进程如图 1 所示:

在 GraalVM 静态编译下无侵入完成可观测探究

图 1:Java 程序的发动进程分析 [ 1]

Java 使用在发动时首要需求加载 JVM 虚拟机到内存中,如图 1 赤色部分描绘所示。然后JVM虚拟机再加载对应的使用程序到内存中,该进程对应上图中的浅蓝色类加载(Class Load,CL)部分。在类加载进程中,使用程序就会开端被解说履行,对应上图中浅绿色部分。解说履行进程 JVM 对垃圾对象进行收回,对应上图中的黄色部分。

跟着程序运转的深化,JVM 会选用及时编译(Just In Time,JIT)技能对履行频率较高的代码进行编译优化,以便提高使用程序运转速度。JIT 进程对应上图中的白色部分。经过 JIT 编译优化后的代码对应图中深绿色部分。经过上述分析,不难看出,一个 Java 程序从发动到到达被 JIT 动态编译优化会经过 VM init,App init 和 App active 几个阶段,比较于其他一些编译型言语,其冷发动问题比较严重。

运转时内存占用高问题

除了冷发动问题,从图 1 中能够看到,一个 Java 程序运转进程中,什么都不做首要便需求加载一个 JVM 虚拟机,该进程一般会占用必定量内存,别的,JIT 编译和 GC 都会有必定量的内存开支。

终究,由于 Java 程序是先解说履行字节码,然后再做 JIT 编译优化,因而由于其编译期比较晚,一些非必要的代码逻辑或许也会被预先加载到内存中进行编译。所以除了实际要履行的使用程序外,这些非必要代码逻辑也是一笔难以忽视的额定开支。综上所述,这些便是许多人常诟病 Java 程序运转内存占用高的原因。

静态编译技能

严重的冷发动耗时和较高的运转时内存占用使得Java使用难以满意云原生快速发动和快速扩缩容的需求。因而业界,以 Oracle 公司为主导的 GraalVM 开源社区 [ 2] ,经过推出 Java 静态编译技能,能够提前将 Java 程序编译为本地可履行文件,到达运转即巅峰的作用,可有用处理了Java使用冷发动和运转时内存占用高问题,让 Java 继续在云原生技能浪潮中勃发活力。

阿里巴巴作为 GraalVM 社区我国唯一的全球顾问委员会成员,持续在 GraalVM 上深化打磨,使之更加适合电商和云上场景。假如之前对静态编译技能不了解,能够阅读从本地原生到云原生,Alibaba Dragonwell 静态编译的实践与挑战 [3 ]根据静态编译构建微服务使用,做更具体的了解。

静态编译技能虽好,但对现有的 Java 技能体系也会有必定的影响。例如,探究过静态编译的朋友或许会清楚,经过静态编译后 Java 言语由于没有了字节码,会让本来一些根据 Java 字节码完成的 Java Agent 无侵入字节码改写技能失效。比方,现在 Java 生态中存在许多根据字节码改写无侵入地为 Java 使用供给如分布式链路追寻才能的处理计划,在现有的 Java 静态编译计划下,它们都将失效,这些也是许多企业在实施静态编译技能之前不得不考虑的技能难题。

经过静态插桩另辟蹊径

那是不是在静态编译场景下,就无法像传统的 Java 使用那样根据 Java Agent 探针完成开箱即用的可观测作用呢?

近期,阿里云可观测团队联合阿里云程序言语与编译器团队一同,为 GraalVM 完成了静态的 Agent 插桩增强才能,并在阿里云 ARMS 可观测平台上验证了静态增强数据的正确性和完整性,可有用处理现在 Java 静态编译时 Java Agent 字节码增强的问题。完成 Java 使用既要有根据 GraalVM 静态编译带来的功能提高,又能跟非静态编译场景下相同,能够经过类似于 Java Agent 这类技能无侵入的对使用完成分布式链路追寻等可观测作用。

什么是静态插桩?

要搞清楚什么是静态插桩?不得不提其相对的一个概念:动态插桩。

了解 Java Agent 探针技能的读者,应该了解 Java Agent 的作用进程,其本质是一种字节码改写技能,在使用运转进程中的类加载阶段经过字节码改写技能,在使用的特定类办法(也叫埋点)前后刺进一些增强逻辑,以到达对使用研发人员而言无感知地给使用添加一些比方,分布式链路追寻等可观测才能。

比较于动态插桩使用运转进程中经过字节码改写动态刺进一些逻辑,静态插桩便是在程序发动前就履行字节码改写,然后在运转前的 GraalVM 静态编译阶段,将之前收集的字节码改写终究内容编译到终究的可履行文件中,以完成动态插装相同的无侵入给使用在特定埋点进行才能增强的作用。

针对 Java Agent 的静态插桩计划

经过上述对静态插桩概念的介绍可知,要对使用代码进行插装,无非要处理以下两个问题:

  1. 在使用的哪些方位进行插桩?
  2. 要在特定的方位插桩哪些内容?

因而,咱们规划了一种 “预履行记载 编译时替换” 的办法来处理该问题,其进程全体分为两步:

  1. 经过使用程序的预履行记载所有被增强的类信息;
  2. 在 GraalVM 静态编译阶段,使用之前预履行收集的被增强类完成编译阶段的替换。

这样理论上就处理了在使用的哪些方位,增强什么内容的问题。

计划正确性论证

首要,回忆一下 Java Agent 机制的具体作业进程,其是在使用的 main 函数发动前,将 Agent 中定义的类转换器(Transformer)和响应 eventHandlerClassFileHook 的钩子完成注册到 JVM 中。每逢使用程序中初次加载一个类时,都先履行 eventHandlerClassFileHook 钩子中注册的代码,然后再加载类。开发者能够在该钩子中完成对指定类的改换,这样运转时加载到的类便是经过 Agent 增强的类了。

因而,关于恣意类 C,JVM 的 Agent 机制能够确保在 C 初次被加载的时刻即被 Agent 替换为 C’。从实际运转的程序的视点,它在运转时从头到尾接触到的只要 C’,而不是 C。因而,假定咱们在编译时就完成了将 C 替换为 C’,那么关于使用程序来说,其所见到的类从头到尾也是 C’。由此可见,在此问题上编译时替换和运转时替换对程序运转的作用是完全等价的。

所以,这个运转时问题也就转换成为了两个编译时问题:

  1. 怎么能够在编译前就获得 C’?
  2. C 和 C’是两个同名类,怎么在编译时确保同名类替换?

预履行记载被增强的类

了解过 GraalVM 静态编译技能的读者,应该知道,GraalVM 供给了一个叫做 native-image-agent [ 4] 的探针,经过给使用进行挂载进行预履行,能够记载 Java 使用程序中的反射、动态类加载、动态代理、序列化等动态行为,输出记载了这些信息的配置文件。在编译阶段,配置文件也会作为编译的输入为编译器供给动态行为信息,以完成 Java 动态特性在静态编译环境依然可收效的意图。

因而,咱们经过对 native-image-agent 进行改写,在原有的基础上添加了对 Agent 完成类改换代码增强行为的调查记载逻辑,完成原理如图 2 所示。图中的黄色 Agent 在原始使用 App 上对赤色的代码 C 实施运转时动态增强,将 C 部分代码转换为 C’,然后得到了 App’。添加了记载代码增强才能的 native-image-agent 担任调查从 C 到 C’ 的进程,将 C 的具体类名保存到配置文件,将改换后的 C’ 保存到磁盘。

在 GraalVM 静态编译下无侵入完成可观测探究

图 2 native-image-agent 监测原 Agent 代码改换进程示意图

经过 native-image-agent 实施增强记载是本计划的中心。整个进程有必要包括被改换的类名、原始类文件的 byte 数组和改换后的类文件 byte 数组。以便能够判别出类是否发生了变化,以免记载下许多的噪音信息。

经过整理 JVM 中 Agent 的作业流程,咱们选择了 Java 函数 sun/instrument/InstrumentationImpl.transform 作为调查切入点,即图 3 中的红圈处。以下将这个函数简称为 transform 函数。

在 GraalVM 静态编译下无侵入完成可观测探究

图 3 JVM 支撑 agent 完成动态代码改换流程图

咱们在 native-image-agent 中添加一个针对 transform 函数的函数断点,然后比照改换前后的类数据是否共同。假如共同,阐明没有做改换,该类无需进行记载;假如不共同,阐明类现已被改变,则将其类的全限定名输出到配置文件,将类的内容保存到磁盘。

编译时替换

得到了增强类,接下来只需在编译时用它们替换原始类,就能够在终究经过静态编译的 native image 可履行文件中完成插桩增强的作用了。那么在编译时怎么替换呢?最简略且安全的方法便是在类加载时替换。GraalVM 的静态编译才能本身也是一个 Java 程序,需求将编译的目标类悉数加载到 classpath 上。

所以简略地说,咱们只需在生成 classpath 列表时,将增强类的途径放在最前边就能够了。关于使用了 module system 的状况,由于同一个类不能出现在两个 module 中,咱们就要将增强类准备为 jar 包,经过 –patch-module 的形式替换原始类。这个进程原理简略,可是自动化完成的进程比较复杂,需求在修正 GraalVM 静态编译结构,在此就不展开了。

经过上述办法的处理,GraalVM 静态编译后的本地可履行程序中就只要改换后的代码,其运转时行为就与期待的行为共同。经过以上预履行记载 编译时替换两个步骤就完成了对使用在 GraalVM 环境下的静态插桩。

静态插桩技能实践

根据上述计划,咱们现已对一些常用的微服务组件,比方 Spring Boot、Kafka、MySQL 和 Redis 进行了作用验证,咱们现在是直接根据业界知名的可观测 Java Agent 探针完成opentelemetry-java-instrumentation [ 5] 进行数据收集(后文简称 OT 探针),然后将收集的可观测数据上签到阿里云使用实时监控服务 ARMS 中的可观测链路 OpenTelemetry 版 [ 6] 中进行的作用验证,如下为相关测验作用。

测验作用

JVM 形式

在一般的 JVM 运转时环境下,使用 OT 探针无侵入对 Spring Boot 使用进行可观测数据收集,然后将数据上签到阿里云使用实时监控服务 ARMS 中的可观测链路 OpenTelemetry 版中的作用如图5所示:

在 GraalVM 静态编译下无侵入完成可观测探究

在 GraalVM 静态编译下无侵入完成可观测探究

图 5 在传统 JVM 条件下的可观测数据收集与展现作用

咱们测验进程中对使用发了 5 次调用,从图 5 的作用调用链记载的次数和调用链详情信息与实例使用都是共同的。

GraalVM 形式

在 GraalVM 静态编译环境下,根据上述计划,然后使用 OT 探针无侵入对 Spring Boot 使用进行可观测数据收集,将数据上签到阿里云使用实时监控服务 ARMS 中的可观测链路 OpenTelemetry 版中的作用如图 6 所示:

在 GraalVM 静态编译下无侵入完成可观测探究

在 GraalVM 静态编译下无侵入完成可观测探究

图 6 在 GraalVM 静态编译条件下的可观测数据收集与展现作用

测验进程中同样对使用发了 5 次调用,经过上述作用比照截图能够发现,Spring Boot 使用根据 GraalVM 静态编译后,选用静态插桩技能,所收集的恳求数等指标与 JVM 环境动态增强方法共同,得益于静态编译技能的优化,恳求 Span 耗时(不触及网络状况下)比 JVM 环境增强方法低许多。除了上述 Spring Boot 使用的测验成果,其他的一些常用组件,例如 Kafka、MySQL 和 Redis 都做了上述同样测验,发现计划都是有用的!

别的,下表为咱们测验的部分结构使用根据正常 JVM 环境下挂载探针 vs 根据静态编译场景挂载探针耗时和运转时内存占用状况数据(测验环境:32 核(vCPU)/64 GiB/5 Mbps):

在 GraalVM 静态编译下无侵入完成可观测探究

根据静态编译后,各类型使用的发动耗时大致降低了98%左右,运转时内存占用比原先下降了约70%左右,从测验成果看,上述 4 个结构组件根据当时计划,既能享受到静态编译带来的功能大幅度提高,也可消除静态编译带来的 Java Agent 无侵入增强失效问题。

其他

终究,如上述内容介绍所示,当时咱们现已完成了计划的验证,并向 GraalVM 社区提交了相关的修正 PR [ 7] 。假如要在出产场景使用,也还有一些其他工程性的问题需求处理和优化。比方,Java Agent 或许出于一些场景需求,要能完成对 JDK 中的类进行替换,而 GraalVM 本身也修正了部分 JDK 类,以使之习惯静态编译后的运转时。所以碰到两边都进行修正要考虑兼容性等。终究,欢迎对该计划感兴趣或者希望进行相关作用复现的读者,能够加钉钉群: 80805000690,获取相关资料和做进一步交流讨论。

相关链接:

[1]Java 程序的发动进程分析

shipilev.net/talks/j1-Oc…

[2]GraalVM 开源社区

www.graalvm.org/

[3]从本地原生到云原生,Alibaba Dragonwell 静态编译的实践与挑战

www.infoq.cn/article/uzH…

[4]native-image-agent

www.graalvm.org/latest/refe…

[5]opentelemetry-java-instrumentation

github.com/open-teleme…

[6]可观测链路 OpenTelemetry 版

help.aliyun.com/zh/arms/tra…

[7]支撑静态插桩相关 PR

github.com/oracle/graa…