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

为什么process1不能在容器中挂上arthas?

时间:2023-04-01 13:46:29 Java

作者:Bubi本文为《容器中的 Java》系列文章的第4篇,欢迎关注后续系列:)。系列一:JVM如何获取当前容器的资源限制?系列二:appendToSystemClassLoaderSearchforJavaAgent踩坑问题系列三:让JavaAgent更好的在Dragonwell上运行最近在容器环境下,发现java进程为1号进程时无法使用arthas,提示AttachNotSupportedException:Unable获取LinuxThreads管理器线程的pid。具体操作和报错如下:#java-jararthas-boot.jar[INFO]arthas-bootversion:3.5.6[INFO]发现存在java进程,请选择一个并输入进程序号,如:1.然后按ENTER。*[1]:1com.alibabacloud.mse.demo.ZuulApplication1[INFO]arthashome:/home/admin/.opt/ArmsAgent/arthas[INFO]Trytoattachprocess1[ERROR]Startarthasfailed,异常堆栈跟踪:com.sun.tools.attach.AttachNotSupportedException:无法在sun.tools.attach.LinuxVirtualMachine.(LinuxVirtualMachine.java:86)在sun.tools.attach.LinuxAttachProvider获取LinuxThreads管理器线程的pid.attachVirtualMachine(LinuxAttachProvider.java:78)在com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:250)在com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:117)在com.taobao.arthas.core.Arthas.(Arthas.java:27)atcom.taobao.arthas.core.Arthas.main(Arthas.java:166)[INFO]Attachprocess1success.之前也遇到过,总是调整了下镜像,让Java进程不要做No.1进程,但这不是长久之计,还是花时间看看这个问题。为了重现这个问题,我们创建了以下项目来重现这个问题:线。睡眠(30*1000);}}}FROMopenjdk:8u212-jdk-alpineCOPY.//appWORKDIR/app/src/main/java/RUNjavacMain.javaCMD["java","Main"]然后正常启动应用,并尝试使用arthas,或jstack:$#构建镜像$dockerbuild.-texample-attach$#启动容器$dockerrun--nameexample-attach--rmexample-attach$#在另一个终端进入容器,执行jstack$dockerexec-itexample-attachsh/app/src/main/java#jstack11:UnabletogetpidofLinuxThreadsmanagerthread问题成功复现!接下来开始分析。正常的附加过程是什么样的?以下是排查时整理的jvmAttach流程:找到unixsocket/tmp/.java_pid${pid},存在则检查权限,建立连接。如果不存在,先创建/proc/${pid}/cwd/.attach_pid${pid},开始通知jvm线程。先判断是不是LinuxThread。如果是LinuxThread,找到LinuxThreadsManager,然后向它的所有子进程发送SIGQUIT。如果不是LinuxThread,则直接向目标进程发送SIGQUIT。目标进程收到信号后,创建一个AttachListener,监听/tmp/.java_pid${pid}。开始正常的socket通信,根据通信的具体内容,可以是dumpThread(jstack),也可以是加载JavaAgent,比如上面提到的arthas。JavaAttach机制的Native文章[1]也很好的分析了attachAPI。为什么附加到进程1会报错?首先,/tmp/.java_pid${pid}那时候肯定是不存在的。如果它存在,它将直接通信并加载Arthas。这也可以通过查看文档来确认。其次,.attach_pid${pid}文件也可以创建成功,我们也可以通过strace输出确认:open("/proc/424/cwd/.attach_pid424",O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE,0666.最有可能的原因是线程判断和信号发送这一步。我们以jstack为例,找出attach失败的原因。和上次搜索过程类似,想通过调试符号来查看,但是alpine上的debug符号不能显示源码内容,编译环境很麻烦。所以还是用strace查比较好。值得注意的是jstack的逻辑有fork,记得用strace-fjstack1查看一下,查看strace输出,没有killrequest,看来是线程模型的问题,刚才说了jvm会判断是不是LinuxThread,那么什么是LinuxThread呢,先看看判断源码:通俗的说,Linux内核是不支持“线程”的,LinuxThread机制通过fork机制+共享内存空间实现线程。但是,LinuxThread在内核中被看作是一些独立的父子进程,在信号处理和同步原语方面存在很多缺陷。线程来处理这些逻辑。后来RedHat发起了NPTL,内核开始支持线程能力,也可以用更标准的方式处理信号和同步等逻辑。您可以使用getconfGNU_LIBPTHREAD_VERSION来查看它是哪种线程模型。例如,我机器上的输出是NPTL2.34。当然,如上面代码所写。您可以使用confstr(_CS_GNU_LIBPTHREAD_VERSION,)获取当前的线程模型,具体请参考手册[2]。如果confstr(_CS_GNU_LIBPTHREAD_VERSION,)返回0,说明是老版本的glibc,认为是LinuxThread:先找到manager线程(通过寻找父进程),然后给每个子进程发送SIGQUIT信号进程(这个进程需要遍历系统中的所有进程)。如果confstr(_CS_GNU_LIBPTHREAD_VERSION,)的结果中包含NPTL,则认为不是LinuxThread,按照NPTL处理:直接发送SIGQUIT。但不幸的是,LinuxThread/confstr(_CS_GNU_LIBPTHREAD_VERSION,)不是POSIX标准,所以Alpine自带的musl对这个调用返回0。按照上面的逻辑,jvm会认为它是一个LinuxThread,并尝试寻找父进程。如果pid为1,自然找不到父进程,所以会报UnabletogetpidofLinuxThreadsmanagerthread的错误,导致文章开头提到的arthas无法使用。两种线程模型的详细对比请参考Linux线程模型对比:LinuxThreads和NPTL[3]。为什么不是No.1的进程可以attach?模拟完后,先手动进入shell(此时sh是1号进程),然后手动执行javaMain(pid为8),再看看getLinuxThreadsManager的表现如何:可以看到,在本例中,jvmThinkmanagerthread是1号进程,此时sendQuitToChildrenOf(mpid)会在后面执行:即遍历所有子进程,发送SIGQUIT。这个逻辑其实有点奇怪。“非凡的主张需要非凡的证据”[4]。让我们再次运行它并使用strace-f验证它。进程树(绿色的是线程):jstack发送的kill信号,可以看到jstack向1号进程的所有子进程发送了SIGQUIT:这个行为和刚才的分析是一致的。但是非常巧合的是,大多数进程都忽略了SIGQUIT信号,所以在这种情况下,jstack是正常工作的。如何解决这个问题呢?最快的workaround注:此方法不需要调整容器参数,不需要重启容器,推荐。由于attach主要停留在发送信号上,所以我们使用shell来模拟这个过程:pid=1;\touch/proc/${pid}/cwd/.attach_pid${pid}&&\kill-SIGQUIT${pid}&&\sleep2&&ls/proc/${pid}/root/tmp/.java_pid${pid}#接下来就可以正常了java-jararthas-boot.jarhangarthas经过上面的操作,AttachListener现在path已经启动并监听,可以直接连接第二个attach;你可以正常使用arthas。需要注意的一点是,必须提前创建.attach_pid${pid}文件,否则jvm会将这个信号交给默认的sigaction处理,而对于pid1,会导致容器退出!也有人根据类似的原理做了一个jattach[5]工具,可以直接在Alpine中通过apkaddjattach安装,然后jattach${pid}properties也可以达到同样的效果。设置启动参数注意:该方法需要调整启动参数或环境变量,重启应用/容器,可能会丢失业务站点。Jvm支持设置-XX:+StartAttachListener,这样在启动Jvm时,AttachListener线程可以自动启动并监听,也可以正常使用arthas。对于容器环境,在容器中添加环境变量JAVA_TOOL_OPTIONS=-XX:+StartAttachListener更简单,这样不用修改启动脚本就可以达到效果。先上游,修改图片注意:该方法需要修改图片。OpenJDK8官方还没有修复这个问题,所以如果直接使用openjdk:8-jdk-alpine,这个问题是无法避免的。Docker镜像仓库中也有人在讨论这个问题[6]。OpenJDK11已经解决了这个问题(见源码[7]),不再判断旧的LinuxThread模型,所以arthas也可以工作。不过Alpine官方仓库中的OpenJDK8已经通过自己打补丁的方式解决了这个问题:https://gitlab.alpinelinux.or...作为一个比较知名的JDK发行版,也在eclipse-temurin:8-jdk中-此问题已在alpine中修复,您可以直接使用此图像。相关讨论见:https://github.com/adoptium/j...总结在arthas的issue,或者网上的相关文章,总是反复说Java不能作为1号进程。很多时候,正因为如此,我们没有办法挂掉诊断工具,导致现场丢失,无法及时定位故障原因。作为技术人员,还是要了解底层,这样在故障排查和架构设计上才会有更大的自由度,能够更好的把握和解决问题。后续会有系列文章解决容器环境下的jvm怪问题,欢迎关注!相关链接[1]JavaAttach机制的原生文章https://my.oschina.net/u/3784...[2]详细参考手册https://man7.org/linux/man-pa...[3]Linux线程模型比较:LinuxThreads和NPTLhttps://www.jianshu.com/p/6c5...[4]非凡的主张需要非凡的证据https://zh.wikipedia.org/zh-hans/%E8%96%A9%E6%A0%B9%E6%A8%99%E6%BA%96[5]jattachhttps://github.com/apangin/ja...[6]Docker镜像仓库也有人讨论这个问题https://github.com/docker-lib...[7]源代码https://github.com/openjdk/jdk11u/blob/jdk-11%2B28/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java#L78