作者: 鲁严波

从 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 的办法入口添加日志:

Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题

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

  • 没有报错的时分,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 的文档,发现它不是完全线程安全的:

Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题

通俗的说,iconv 之前,需求先用 iconv_open 翻开一个 iconv_t,并且这个 iconv_t,不支撑多线程一起运用。

至此,问题已经差不多定位清楚了,由于 jvm 把 iconv_t 写成了全局变量,这样在多个线程 append 的时分,就有可能一起调用 iconv,导致竞态问题。

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

怎么修正

先修正 one-java-agent

对于 Java 代码,十分简单修正,只需求加一个锁就能够了:

Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题

可是这儿有一个规划问题,instrument 对象已经在代码中到处散落了,现在忽然要加一个锁,几乎一切用到的当地都要改,代码改造本钱比较大。

所以终究仍是经过 proxy 类来处理:

Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题

这样其他当地就只需求运用 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:

Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题

Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题

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

总结

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

  • printf 要加上 fflush 才干确保输出成功
  • 环境变量会影响本地字符编码转化
  • iconv 不是线程安全的
  • 运用 pthread thread-local storage 来完成线程局部变量的全生命周期办理

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

参阅链接:

[1] 文档:

pubs.opengroup.org/onlinepubs/…

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

github.com/alibaba/one…

[3] dragonwell 修正的链接:

github.com/alibaba/dra…

[4] one-java-agent 给我们带来了愈加便利、无侵入的微服务管理办法:

www.aliyun.com/product/ali…

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