当前位置: 首页 > 后端技术 > Java

JavaAgent踩坑的appendToSystemClassLoaderSearch问题

时间:2023-04-01 22:06:36 Java

作者:陆彦博从JavaAgent报错,到JVM原理,到glibc线程安全,再到pthreadtls,逐步探索JavaAgent的诡异报错。背景由于阿里云的多个产品都提供了JavaAgent供用户使用,在多个JavaAgent一起使用的场景下,JavaAgent的整体耗时增加,每个Agent单独存储,导致内存占用和资源消耗增加.MSE推出one-java-agent项目,可以配合各种JavaAgent;它还支持更高效、更方便的字节码注入。其中,每个JavaAgent作为one-java-agent的插件,在premain阶段通过多线程启动加载,从而将启动速度从O(n)降低到O(1),降低了整体成本Java代理作为一个整体。加载时间。问题但是最近在验证新版Agent的过程中,在one-java-agent的premain阶段发现如下错误:2022-06-1506:22:47[oneagentpluginarms-agentstart]ERRORc.a.o.plugin.PluginManagerImpl-启动插件错误,名称:arms-agentcom.alibaba.oneagent.plugin.PluginException:启动错误,agentjar::/home/admin/.opt/ArmsAgent/plugins/ArmsAgent/arms-bootstrap-1.7。0-SNAPSHOT.jar在com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)在com.alibaba.oneagent.plugin.PluginManagerImpl.startOnePlugin(PluginManagerImpl.java:294)在com.alibaba.oneagent。plugin.PluginManagerImpl.access$200(PluginManagerImpl.java:22)atcom.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)atjava.lang.Thread.run(Thread.java:750)由:java.lang.InternalError:nullatsun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(NativeMethod)atsun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)atcom.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)...省略了4个公共框架2022-06-1609:51:09[oneagent插件ahas-java-agent启动]错误c.a.o.plugin.PluginManagerImpl-start插件错误,名称:ahas-java-agentcom.alibaba.oneagent.plugin.PluginException:启动错误,agentjar::/home/admin/.opt/ArmsAgent/plugins/ahas-java-agent/ahas-java-agent.jar在com.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:113)$200(PluginManagerImpl.java:22)在com.alibaba.oneagent.plugin.PluginManagerImpl$2.run(PluginManagerImpl.java:325)在java.lang.Thread.run(Thread.java:855)引起:java.lang。IllegalArgumentException:nullatsun.instrument.InstrumentationImpl.appendToClassLoaderSearch0(NativeMethod)atsun.instrument.InstrumentationImpl.appendToSystemClassLoaderSearch(InstrumentationImpl.java:200)atcom.alibaba.oneagent.plugin.TraditionalPlugin.start(TraditionalPlugin.java:100)...省略了4个常用框架熟悉JavaAgent的同学可能会注意到这是在调用Instrumentation.appendToSystemClassLoaderSearch报错,但首先appendToSystemClassLoaderSearch的路径是存在的;其次,这个错误的真正原因是在C++部分,很难排查。但无论如何,还是有必要深究为什么会出现这个错误。首先梳理一下具体的调用过程,下面的分析就是基于这个分析:`-create_class_path_zip_entry|`-stat`-convertUft8ToPlatformString`-iconv用于记录和确认站点。因为这个问题在容器环境下有10%的概率出现,所以比较容易重现,所以我用的是dragonwell8最新的代码,添加日志,现场确认。首先在JNI的实际入口,即appendToClassLoaderSearch的方法入口添加一条日志:加上上面的日志后,问题就更秃了:没有报错的时候,会输出appendToClassLoaderSearch入口。报错时,appendToClassLoaderSearch入口没有输出,这里不执行?这与错误日志不匹配。难道是stacktrace信息欺骗了我们?难熬了一夜,第二天问了龙井的同学。logger的姿势是这样的:tty->print_cr("internalerror");如果上述方法不起作用,请使用printf("xxx\n");fflush(stdout);这样添加日志后,我们的日志就可以打出来了。这是第一个踩的坑。printf需要加上fflush才能保证输出成功。分析代码后,不断添加日志,最后发现create_class_path_zip_entry返回NULL。找不到对应的jar文件?继续查看,发现stat报错,返回Nosuchfileordirectory。但是前面说了,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,就不会再出现这个问题。额外的验证也证实了这一点。然后添加log,等到最后确认是iconv的时候,目标字符串就写好了,挂了。难道iconv是线程不安全的吗?iconv不是线程安全的!查看iconv的文档,发现它并不是完全线程安全的:一般来说,在iconv之前,需要用iconv_open开启一个iconv_t,而这个iconv_t是不支持多线程同时使用的。至此,问题已经差不多定位清楚了,因为jvm把iconv_t写成了全局变量,所以当多个线程append时,有可能同时调用iconv,导致racecondition问题。这里是第三个坑,iconv不是线程安全的。Howtofixfirstfixone-java-agent对于Java代码,修改起来非常容易,只需要加一把锁即可:但是设计有问题,instrument对象一直散落在代码中,现在突然需要加一把锁,几乎所有用到的地方都要改,代码改造成本比较高。所以最终通过代理类解决了:这样其他地方只需要使用InstrumentationWrapper,就不会触发这个问题。应该修复jvm吗?然后我们分析jvm端的代码,发现appendToClassLoaderSearch0方法不是线程安全的,因为iconv_t不是线程安全的。能否优雅地解决?如果是Java程序,直接使用ThreadLoal存储iconv_t即可解决。但是cpp这边,虽然C++11支持thread_local,但是首先jdk8还没有使用C++11(这个可以参考JEP);其次,C++11只支持thread_local的set和get,thread_local的初始化、销毁等生命周期管理还没有。不支持,比如没有办法在线程结束时自动回收iconv_t资源。然后让我们回退到pthread?因为pthread提供线程特定的数据,所以可以做类似的事情。pthread_key_create创建一个线程本地存储区pthread_setspecific用于将值放入线程本地存储pthread_getspecific用于从线程本地存储中取出值最重要的是,pthread_once满足了pthread_key_t只能初始化一次的要求。另外需要说明的是,pthread_once的第二个参数是线程结束时的回调。我们可以用它来关闭iconv_t,避免资源泄露。总之,pthread提供了thread_local的全生命周期管理。所以,最终代码如下,使用make_key初始化线程本地存储:编译JDK,镜像,批量重启pod几次后,文章开头提到的问题没有再出现。总结一下,整个过程中,从Java到JNI/JVMTi,再到glibc,再到pthread,踩了很多坑:printf需要加上fflush才能保证输出成功。环境变量会影响本地字符编码转换。iconv非线程安全使用pthread线程局部存储实现线程局部变量的全生命周期管理从这个案例开始,沿着调用栈,代码,逐步还原问题,并提出解决方案,希望大家学习有关Java/JVM的更多信息。参考链接:[1]文档:https://pubs.opengroup.org/on...[2]One-java-agent修复链接:https://github.com/alibaba/on...[3]Dragonwell修复链接:https://github.com/alibaba/dr...[4]one-java-agent为您带来更便捷、无侵入的微服务治理方式:https://www.one-java-agentaliyun.com/produc...MSE注册配置中心专业版首购10折,MSE云原生网关预付费全规格15折。点击“这里”立即享受折扣!