在 GraalVM 静态编译下无侵入实现可观测探索

作者:铖朴、层风

GraalVM 静态编译

背景介绍

随着云原生浪潮的蓬勃发展,利用云原生技术为企业应用提供极致的弹性能力是企业数字化升级的核心诉求。但 Java 作为一种解释执行+运行时实时编译的语言,相比于其他静态编译型语言天生具有如下不足,严重影响了其快速启动与扩缩容效果。

冷启动问题

Java 程序启动运行详细过程如图 1 所示:

图片

图 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’ 保存到磁盘。

图片

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

通过 native-image-agent 实施增强记录是本方案的核心。整个过程必须包括被变换的类名、原始类文件的 byte 数组和变换后的类文件 byte 数组。以便可以判断出类是否发生了变化,以免记录下大量的噪音信息。

通过梳理 JVM 中 Agent 的工作流程,我们选择了 Java 函数 sun/instrument/InstrumentationImpl.transform 作为观察切入点,即图 3 中的红圈处。以下将这个函数简称为 transform 函数。

图片

图 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所示:

图片

图片

图 5 在传统 JVM 条件下的可观测数据采集与展示效果

我们测试过程中对应用发了 5 次调用,从图 5 的效果调用链记录的次数和调用链详情信息与实例应用都是一致的。

GraalVM 模式

在 GraalVM 静态编译环境下,基于上述方案,然后利用 OT 探针无侵入对 Spring Boot 应用进行可观测数据采集,将数据上报到阿里云应用实时监控服务 ARMS 中的可观测链路 OpenTelemetry 版中的效果如图 6 所示:

图片

图片

图 6 在 GraalVM 静态编译条件下的可观测数据采集与展示效果

测试过程中同样对应用发了 5 次调用,通过上述效果对比截图可以发现,Spring Boot 应用基于 GraalVM 静态编译后,采用静态插桩技术,所采集的请求数等指标与 JVM 环境动态增强方式一致,得益于静态编译技术的优化,请求 Span 耗时(不涉及网络情况下)比 JVM 环境增强方式低很多。除了上述 Spring Boot 应用的测试结果,其他的一些常用组件,例如 Kafka、MySQL 和 Redis 都做了上述同样测试,发现方案都是有效的!

另外,下表为我们测试的部分框架应用基于正常 JVM 环境下挂载探针 vs 基于静态编译场景挂载探针耗时和运行时内存占用情况数据(测试环境:32 核(vCPU)/64 GiB/5 Mbps):

图片

基于静态编译后,各类型应用的启动耗时大致降低了 98% 左右,运行时内存占用比原先下降了约 70% 左右,从测试结果看,上述 4 个框架组件基于当前方案,既能享受到静态编译带来的性能大幅度提升,也可消除静态编译带来的 Java Agent 无侵入增强失效问题。

其他

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

相关链接:

[1] Java 程序的启动过程分析

https://shipilev.net/talks/j1-Oct2011-21682-benchmarking.pdf

[2] GraalVM 开源社区

https://www.graalvm.org/

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

https://www.infoq.cn/article/uzHpEbpMwiYd85jYslka

[4] native-image-agent

https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/

[5] opentelemetry-java-instrumentation

https://github.com/open-telemetry/opentelemetry-java-instrumentation

[6] 可观测链路 OpenTelemetry 版

https://help.aliyun.com/zh/arms/tracing-analysis/

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

https://github.com/oracle/graal/pull/8077

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/481957.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

医疗器械-安规之漏电流测量

导致漏电的原因 半导体 PN结在截止时流过的很微小的电流。在D-S设在正向偏置,G-S反向偏置,导电沟道打开后,D到S才会有电流流过。但实际上由于自由电子的存在,自由电子的附着在SIO2和N、导致D-S有漏电流。 电源 开关电源中为了…

2 Spring之IOC详解

文章目录 4,IOC相关内容4.1 bean基础配置4.1.1 bean基础配置(id与class)4.1.2 bean的name属性步骤1:配置别名步骤2:根据名称容器中获取bean对象步骤3:运行程序 4.1.3 bean作用范围scope配置4.1.3.1 验证IOC容器中对象是否为单例验证思路具体实现 4.1.3.2…

AI元年,这5款AI写作能为你提供帮助

自从人工智能技术的迅猛发展以来,AI在各个领域都取得了巨大的进步。其中,AI写作工具成为越来越多人关注的焦点。在这个AI元年,小编想向大家分享5款可能对你有帮助的AI写作工具,如果你也想找AI写作相关的工具,那么来看看…

面试算法-82-不同路径

题目 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 问总共有多少条不同的路径? …

分布式ID生成方案总结

分布式场景下,由于通常是分库分表,所以通常无法仅仅使用数据库的自增Id。需要使用其他方案生成唯一的id。目前业界主流的是基于雪花算法或者雪花算法的改进版本。 UUID 有什么特点? 足够的简单,java原生自带。本地生成具有唯一性…

clang-query 的编译安装与使用示例

