Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题

news2024/11/25 12:52:36

作者:卜比

本文是《容器中的 Java》系列文章之 2/n,欢迎关注后续连载 😃 。

从 Java Agent 报错开始,到 JVM 原理,到 glibc 线程安全,再到 pthread tls,逐步探究 Java Agent 诡异报错。

背景

由于阿里云多个产品都提供了Java Agent 给用户使用,在多个 Java Agent 一起使用的场景下,造成了总体 Java Agent 耗时增加,各个 Agent 各自存储,导致内存占用、资源消耗增加。

所以我们发起了 one-java-agent 项目,能够协同各个 Java Agent;同时也支持更加高效、方便的字节码注入。

其中,各个 Java Agent 作为 one-java-agent 的 plugin,在 premain 阶段是通过多线程启动的方式来加载,从而将启动速度由 O(n) 降低到 O(1),降低了整体 Java Agent 整体的加载时间。

问题

但最近在新版 Agent 验证过程中,one-java-agent 的 premain 阶段,发现有如下报错:

2022-06-16 09:51:09 [oneagent plugin a-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: a-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/path/to/one-java-agent/plugins/a-java-agent/a-java-agent-1.7.0-SNAPSHOT.jar
  at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
  at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
  at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
  at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
  at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.InternalError: null
  at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
  at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
  at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
  ... 4 common frames omitted
2022-06-16 09:51:09 [oneagent plugin b-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: b-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/path/to/one-java-agent/plugins/b-java-agent/b-java-agent.jar
  at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)
  at com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)
  at com.alibaba.oneagent.plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)
  at com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)
  at java.lang.Thread.run(Thread.java:855)
Caused by: java.lang.IllegalArgumentException: null
  at sun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(Native Method)
  at sun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)
  at com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)
  ... 4 common frames omitted

熟悉 Java Agent 的同学可能能注意到,这是调用 Instrumentation.appendToSystemClassLoaderSearch 报错了。

但首先 appendToSystemClassLoaderSearch 的路径是存在的;其次,这个报错的真实原因是在 C++ 部分,比较难排查。

但不管怎样,还是要深究下为什么出现这个错误。

首先我们梳理下具体的调用流程,下面的分析都是基于此来分析的(当然,这些图也是解决了问题后,逆向推导出来的):

