为什么在容器中 1 号进程挂不上 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 流程:

  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 篇[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 已经启动并且监听了路径,第二次 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.org/alpine/aports/-/issues/13032

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

总结

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

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

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

相关链接

[1] Java Attach 机制之 Native 篇

https://my.oschina.net/u/3784034/blog/5526214

[2] 详情参考手册

https://man7.org/linux/man-pages/man3/confstr.3.html

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

https://www.jianshu.com/p/6c507b966ad1

[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/jattach

[6] Docker 镜像仓库也有人讨论这个问题

https://github.com/docker-library/openjdk/issues/76

[7] 源码

https://github.com/openjdk/jdk11u/blob/jdk-11%2B28/src/jdk.attach/linux/classes/sun/tools/attach/VirtualMachineImpl.java#L78

作者:卜比

原文链接

本文为阿里云原创内容,未经允许不得转载。文章来源地址https://www.toymoban.com/news/detail-449292.html

到了这里,关于为什么在容器中 1 号进程挂不上 arthas?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 什么是线程?为什么需要线程?和进程的区别?

    目录 前言 一.线程是什么? 1.1.为什么需要线程 1.2线程的概念 1.3线程和进程的区别  二.线程的生命周期 三.认识多线程 总结 🎁个人主页:tq02的博客_CSDN博客-C语言,Java,Java数据结构领域博主 🎥 本文由 tq02 原创,首发于 CSDN🙉 🎄 本章讲解内容: 线程的讲解 🎥学习专栏:

    2024年02月14日
    浏览(59)
  • 僵尸进程?孤儿进程?为什么他有如此惨烈的身世...

    🌸作者简介: 花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。 🌸 专栏简介:本文收录于 Linux从入门到精通 ,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。

    2024年02月05日
    浏览(35)
  • 进程切换的开销为什么比线程更大

    进程切换的开销通常比线程更大,这是因为进程是操作系统中的基本执行单元,而线程是在进程内部的执行单元。 主要原因: 上下文切换的开销: 进程切换需要保存和恢复更多的上下文信息。上下文信息包括进程的程序计数器、寄存器状态、内存映射、I/O 状态等,这些信息

    2024年01月18日
    浏览(33)
  • 为什么要用开源容器?

    说到开源容器,大家首先想起来的应该是Docker吧,那么我们就以Docker来从个人角度理解一下为什么要用开源容器。 通常都会说Docker开源容器,但是Docker 实际上是一个开源的应用容器引擎。Docker是一个基于轻量级虚拟化技术的容器,整个项目基于Go语言开发,并采用了Apache 2

    2024年04月16日
    浏览(48)
  • 31、Spring容器启动时,为什么先加载BeanFactoryPostProcess

    因为BeanDefinition会在ioc容器加载的时候先注册, 而BeanFactoryPostProcess就是在所有的BeanDefinition注册完后做扩展的,所以要先加载BeanFactoryPostProcess 解析配置类的组件 它就实现BeanFactoryPostProcess, 所以要先去加载BeanFactoryPostProcess 方式一:通过BeanFactory获取 方式二 :通过BeanFactor

    2024年02月04日
    浏览(37)
  • 『Linux从入门到精通』第 ⑭ 期 - 僵尸进程?孤儿进程?为什么他有如此惨烈的身世...

    🌸作者简介: 花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。 🌸 专栏简介:本文收录于 Linux从入门到精通 ,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。

    2024年02月16日
    浏览(56)
  • 06 为什么需要多线程;多线程的优缺点;程序 进程 线程之间的关系;进程和线程之间的区别

    CPU、内存、IO之间的性能差异巨大 多核心CPU的发展 线程的本质是增加一个可以执行代码工人 多线程的优点 多个执行流,并行执行。(多个工人,干不一样的活) 多线程的缺点 上下文切换慢,切换上下文典型值1us vs 0.3ns/cycle CPU在执行A任务(A没有执行完)时,切换到任务B,需

    2024年02月14日
    浏览(39)
  • 多进程运行含有任意参数的函数、为什么multiprosessing会进行多次初始化

    目录 多进程运行含有任意个参数的函数,以map_async为例 为什么multiprocessing 的了进程会多次初始化?         使用偏函数:偏函数有点像数学中的偏导数,可以让我们只关注其中的某一个变量而不考虑其他变量的影响。 如以下代码中,我们要将set_seq、tokenizer和model作为变量

    2024年02月03日
    浏览(44)
  • 在容器镜像中为了安全为什么要删除 setuid 和 setgid?

    在容器镜像中删除 setuid(set user ID)和 setgid(set group ID)权限通常是出于安全考虑。这两个权限位允许进程在执行时以文件所有者或文件所属组的身份运行,而不是以调用进程的用户身份运行。 删除 setuid 和 setgid 权限的主要原因包括: 减少潜在的权限滥用: 如果容器中的

    2024年02月20日
    浏览(25)
  • 【容器架构】你知道有 Docker 为什么还要 K8s 吗?

    👉 博主介绍 : 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO TOP红人 Java知识图谱点击链接: 体系化学习Java(Java面试专题) 💕💕 感兴趣的同学可以收藏关注下 , 不然下次找不到哟

    2024年02月16日
    浏览(59)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包