java agent 踩坑之 appendtosystemclassloadersearch 问题_阿里云云原生-编程思维

作者: 鲁严波

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

背景

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

MSE 发起了 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-15 06:22:47 [oneagent plugin arms-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: arms-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/home/admin/.opt/ArmsAgent/plugins/ArmsAgent/arms-bootstrap-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 ahas-java-agent start] ERROR c.a.o.plugin.PluginManagerImpl -start plugin error, name: ahas-java-agent
com.alibaba.oneagent.plugin.PluginException: start error, agent jar::/home/admin/.opt/ArmsAgent/plugins/ahas-java-agent/ahas-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 不是线程安全的?

查了下文档 [ 1] ,发现 stat 是线程安全的。

于是又回过头来再看,这时候注意到 stat 的路径是不正常的:有的时候路径是空,有的时候路径是/home/admin/.opt/ArmsAgent/plugins/ahas-java-agent/ahas-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。

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

结合如上现象和代码,发现在容器环境下,还是要经过 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 写成了全局变量,这样在多个线程 append 的时候,就有可能同时调用 iconv,导致竞态问题。

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

如何修复

先修复 one-java-agent

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

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

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

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

jvm要不要修复

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

如果是 Java 程序,直接用 ThreadLoal 来存储 iconv_t 就能解决了。

但是 cpp 这边,虽然 C++ 11 支持 thread_local,但首先 jdk8 还没用 C++ 11(这个可以参考 JEP );其次,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 的全生命周期管理。于是,最终代码如下,用 make_key 初始化 thread-local storage:

于是编译 JDK 之后,打镜像、批量重启数次 pod,就没有再出现文章开头提到的问题了。

总结

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

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

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

参考链接:

[1] 文档:

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

[2] one-java-agent 修复的链接:

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

[3] dragonwell 修复的链接:

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

[4] one-java-agent 给大家带来了更加方便、无侵入的微服务治理方式:

https://www.aliyun.com/product/aliware/mse

MSE 注册配置中心专业版首购享 9 折优惠,MSE 云原生网关预付费全规格享  85 折优惠。点击“此处”,即刻享受优惠!

版权声明:本文版权归作者所有,遵循 CC 4.0 BY-SA 许可协议, 转载请注明原文链接
https://www.cnblogs.com/alisystemsoftware/p/16423860.html

消息队列 rabbitmq 遇上可观测--业务链路可视化_阿里云云原生-编程思维

作者:文婷、不周 本篇文章主要介绍阿里云消息队列 RabbitMQ 版的可观测功能。RabbitMQ 的可观测能力相对开源有了全面的加强,为业务链路保驾护航。 消息队列 RabbitMQ 简介 阿里云消息队列 RabbitMQ 版是一款基于高可用分布式存储架构实现的 AMQP 0-9-1 协议的消息产品,兼容开源

第八届“互联网+”大赛 | 云原生赛道邀你来挑战_阿里云云原生-编程思维

第八届中国国际“互联网+”大学生创新创业大赛正式开启,阿里云受邀命题,围绕“以云为核心的新型   推出 7 大技术方向共 32 道赛题,诚邀各大高校创新创业团队关注云计算核心技术,共创数字未来。 报名时间: 2022 年 6 月 1 日-7 月 31 日 技术支持社区: https://developer.aliyun

传统微服务框架如何无缝过渡到服务网格 asm_阿里云云原生-编程思维

作者:宇曾 背景 软件技术的发展历史,从单体的应用,逐渐演进到分布式应用, 特别是微服务理念的兴起,让大规模、高并发、低延迟的分布式应用成为可能。云原生时代下,微服务框架本身也在不断地进化和迭代演进。 微服务框架一般会涉及到以下几个知识点: 本文我们着重探讨以下三大微服务框架: SpringCloud Dubbo S

基于信通院 serverless 工具链模型的实践:serverless devs_阿里云云原生-编程思维

作者: 江昱 前言 2022 年 6 月 15 日,信通院在中国信通院云原生产业大会上发布《基于无服务器架构的工具链能力要求》标准,至此全球首个云原生 Serverless 开放工具链模型正式发布!Serverless Devs [ 1] 作为开源开放的开发者工具积极参与工具链模型建设,为树立行业领域统一规范发挥出助推

