共计 5463 个字符,预计需要花费 14 分钟才能阅读完成。
作者:卜比
本文是《容器中的 Java》系列文章之 4/n,欢送关注后续连载 :)。
系列 1:JVM 如何获取以后容器的资源限度?
系列 2:Java Agent 踩坑之
appendToSystemClassLoaderSearch 问题
系列 3:让 Java Agent 在 Dragonwell 上更好用
最近在容器环境中,发现在 Java 过程是 1 号过程的状况下,无奈应用 arthas。
提醒 AttachNotSupportedException: Unable to get pid of LinuxThreads manager thread。具体操作和报错如下:
# java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.6
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 1 com.alibabacloud.mse.demo.ZuulApplication
1
[INFO] arthas home: /home/admin/.opt/ArmsAgent/arthas
[INFO] Try to attach process 1
[ERROR] Start arthas failed, exception stack trace:
com.sun.tools.attach.AttachNotSupportedException: Unable to get pid of LinuxThreads manager thread
at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:86)
at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:78)
at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:250)
at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:117)
at com.taobao.arthas.core.Arthas.<init>(Arthas.java:27)
at com.taobao.arthas.core.Arthas.main(Arthas.java:166)
[INFO] Attach process 1 success.
之前也遇到过,总是调整了下镜像,让 Java 过程不是 1 号过程就能够了。但这个不是长久之计,还是要抽时间看下这个问题。
复现问题
咱们创立如下我的项目,来复现这个问题:
public class Main {public static void main(String args[]) throws Exception {while (true) {System.out.println("hello!");
Thread.sleep(30 * 1000);
}
}
}
FROM openjdk:8u212-jdk-alpine
COPY ./ /app
WORKDIR /app/src/main/java/
RUN javac Main.java
CMD ["java", "Main"]
而后失常启动利用,并尝试用 arthas,或者 jstack:
$ # 构建镜像
$ docker build . -t example-attach
$ # 启动容器
$ docker run --name example-attach --rm example-attach
$ # 在另一个终端进入容器,执行 jstack
$ docker exec -it example-attach sh
/app/src/main/java # jstack 1
1: Unable to get pid of LinuxThreads manager thread
胜利复现问题!接下来开始剖析。
失常的 attach 流程是什么样子的?
如下是在排查问题中,梳理进去的 jvm Attach 流程:
- 查找 /tmp/.java_pid${pid} 这个 unix socket,如果存在则查看权限,而后建设连贯。
- 如果不存在则先创立 /proc/${pid}/cwd/.attach_pid${pid},开始告诉 jvm 线程。
- 首先判断是不是 LinuxThread 如果是 LinuxThread 则找到 LinuxThreadsManager,而后给其所有子过程发送 SIGQUIT.
- 如果不是 LinuxThread,则间接给指标过程发送 SIGQUIT。
- 指标过程收到信号后,创立 Attach Listener,监听 /tmp/.java_pid${pid}。
- 开始失常的 socket 通信,依据通信的具体内容,能够是 dumpThread(jstack),也能够是加载 JavaAgent,比方下面提到的 arthas。
Java Attach 机制之 Native 篇 [ 1] 也是一个不错的 attach API 解析。
为什么对 1 号过程 attach 会报错?
首先,/tmp/.java_pid${pid} 过后是必定不存在的,如果存在就是间接通信加载 Arthas 了。也能够通过查看文件来确认这一点。
其次,.attach_pid${pid} 文件也是可能创立胜利的,
咱们也能够通过 strace 输入来确认:
open(“/proc/424/cwd/.attach_pid424”, O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0666 <unfinished …>。
最有可能的起因就是线程判断、发送信号这一步了,咱们以 jstack 为例查找为什么 attach 会失败。
原本相似上一次的查找过程,想着通过调试符号来查,然而在 alpine 上的调试符号无奈显示源码内容,编译环境又很麻烦。所以还是优先用 strace 来查,值得注意的是,jstack 的逻辑中有 fork,所以记得应用 strace -f jstack 1 来查。
查了下 strace 的输入,没有 kill 申请。看来问题是处在线程模型断定的。
刚刚提到 jvm 会判断是不是 LinuxThread,那么什么是 LinuxThread 呢?首先看下判断的源码:
艰深的讲,Linux 内核刚开始是不反对“线程”的,LinuxThread 机制就是通过 fork 机制 + 共享内存空间的形式来实现线程。但 LinuxThread 在内核看来就是一些独立的父子过程,在信号处理、同步原语上有很多缺点,要通过 manager thread 来解决这些逻辑。起初 Red Hat 发动 NPTL,内核开始反对线程能力,也可能通过更加规范的形式来解决信号、同步等逻辑。
能够用 getconf GNU_LIBPTHREAD_VERSION 来查看是哪种线程模型,比方我的机器上输入是 NPTL 2.34。
当然,如下面代码所写。能够用 confstr(_CS_GNU_LIBPTHREAD_VERSION,) 来获取以后的线程模型,详情参考手册 [ 2] 。
- 如果 confstr(_CS_GNU_LIBPTHREAD_VERSION,) 返回 0,则示意是 glibc 旧版本,认为是 LinuxThread:先找到 manager thread(通过查找父过程),而后给各个子过程发送 SIGQUIT 信号(这个过程须要遍历零碎内所有过程)。
- 如果 confstr(_CS_GNU_LIBPTHREAD_VERSION,) 后果蕴含 NPTL,则认为不是 LinuxThread,依照 NPTL 来解决:间接发送 SIGQUIT。
但很惋惜的是,LinuxThread/confstr(_CS_GNU_LIBPTHREAD_VERSION,) 不是 POSIX 规范,所以 Alpine 自带的 musl 对这个调用返回 0。
依照下面逻辑,jvm 会认为是 LinuxThread,尝试找到父过程,如果 pid 是 1 的话,天然找不到父过程,所以报错 Unable to get pid of LinuxThreads manager thread,导致文章最开始说的 arthas 无奈应用。
对于两种线程模型的具体比拟,能够参考 Linux 线程模型比拟:LinuxThreads 和 NPTL [3 ] 。
为什么非 1 号过程就能 attach?
模仿了下先手动进入 shell(这时 sh 就是 1 号过程),而后再手动执行 java Main(pid 为 8),而后咱们看下 getLinuxThreadsManager 是怎么体现的:
能够看到,在这种状况下,jvm 认为 manager thread 是 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} && \
sleep 2 &&
ls /proc/${pid}/root/tmp/.java_pid${pid}
# 接下来就能够失常 java -jar arthas-boot.jar 挂 arthas 了
通过下面的操作后,Attach Listener 曾经启动并且监听了门路,第二次 attach 就间接能够连贯了;就能够依照失常的形式应用 arthas 了。
其中有一点须要留神,肯定须要提前创立 .attach_pid${pid} 文件, 不然 jvm 会将这个信号交给默认的 sigaction 解决,对于 pid 1 来说,会导致容器退出!
也有人基于相似原理,做了一个 jattach [ 5] 工具,能够间接在 Alpine 中,通过 apk add jattach 来装置,而后 jattach ${pid} properties,也能起到一样的成果。
设置启动参数
注:这种形式须要调整启动参数或者环境变量,须要重启利用 / 容器,可能会失落业务现场。
Jvm 反对设置 -XX:+StartAttachListener,这样就能在启动 Jvm 的时候,主动启动 Attach Listener 线程并监听,也能够失常应用 arthas。
对于容器环境下,更加容易的做法是给容器增加环境变量 JAVA_TOOL_OPTIONS=-XX:+StartAttachListener,这样不必批改启动脚本也能达到成果。
上游优先,批改镜像
注:这种形式须要批改镜像。
OpenJDK 8 官网没有修复这个问题,所以如果间接应用 openjdk:8-jdk-alpine,是防止不了这个问题的。Docker 镜像仓库也有人探讨这个问题 [ 6] 。
OpenJDK 11 就曾经解决了这个问题了(见 源码 [ 7] ),不再对古旧的 LinuxThread 模型做判断,这样 arthas 也能工作。
不过 Alpine 官网仓库中的 OpenJDK 8 曾经通过本人打 patch 的形式,修复了这个问题:
https://gitlab.alpinelinux.or…
作为比拟出名的 JDK 发行版,也在 eclipse-temurin:8-jdk-alpine 中修复了这个问题,能够间接应用这个镜像。相干探讨见:
https://github.com/adoptium/j…
总结
在 arthas 的 issue 中,或者网上相干的文章中,总是反复着 Java 不能作为 1 号过程。很多时候,就因为如此,咱们没有方法挂上诊断工具,导致现场失落,故障起因不能及时定位。
作为技术人员还是须要理解底层,这样在排查问题、架构设计上才会有更多自由度,更可能抓住问题、解决问题。
后续还会出系列文章,来解决容器环境下奇奇怪怪的 jvm 问题,欢送关注!
相干链接
[1] Java Attach 机制之 Native 篇
https://my.oschina.net/u/3784…
[2] 详情参考手册
https://man7.org/linux/man-pa…
[3] Linux 线程模型比拟:LinuxThreads 和 NPTL
https://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] jattach
https://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