作者:卜比

本文是《容器中的 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 流程:

  1. 查找 /tmp/.java_pid${pid} 这个 unix socket,假如存在则检查权限,然后树立衔接。
  2. 假如不存在则先创立 /proc/pid/cwd/.attachpid{pid}/cwd/.attach_pid{pid},开端通知 jvm 线程。
  3. 首要判别是不是 LinuxThread假如是 LinuxThread则找到 LinuxThreadsManager,然后给其一切子进程发送 SIGQUIT.
  4. 假如不是 LinuxThread,则直接给方针进程发送 SIGQUIT。
  5. 方针进程收到信号后,创立 Attach Listener,监听 /tmp/.java_pid${pid}。
  6. 开端正常的 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 呢?首要看下判别的源码:

为什么在容器中 1 号进程挂不上 arthas?

浅显的讲,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 是怎样体现的:

为什么在容器中 1 号进程挂不上 arthas?

可以看到,在这种情况下,jvm 认为 manager thread 是 1 号进程。此刻会后履行 sendQuitToChildrenOf(mpid):

为什么在容器中 1 号进程挂不上 arthas?

为什么在容器中 1 号进程挂不上 arthas?

即遍历一切的子进程,都发送 SIGQUIT,这个逻辑其实是有点奇怪的。 **“超凡的建议,需求有超凡的依据” [ 4] **。咱们重新跑一遍,用 strace -f 验证一下。

进程树(其间绿色的是线程):

为什么在容器中 1 号进程挂不上 arthas?

jstack 发送的 kill 信号,可以看到 jstack 给 1 号进程的一切子进程发送了 SIGQUIT:

为什么在容器中 1 号进程挂不上 arthas?

这个行为和刚刚剖析是共同的。不过十分偶然的是,大部分进程是忽略了 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 现已发动并且监听了途径,第2次 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 的方法,修正了这个问题:

gitlab.alpinelinux.org/alpine/apor…

作为比较闻名的 JDK 发行版,也在 eclipse-temurin:8-jdk-alpine 中修正了这个问题,可以直接运用这个镜像。相关评论见:

github.com/adoptium/jd…

总结

在 arthas 的 issue 中,或许网上相关的文章中,总是重复着 Java 不能作为 1 号进程。许多时候,就因为如此,咱们没有办法挂上诊断工具,导致现场丢掉,毛病原因不能及时定位。

作为技术人员仍是需求了解底层,这样在排查问题、架构规划上才会有更多自由度,更可以抓住问题、处理问题。

后续还会出系列文章,来处理容器环境下奇奇怪怪的 jvm 问题,欢迎重视!

相关链接

[1] Java Attach 机制之Native 篇

my.oschina.net/u/3784034/b…

[2]概况参阅手册

man7.org/linux/man-p…

[3] Linux 线程模型比较:LinuxThreads 和 NPTL

www.jianshu.com/p/6c507b966…

[4]超凡的建议,需求有超凡的依据

zh.wikipedia.org/zh-hans/%E8…

[5]jattach

github.com/apangin/jat…

[6]Docker 镜像库房也有人评论这个问题

github.com/docker-libr…

[7]源码

github.com/openjdk/jdk…