最近在容器環(huán)境中,發(fā)現在 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 號進程就可以了。但這個不是長久之計,還是要抽時間看下這個問題。
復現問題
我們創(chuàng)建如下項目,來復現這個問題:
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
$ # 在另一個終端進入容器,執(zhí)行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,如果存在則檢查權限,然后建立連接。
- 如果不存在則先創(chuàng)建 /proc/${pid}/cwd/.attach_pid${pid},開始通知 jvm 線程。
- 首先判斷是不是 LinuxThread如果是 LinuxThread則找到 LinuxThreadsManager,然后給其所有子進程發(fā)送 SIGQUIT.
- 如果不是 LinuxThread,則直接給目標進程發(fā)送 SIGQUIT。
- 目標進程收到信號后,創(chuàng)建 Attach Listener,監(jiān)聽 /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} 文件也是能夠創(chuàng)建成功的,
我們也可以通過 strace 輸出來確認:
open("/proc/424/cwd/.attach_pid424", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0666 <unfinished ...>。
最有可能的原因就是線程判斷、發(fā)送信號這一步了,我們以 jstack 為例查找為什么 attach 會失敗。
本來類似上一次的查找過程,想著通過調試符號來查,但是在 alpine 上的調試符號無法顯示源碼內容,編譯環(huán)境又很麻煩。所以還是優(yōu)先用 strace 來查,值得注意的是, jstack 的邏輯中有 fork,所以記得使用 strace -f jstack 1 來查。
查了下 strace 的輸出,沒有 kill 請求??磥韱栴}是處在線程模型判定的。
剛剛提到 jvm 會判斷是不是 LinuxThread,那么什么是 LinuxThread 呢?首先看下判斷的源碼:
通俗的講,Linux 內核剛開始是不支持“線程”的,LinuxThread 機制就是通過 fork 機制+共享內存空間的方式來實現線程。但 LinuxThread 在內核看來就是一些獨立的父子進程,在信號處理、同步原語上有很多缺陷,要通過 manager thread 來處理這些邏輯。后來 Red Hat 發(fā)起 NPTL,內核開始支持線程能力,也能夠通過更加標準的方式來處理信號、同步等邏輯。
可以用 getconf GNU_LIBPTHREAD_VERSION 來查看是哪種線程模型,比如我的機器上輸出是 NPTL 2.34。
當然,如上面代碼所寫。可以用 confstr(_CS_GNU_LIBPTHREAD_VERSION,) 來獲取當前的線程模型,詳情參考手冊[2]。
- 如果 confstr(_CS_GNU_LIBPTHREAD_VERSION,) 返回 0,則表示是 glibc 舊版本,認為是 LinuxThread:先找到 manager thread(通過查找父進程),然后給各個子進程發(fā)送 SIGQUIT 信號(這個過程需要遍歷系統(tǒng)內所有進程)。
- 如果 confstr(_CS_GNU_LIBPTHREAD_VERSION,) 結果包含 NPTL,則認為不是 LinuxThread,按照 NPTL 來處理:直接發(fā)送 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 號進程),然后再手動執(zhí)行 java Main(pid為 8 ),然后我們看下 getLinuxThreadsManager 是怎么表現的:
可以看到,在這種情況下,jvm 認為 manager thread 是 1 號進程。此時會后執(zhí)行 sendQuitToChildrenOf(mpid):
即遍歷所有的子進程,都發(fā)送 SIGQUIT,這個邏輯其實是有點奇怪的。“超凡的主張,需要有超凡的證據”[4]。我們重新跑一遍,用 strace -f 驗證一下。
進程樹(其中綠色的是線程):
jstack 發(fā)送的 kill 信號,可以看到 jstack 給 1 號進程的所有子進程發(fā)送了 SIGQUIT:
這個行為和剛剛分析是一致的。不過非常巧合的是,大部分進程是忽略了 SIGQUIT 信號的,所以在這種情況下,jstack 反而是正常工作了的。
怎么解決這個問題?
最快捷 workaround
注:這種方式不需要調整容器參數,不需要重啟容器,比較推薦。
既然 attach 主要卡在了發(fā)送信號上,那我們就用 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 已經啟動并且監(jiān)聽了路徑,第二次 attach 就直接可以連接了;就可以按照正常的方式使用 arthas 了。
其中有一點需要注意,一定需要提前創(chuàng)建 .attach_pid${pid} 文件,不然 jvm 會將這個信號交給默認的 sigaction 處理,對于 pid 1 來說,會導致容器退出!
也有人基于類似原理,做了一個?jattach[5]?工具,可以直接在 Alpine 中,通過 apk add jattach 來安裝,然后 jattach ${pid} properties,也能起到一樣的效果。
設置啟動參數
注:這種方式需要調整啟動參數或者環(huán)境變量,需要重啟應用/容器,可能會丟失業(yè)務現場。
Jvm 支持設置 -XX:+StartAttachListener,這樣就能在啟動 Jvm 的時候,自動啟動 Attach Listener 線程并監(jiān)聽,也可以正常使用 arthas。
對于容器環(huán)境下,更加容易的做法是給容器添加環(huán)境變量 JAVA_TOOL_OPTIONS=-XX:+StartAttachListener,這樣不用修改啟動腳本也能達到效果。
上游優(yōu)先,修改鏡像
注:這種方式需要修改鏡像。
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 發(fā)行版,也在 eclipse-temurin:8-jdk-alpine 中修復了這個問題,可以直接使用這個鏡像。相關討論見:https://github.com/adoptium/jdk8u/pull/8
總結
在 arthas 的 issue 中,或者網上相關的文章中,總是重復著 Java 不能作為 1 號進程。很多時候,就因為如此,我們沒有辦法掛上診斷工具,導致現場丟失,故障原因不能及時定位。
作為技術人員還是需要了解底層,這樣在排查問題、架構設計上才會有更多自由度,更能夠抓住問題、解決問題。
后續(xù)還會出系列文章,來解決容器環(huán)境下奇奇怪怪的 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
作者:卜比
原文鏈接文章來源:http://www.zghlxwxcb.cn/news/detail-449292.html
本文為阿里云原創(chuàng)內容,未經允許不得轉載。文章來源地址http://www.zghlxwxcb.cn/news/detail-449292.html
到了這里,關于為什么在容器中 1 號進程掛不上 arthas?的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!