- Instrumentation.appendToSystemClassLoaderSearch (java)
  - appendToClassLoaderSearch0 (JNI)
     `- appendToClassLoaderSearch
         |- AddToSystemClassLoaderSearch
         |  `-create_class_path_zip_entry
         |      `-stat
         `-convertUft8ToPlatformString
            `- iconv

打日志、确定现场

因为这个问题在容器环境下,有 10% 的概率出现,比较容易复现,于是就用 dragonwell8 的最新代码,加日志,确认下现场。

首先在 JNI 的实际入口处,也就是 appendToClassLoaderSearch 的方法入口添加日志:

在这里插入图片描述

加了上面的日志后,发现问题更加令人头秃了:

  • 没有报错的时候,appendToClassLoaderSearch entry 会输出。
  • 有报错的时候,appendToClassLoaderSearch entry 反而没有输出。难道是没执行到这儿?

这个和报错的日志对不上啊,难道是 stacktrace 信息骗了我们?

过了难熬的一晚上后,第二天请教了 dragonwell 的同学,大佬打日志的姿势是这样的:

  • tty->print_cr(“internal error”);
  • 如果上面用不了,再用 printf(“xxx\n”);fflush(stdout);

这样加日志后,果然我们的日志都能打出来了。

这是踩的第一个坑,printf 要加上 fflush 才能保证输出成功

分析代码

后面又是不断加日志,最终发现原因是 create_class_path_zip_entry 返回 NULL。

找不到对应的 jar 文件?

继续排查,发现是 stat 报错,返回 No such file or directory。但是前面也提到了,jarFile 的路径是存在的,难道 stat 不是线程安全的?

查了下 stat 相关文档 [ 1] ,发现 stat 系统调用是线程安全的。

于是又回过头来再看,这时候注意到 stat 的路径是不正常的:

有的时候路径是空,有的时候路径是 /path/to/b-java-agent/b-java-agent.jarSHOT.jar,从字符末尾可以看出,基本上是因为两个字符写到了同一片内存导致的;而且对应字符串长度也变成了一个不规律的数字了。

那么问题就很明确了,开始查找这个字符串的生成。这个字符是 convertUft8ToPlatformString 生成的。

字符编码转换有问题?

于是开始调试 utf8ToPlatform 的逻辑,这时候为了避免频繁加日志、编译、重启容器,所以直接在 ECS 上运行 gdb 来调试 jvm。

结果发现,在 Linux 下,utf8ToPlatform 就是直接 memcpy,而且 memcpy 的目标地址是在栈上分配的:

在这里插入图片描述

这怎么看都不太可能有线程安全问题啊?

后来仔细查了下,发现和环境变量有关,ECS 上编码相关的环境变量是 LANG=en_US.UTF-8;而在在容器上,centos:7 默认没有这个环境变量,此种情况下,jvm 读到的是 ANSI_X3.4-1968。

这一块可以参考下 nl_langinfo(CODESET)的文档:https://man7.org/linux/man-pages/man3/nl_langinfo.3.html

这儿是第二个坑,环境变量会影响本地编码转换

结合如上现象和代码,发现在容器环境下,还是要经过 iconv,从UTF-8 转到 ANSI_X3.4-1968 编码的。

其实,这儿也可以推测出来,如果手动在容器中设置了 LANG=en_US.UTF-8,这个问题就不会再出现。额外的验证也证实了这点。

言归正传,然后又加日志,最终确认是 iconv 的时候,目标字符串写乱掉了。

难道是 iconv 线程不安全?

iconv 不是线程安全的!

查一下 iconv 的文档,发现它不是完全线程安全的:

在这里插入图片描述

解释一下就是,iconv 之前,需要先用 iconv_open 打开一个 iconv_t,而且这个 iconv_t,不支持多线程同时使用。

至此,问题已经差不多定位清楚了:因为 jvm 把 iconv_t 写成了全局变量,这样在多个线程同时调用 appendToClassLoaderSearch0 的时候,就有可能同时调用 iconv,导致竞态问题,把字符串写乱掉。

这儿是第三个坑,iconv 不是线程安全的

如何修复

先修复 one-java-agent

对于 Java 代码,非常容易修改,只需要加一个锁就可以了:

在这里插入图片描述

但是这儿有一个设计问题,instrument 对象已经在代码中到处散落了,现在突然要加一个锁,几乎所有用到的地方都要改,代码改造成本比较大。

于是最终还是通过 proxy 类来解决:

在这里插入图片描述

这样其他地方就只需要使用 InstrumentationWrapper 就可以了,也不会触发这个问题。

Java 9+ 的处理

但是注意到,在 JDK9 中,Instrumentation 这个接口增加了 redefineModule/isModifiableModule 方法,在新版本 JVM 下,上图中的 InstrumentationWrapper 就会因为没有这两个方法而报错。

本质上,是 JDK 对Instrumentation 这个 interface 做了不兼容的修改,这个改动就很难通过手动 proxy 的方式来兼容。

于是只能使用 JDK Proxy 来实现了,主要代码如图:

在这里插入图片描述

这样 one-java-agent 总算完整的修完了这个 bug 了。

jvm 要不要修复

然后我们分析下 jvm 侧的代码,发现其实就是因为 iconv_t 不是线程安全的,导致 appendToClassLoaderSearch0 方法不是线程安全的,那能不能优雅的解决掉这个问题呢?

如果是 Java 程序,我们可以直接用 ThreadLoal 来存储 iconv_t,就能解决这个问题。

但是 cpp 这边,虽然 C++ 11 支持 thread_local,但首先 jdk8 还没用 C++ 11(这个可以参考 JEP 347 [2 ] );其次,C++ 11 的也仅仅支持 thread_local 的 set 和 get,thread_local 的初始化、销毁等生命周期管理还不支持,比如没办法在线程结束时自动回收 iconv_t 资源。

那咱们就 fallback 到 pthread?因为 pthread 提供了 thread-specific data,可以做类似的事情。

  1. pthread_key_create 创建 thread-local storage 区域
  2. pthread_setspecific 用于将值放入 thread-local storage
  3. pthread_getspecific 用于从 thread-local storage 取出值
  4. 最重要的,pthread_once 满足了 pthread_key_t 只能初始化一次的需求。
  5. 另外也需要提到的,pthread_once 的第二个参数就是线程结束时的回调,我们就可以用它来关闭 iconv_t,避免资源泄漏。

总结一下就是,pthread 提供了 thread_local 的全生命周期管理。

于是,最终代码如下,用 pthread_once 初始化 thread-local storage,然后每个线程分配一个 iconv_t:

在这里插入图片描述

在这里插入图片描述

接下来,编译 JDK、打镜像、批量重启数次 pod 验证,就没有再出现文章开头提到的问题了。

总结

在整个过程中,我们从 Java 到 JNI/JVMTi,再到 glibc,再到 pthread,踩了很多坑:

  • printf 要加上 fflush 才能保证输出成功
  • 环境变量会影响本地字符编码转换
  • iconv 不是线程安全的
  • 使用 pthread thread-local storage 来实现线程局部变量的全生命周期管理

从这个案例中,沿着调用栈、代码,逐步还原问题、并修复问题,希望大家能通过这个案例,对 Java/JVM 多了解一点。

参考资料

one-java-agent 修复的链接:

https://github.com/alibaba/one-java-agent/issues/31

dragonwell8 修复的链接:

https://github.com/alibaba/dragonwell8/pull/346

[1] stat 相关文档:

https://pubs.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_09.html

[2] JEP 347:

https://openjdk.org/jeps/347

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据分析,你还在单纯地看数据?

企业的数字化意识越来越强,工作中也开始使用各种业务系统来管理业务,管理数据。很多人以为上了业务系统,对数据进行统计了,就是数据分析,这是大错特错的观点,数据分析是通过数据来剖析企业经营管理和业务发…

Using chatbots to scaffold EFL students argumentative writing (论文翻译)

使用聊天机器人来指导学生的议论文写作摘要研究表明,英语作为外语的学生的议论文写作受益于与同龄人的互动。然而,在实践中找到一个理想的对象很困难,聊天机器人被认为是这个问题的潜在解决方案。聊天机器人是人工智能的一种形式,…

Studio One6最新更新教程及安装包下载

Studio One6拥有多达50款原生效果插件,例如Analog Delay延迟插件,除能制作延迟效果外,还提供了制作复古的镶边与和声效果。Rotor插件制作的经典旋转扬声器效果也是非常不错的。这些插件,无论是在用户界面,还是使用体验…

http-serve开启一个服务器

前言在写前端页面中,经常会在浏览器运行HTML页面,从本地文件夹中直接打开的一般都是file协议,当代码中存在http或https的链接时,HTML页面就无法正常打开,为了解决这种情况,需要在在本地开启一个本地的服务器…

70. 爬楼梯

70. 爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 示例 1: 输入:n 2 输出:2 解释:有两种方法可以爬到楼顶。 1 阶 1 阶2 阶 示例 2&am…

如何将python脚本打包成可执行exe文件

如何将python脚本打包成可执行exe文件 前提条件 1. 新建一个python项目,并且配置虚拟环境 2. 安装pyinstaller 打包EXE文件 写一个支持入参的python脚本,打包成exe文件 找一张图片作为exe文件的图标 百度搜索” 在线jpg转cio”,将图片转换成cio格式 …

MySQL基础篇第11章(数据处理之增删改)

1.插入数据 1.1 实际问题 1.2 方式1:VALUES的方式添加 使用这种语法一次只能向表中插入一条数据。 情况1:为表的所有字段按默认顺序插入数据 INSERT INTO 表名 VALUES (value1,value2,....);值列表中需要为表的每一个字段指定值,并且值的…

MTIC工业大脑,海量接入网关!

MTIC3.0工业大脑是高性能,高可靠性的低代码开发数据核心平台,支持上万级别的网关设备同时在线,提供标准版本业务系统,可实现多业主工程项目并行管理,吸取大型物联网数据监测工程专家建议,实现项目精细化管理…

什么是DPU

什么是DPU 什么是 DPU? 在数据中心、DPU 或数据处理单元中移动数据的专家是一种新型的可编程处理器,将与 CPU 和 GPU 一起成为计算的三大支柱之一。 当然,您可能已经熟悉中央处理器。 多年来,CPU 是大多数计算机中唯一的可编程元…

如何避免成为背锅侠?

你被同事甩过锅吗? 打工人在职场中犯错都是不可避免的。 但明明不是自己的问题,还要背个黑锅,就非常闹心了! 大家好,我是大D。 前几天,大D开发完了一个项目,在交付验收中发现业务逻辑存在漏洞&a…

java swing人机对战五子棋(含背景音乐)

一、项目简介 本项目是一套基于java swing的人机对战五子棋系统,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含:项目源码、数据库脚本等,该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xf…

java BigDecimal保留两位小数

对于一些精准的数字,如涉及到金额时我们一般会使用BigDecimal类型来保存和处理。在处理保留小数位数时,如果通过DecimalFormat表达式需要注意下。 1、通过DecimalFormat保留两位小数 通过上图可以看到,#在补位时,如果该位没有数…

C/C++入门005-C语言数组

文章目录C语言数组数组的基本概念及定义数组定义数组中的几个名词数组长度计算方法二维数组的含义二维数组的定义字符数组字符函数1. strlen 计算字符串长度2. strcpy 字符串拷贝3. strcat 字符串追加4. strcmp 字符串比较1. strncpy 字符串拷贝3. strncmp 字符串比较4. strstr…

【web安全】——web渗透的前缀知识

作者名:Demo不是emo 主页面链接:主页传送门 创作初心:舞台再大,你不上台,永远是观众,没人会关心你努不努力,摔的痛不痛,他们只会看你最后站在什么位置,然后羡慕或鄙夷座…

海康摄像头CVE-2021-36260漏洞复现

海康摄像头CVE-2021-36260漏洞复现1.漏洞介绍2.手动测试2.1.摄像头版本2.2.漏洞复现2.2.1.更改URL2.2.2.抓包修改数据2.2.3.更换请求路径2.2.4.后期利用3.POC测试3.1.下载POC3.2.运行POC3.3.测试漏洞3.4.执行命令1.漏洞介绍 攻击者利用该漏洞可以用无限制的 root shell 来完全控…

Internet Download Manager6.41加速器最快的电脑工具下载器

IDM下载器,全称是Internet Download Manager,中文是互联网下载管理器可以说是最好用下载速度最快的下载器,可以利用全部带宽多线程下载,让你的下载速度起飞! 提到下载工具,大多数国人映入脑海的或许是迅雷…

优雅应对故障:QQ音乐怎么做高可用架构体系?

导语 | 故障是开发者高频关注的问题。在分布式系统建设的过程中,我们思考的重点不是避免故障,而是拥抱故障,通过构建高可用架构体系来获得优雅应对故障的能力。本文作者冯煦亮从架构、工具链、可观测三个维度,介绍了QQ音乐多年来积…

迅速配置hadoop Xshell 会话(安装java和jdk)

安装 yum install -y epel-release 防火墙 systemctl stop firewalld systemctl disable firewalld.service root权限 vim /etc/sudoers yy p 创建文件夹 mkdir /opt/module mkdir /opt/software chown hhh:hhh /opt/module/ chown hhh:hhh /opt/software/ cd /opt/ ll 卸…

shell第六天练习

正则表达式练习题: 1、显示/etc/rc.d/rc.sysinit文件中以不区分大小的h开头的行; 2、显示/etc/passwd中以sh结尾的行; 3、显示/etc/fstab中以#开头,且后面跟一个或多个空白字符,而后又跟了任意非空白字符的行; 4、查找…

2023年AI十大展望:GPT-4领衔大模型变革,谷歌拉响警报,训练数据告急

新年伊始,大模型的话题热度不减。2022年11月底,ChatGPT展现的惊人能力将大模型研究和应用热度推向高潮,人们激烈讨论着这个高级“物种”的推出意味着什么,比如是否会颠覆搜索引擎市场格局。踏入2023年,这不禁让我们对G…