乐趣区

关于arthas:为什么在容器中-1-号进程挂不上-arthas

最近在容器环境中,发现在 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 和 NPTLhttps://www.jianshu.com/p/6c5…
[4] 超常的主张,须要有超常的证据 https://zh.wikipedia.org/zh-h…
[5] jattachhttps://github.com/apangin/ja…
[6] Docker 镜像仓库也有人探讨这个问题 https://github.com/docker-lib…
[7] 源码 https://github.com/openjdk/jd…

作者:卜比

原文链接

本文为阿里云原创内容,未经容许不得转载。

退出移动版