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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 为什么在容器中1号进程挂不上arthas?

文是《容器中的Java》系列文章之 4/n ,欢迎关注后续连载 :) 。


最近在容器环境中,发现在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/.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篇也是一个不错的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,)来获取当前的线程模型,详情参考手册

  • 如果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

为什么非1号进程就能attach?

模拟了下先手动进入shell(这时sh就是1号进程),然后再手动执行java Main(pid为8),然后我们看下getLinuxThreadsManager是怎么表现的:

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

即遍历所有的子进程,都发送SIGQUIT,这个逻辑其实是有点奇怪的。“超凡的主张,需要有超凡的证据”。我们重新跑一遍,用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工具,可以直接在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镜像仓库也有人讨论这个问题

OpenJDK 11就已经解决了这个问题了(见源码),不再对古旧的LinuxThread模型做判断,这样arthas也能工作。

不过Alpine官方仓库中的OpenJDK 8已经通过自己打patch的方式,修复了这个问题https://gitlab.alpinelinux.org/alpine/aports/-/issues/13032

作为比较知名的JDK发行版,也在eclipse-temurin:8-jdk-alpine中修复了这个问题,可以直接使用这个镜像,相关讨论见https://github.com/adoptium/jdk8u/pull/8

总结

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

作为技术人员还是需要了解底层,这样在排查问题、架构设计上才会有更多自由度,更能够抓住问题、解决问题。

后续还会出系列文章,来解决容器环境下奇奇怪怪的jvm问题,欢迎关注!

相关文章
|
6月前
|
Arthas Java 测试技术
Arthas本身并没有提供直接让进程结束时自动生成火焰图的配置
【2月更文挑战第31天】Arthas本身并没有提供直接让进程结束时自动生成火焰图的配置
162 2
|
6月前
|
Arthas 测试技术
这个错误提示表明Arthas无法打开目标进程的socket文
【1月更文挑战第11天】【1月更文挑战第55篇】这个错误提示表明Arthas无法打开目标进程的socket文
867 4
|
Kubernetes Ubuntu Cloud Native
深入剖析Kubernetes学习笔记-05 | 白话容器基础(一):从进程说开去
深入剖析Kubernetes学习笔记-05 | 白话容器基础(一):从进程说开去
134 0
|
3月前
|
Kubernetes Shell 测试技术
在Docker中,可以在一个容器中同时运行多个应用进程吗?
在Docker中,可以在一个容器中同时运行多个应用进程吗?
|
Linux Docker 容器
在Docker守护进程停机期间保持容器运行(即重启Docker时,正在运行的容器不会停止)
在Docker守护进程停机期间保持容器运行(即重启Docker时,正在运行的容器不会停止)
407 0
|
6月前
|
Arthas 测试技术
错误提示表明Arthas无法打开目标进程的socket文件
错误提示表明Arthas无法打开目标进程的socket文件
99 2
|
NoSQL Shell Linux
跨cpu架构部署容器技术点:怎么将容器启动时的1号进程挂载到systemctl
--privileged=true:是Docker中的一个参数,用于授予容器的特权权限。当一个容器被设置为特权容器时,它将拥有与主机操作系统相同的权限,可以执行一些高级操作,如访问主机设备、加载内核模块等。
92 0
|
监控 Java 数据安全/隐私保护
在Docker容器中,有时候无法监控到正在运行的进程
在Docker容器中,有时候无法监控到正在运行的进程
313 2
|
存储 Kubernetes Java
【探索 Kubernetes|容器基础进阶篇 系列 3】容器进程的文件系统
【探索 Kubernetes|容器基础进阶篇 系列 3】容器进程的文件系统
278 0
|
Kubernetes Cloud Native 安全
【探索 Kubernetes|容器基础进阶篇 系列1】容器的本质是进程
大家好,我是秋意零。 😈 CSDN作者主页 • 😎 博客主页 👿 简介 • 👻 普通本科生在读 • 在校期间参与众多计算机相关比赛,如:🌟 “省赛”、“国赛”,斩获多项奖项荣誉证书 • 🔥 各个平台,秋意零/秋意临 账号创作者 • 🔥 云社区 创建者 点赞、收藏+关注下次不迷路! 欢迎加入云社区
199 0