再测云原生数据库性能:polardb依旧最强,tdsql-c、gaussdb变化不大_jyzhou-编程思维

1.摘要 近期,腾讯云数据库在文章「腾讯云TDSQL-C重磅升级,性能全面领跑云原生数据库市场」中提到,某些场景下性能有非常大的提升,且超过国内某橙色云厂商。恰好,在5月份,我们对各个厂商的云原生数据库进行过一次压测,所以,看到文章,我们第一时间做了验证。具体的,验证内容包括: 当前的TDSQL-C性能与五月份相比,

kubevela 1.4:让应用交付更安全、上手更简单、过程更透明_阿里云云原生-编程思维

作者:孙健波,曾庆国 KubeVela 是一个现代化的软件交付控制平面,目标是让应用的部署和运维在如今的混合多云环境下更简单、敏捷、可靠。自 1.1 版本发布以来,KubeVela 架构上天然打通了企业面向混合多云环境的交付难题,且围绕 OAM 模型提供了充分的可扩展性,赢得了大量企业开发者的喜爱,这也使得 KubeV

消息队列 rabbitmq 遇上可观测--业务链路可视化_阿里云云原生-编程思维

作者:文婷、不周 本篇文章主要介绍阿里云消息队列 RabbitMQ 版的可观测功能。RabbitMQ 的可观测能力相对开源有了全面的加强,为业务链路保驾护航。 消息队列 RabbitMQ 简介 阿里云消息队列 RabbitMQ 版是一款基于高可用分布式存储架构实现的 AMQP 0-9-1 协议的消息产品,兼容开源

第八届“互联网+”大赛 | 云原生赛道邀你来挑战_阿里云云原生-编程思维

第八届中国国际“互联网+”大学生创新创业大赛正式开启,阿里云受邀命题,围绕“以云为核心的新型   推出 7 大技术方向共 32 道赛题,诚邀各大高校创新创业团队关注云计算核心技术,共创数字未来。 报名时间: 2022 年 6 月 1 日-7 月 31 日 技术支持社区: https://developer.aliyun

传统微服务框架如何无缝过渡到服务网格 asm_阿里云云原生-编程思维

作者:宇曾 背景 软件技术的发展历史,从单体的应用,逐渐演进到分布式应用, 特别是微服务理念的兴起,让大规模、高并发、低延迟的分布式应用成为可能。云原生时代下,微服务框架本身也在不断地进化和迭代演进。 微服务框架一般会涉及到以下几个知识点: 本文我们着重探讨以下三大微服务框架: SpringCloud Dubbo S

基于信通院 serverless 工具链模型的实践:serverless devs_阿里云云原生-编程思维

作者: 江昱 前言 2022 年 6 月 15 日,信通院在中国信通院云原生产业大会上发布《基于无服务器架构的工具链能力要求》标准,至此全球首个云原生 Serverless 开放工具链模型正式发布!Serverless Devs [ 1] 作为开源开放的开发者工具积极参与工具链模型建设,为树立行业领域统一规范发挥出助推

kubevela 1.4:让应用交付更安全、上手更简单、过程更透明_阿里云云原生-编程思维

作者:孙健波,曾庆国 KubeVela 是一个现代化的软件交付控制平面,目标是让应用的部署和运维在如今的混合多云环境下更简单、敏捷、可靠。自 1.1 版本发布以来,KubeVela 架构上天然打通了企业面向混合多云环境的交付难题,且围绕 OAM 模型提供了充分的可扩展性,赢得了大量企业开发者的喜爱,这也使得 KubeV

1 分钟 serverless 搭建你的首个个人网站(完成就送猫超卡)_阿里云云原生-编程思维

能否成功建网站,是小白走向成熟的一个关键动作。搭建网站一般要弄懂三个问题:域名、网站源码和服务器。 域名很好理解,它就像“手机号”一样独一无二,可以让别人找到你的网站。那如何能一步到位解决网站源码和服务器问题呢? 这篇文章让你不用考虑服务器和网站源码,使用 Serverless 1 分钟轻松搭建你的第一个个人网站。 S