1,clang query 概述 作用: 检查一个程序源码的抽象语法树,测试 AST 匹配器; 帮助检查哪些 AST 节点与指定的 AST 匹配器相匹配; 2,clang-query 安装 准备: git clone --recursive https://git…

创建和运行任务

任务函数void ATaskFunction(void *pvParameters); 自定义延时函数

【设计】 【数学】1622 奇妙序列

本文涉及知识点 设计 数学 LeetCode1622. 奇妙序列 请你实现三个 API append,addAll 和 multAll 来实现奇妙序列。 请实现 Fancy 类 : Fancy() 初始化一个空序列对象。 void append(val) 将整数 val 添加在序列末尾。 void addAll(inc) 将所有序列中的…

C/C++之内存旋律:星辰大海的指挥家

个人主页:日刷百题 系列专栏:〖C/C小游戏〗〖Linux〗〖数据结构〗 〖C语言〗 🌎欢迎各位→点赞👍收藏⭐️留言📝 ​ ​ 一、C/C内存分布 我们先来了解一下C/C内存分配的几个区域,以下面的代码为例来看…

NIO简介以及用NIO实现一个群聊系统

一、BIO的工作原理 传统Io(BIO)的本质就是面向字节流来进行数据传输的 ①:当两个进程之间进行相互通信,我们需要建立一个用于传输数据的管道(输入流、输出流),原来我们传输数据面对的直接就是管道里面一个个字节数据的流动(我们弄了一个 by…

Opencascade基础教程(13):读取step文件(方式2)

1、 读取step文件 void COCCDemoDoc::OnButtonImportStep2() {// 设置过滤器 CString szFilter _T("step(*.stp;*.step)|*.stp;*.step||");// 构造打开文件对话框 CFileDialog fileDlg(true, _T("step"), 0, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,…

一文详解Rust中的字符串

有人可能会说,字符串这么简单还用介绍?但是很多人学习rust受到的第一个暴击就来自这浓眉大眼、看似毫无难度的字符串。 请看下面的例子。 fn main() {let my_name "World!";greet(my_name); }fn greet(name: String) {println!("Hello…

Mysql---备份恢复

文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 一.Mysql日志类型 错误日志: 错误日志主要记录如下几种日志: 服务器启动和关闭过程中的信息 服务器运行过程中的错误信息 事件调度器运行一个时间是产生的信息 在从服…

第十一届蓝桥杯大赛第二场省赛试题 CC++ 研究生组-寻找2020

数据很恶心&#xff0c;但是考点挺友好~ 把测试数据黏贴到记事本中&#xff0c;知测试数据的行列数 然后根据规则判断2020是否出现&#xff0c;并累计其次数即可。 判断可能需要注意超出下标&#xff0c;可以索性把数组定大些。 #include<stdio.h> const int N 310; ch…

校招应该如何准备

校园招聘是大学生进入职场的重要途径之一&#xff0c;请从以下方面去准备校招&#xff0c;通过认真的准备和努力&#xff0c;相信你一定能够在校园招聘中找到理想的工作机会。 1. 简历 简历是应聘过程当中最重要的材料&#xff0c;是我们在求职市场的一张名片&#xff0c;一份…

C语言——字符函数

前言 字符函数是C语言中专门用来处理字符的函数&#xff0c;再C语言中&#xff0c;我们有时需要大量的处理有关字符的问题&#xff0c;所以字符函数就由此应运而生&#xff0c;接下来我来为大家简单介绍一下字符函数。 一.字符分类函数 函数如果它的参数满足下列条件就返回真…

钡铼R40工业4G路由器保障智能物流仓储系统高效运行

随着物流行业的不断发展和智能化技术的广泛应用&#xff0c;智能物流仓储系统已成为提升物流效率、降低成本、提高服务质量的重要手段。在这样的背景下&#xff0c;钡铼R40工业4G路由器作为一种先进的网络通信设备&#xff0c;在智能物流仓储系统中扮演着关键的角色&#xff0c…

Docker学习笔记 - 常用命令

目录 基本概念常用命令使用docker compose启动脚本创建自己的image Docker命令文档 1. 下载一个image 从hub.docker.com下载一个image。 docker pull [image name]下载时指定image的tag。 docker pull [image name]:<tag>举例&#xff0c;下载postgre的tag为alpine…

重学SpringBoot3-MyBatis的三种分页方式

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-MyBatis的三种分页方式 准备工作环境搭建数据准备未分页效果 1. 使用MyBatis自带的RowBounds进行分页演示 2. 使用物理分页插件演示 3. 手动编写分页SQL…

算法体系-12 第 十二 二叉树的基本算法 下

一 实现二叉树的按层遍历 1.1 描述 1&#xff09;其实就是宽度优先遍历&#xff0c;用队列 2&#xff09;可以通过设置flag变量的方式&#xff0c;来发现某一层的结束&#xff08;看题目&#xff09;看下边的第四题解答 1.2 代码 public class Code01_LevelTraversalBT {publ…