作者:卜比
本文是《容器中的 Java》系列文章之 5/n,欢送关注后续连载 :)。
- JVM 如何获取以后容器的资源限度?——容器中的 Java 1
- Java Agent 踩坑之 appendToSystemClassLoaderSearch 问题——容器中的 Java 2
- 让 Java Agent 在 Dragonwell 上更好用——容器中的 Java 3
- 为什么在容器中 1 号过程挂不上 arthas?——容器中的 Java 4
之前常常遇到的问题是,排查问题须要挂 arthas,但客户用的是 JRE,没法挂载 arthas。就只能让客户更换成 JDK,再重新部署、排查问题。
很多有用的现场,在这个过程中也会失落,最终导致问题排查效率升高。于是就摸索了下如何在 JRE 环境中,应用 artahs。
复现问题
如果一个 Bug 没法复现,研发大概率是无奈修复的。—— by 网友
咱们写一个 Java 例子和 Dockerfile:
// ./src/main/java/Main.java
public class Main {public static void main(String[] args) throws Exception {while (true) {System.out.println("hello!");
Thread.sleep(30 * 1000);
}
}
}
# ./Dockerfile
FROM openjdk:8-jdk-alpine as builder
COPY ./ /app
WORKDIR /app/src/main/java/
# 编译 java 文件
RUN javac Main.java
# 运行时容器应用 JRE
FROM openjdk:8-jre-alpine
RUN apk add bash curl busybox-extras
WORKDIR /app/src/main/java/
# 将 arthas copy 到容器中
COPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthas
COPY --from=builder /app/src/main/java/ /app/src/main/java/
CMD ["java", "Main"]
构建并失常启动利用,并尝试用 arthas attach,此处为了便于理解原理,咱们应用 as.sh 来执行:
$ # 构建镜像
$ docker build . -t example-attach
$ # 启动容器
$ docker run --name example-attach --rm example-attach
$ # 在另一个终端进入容器,执行 as.sh
$ docker exec -it example-attach sh
/app/src/main/java $ /opt/arthas/as.sh
Arthas script version: 3.6.7
tools.jar was not found, so arthas could not be launched!
行吧,咱们先用 jdk 运行下,先看下 arthas 是怎么 attach 起来的:
# 替换容器为 JDK 镜像并运行
# 先启动 Attach Listener
$ pid=1 ;\
touch /proc/${pid}/cwd/.attach_pid${pid} && \
kill -SIGQUIT ${pid} && \
sleep 2 &&
ls /proc/${pid}/root/tmp/.java_pid${pid}
# - x 示意调试执行,会输入执行了哪些命令;1 为 java 过程 pid
$ bash -x /opt/arthas/as.sh 1
...
+ /usr/lib/jvm/java-1.8-openjdk/bin/java -Xbootclasspath/a:/usr/lib/jvm/java-1.8-openjdk/lib/tools.jar -Djava.awt.headless=true -jar /opt/arthas/arthas-core.jar -pid 1 -core /opt/arthas/arthas-core.jar -agent /opt/arthas/arthas-agent.jar
...
+ telnet 127.0.0.1 3658
...
能够看到,最次要的逻辑是 java -jar arthas-core.jar -pid 1 -core arthas-core.jar -agent arthas-agent.jar,而后再去连贯 3658 端口。
-Xbootclasspath/a:tools.jar 当然有用,然而在 JRE 中没有 tools.jar,所以能够疏忽。那么下面的逻辑咱们间接尝试在 JRE 上运行呢?咱们持续在 JRE 镜像中执行下面的命令:
# 替换容器为 JRE 镜像并运行
# 先启动 Attach Listener
$ pid=1 ;\
touch /proc/${pid}/cwd/.attach_pid${pid} && \
kill -SIGQUIT ${pid} && \
sleep 2 &&
ls /proc/${pid}/root/tmp/.java_pid${pid}
$ cd /opt/arthas/
$ java -jar arthas-core.jar -pid 1 -core arthas-core.jar -agent arthas-agent.jar
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/tools/attach/AgentLoadException
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
at java.lang.Class.privateGetMethodRecursive(Class.java:3048)
at java.lang.Class.getMethod0(Class.java:3018)
at java.lang.Class.getMethod(Class.java:1784)
at sun.launcher.LauncherHelper.validateMainClass(LauncherHelper.java:544)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:526)
Caused by: java.lang.ClassNotFoundException: com.sun.tools.attach.AgentLoadException
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 7 more
对照代码来看,这个报错其实很失常,arthas-core 中会调用 Attach API,而后加载 Agent(重点代码都曾经标记):
相熟类加载机制的同学们可能猜到了,Arthas.class 中依赖了 com.sun.tools. 的一些类,所以下面的报错其实是在类链接的时候就报错了。这也是为什么报错的 stacktrace 中没有任何 arthas 的包呈现。
看着下面 arthas 的代码,就不得不思考下如何躲避掉对 tools.jar 的依赖了。
如何去除对 JDK 的依赖
第一 , 像图中这样,间接调用 com.sun.tools.attach.* 相干类、办法,是必定不行的,下面的报错其实曾经很阐明状况了。另外,通过反射也不行,tools.jar 就不存在,天然无奈加载这些类。
第二, 能不能通过咱们手动把 tools.jar 放到容器中的形式呢?实践上的确能够,相干 issue 也说了具体的操作和注意事项:
实践上这样的确能工作,但其一,tools.jar 是依据不同的 jdk 发行版、不同的 jdk 版本而不同的。比方,同样在 eclipse-temurin:11-jre-alpine 外面也挂不上 arthas,你就不能 copy jdk8 的 tools.jar 来解决。
咱们在持续看下有没有其余形式来挂 agent。
第三, 看了一圈,ByteBuddy 实现了 attach agent 的性能。但 ByteBuddy 是通过一一尝试的形式来尝试 attach,而且简直都依赖 tools.jar,大家感兴趣的话,能够看下上面几个策略的实现:
看起来咱们能够本人实现一个 AttachmentProvider,而后革新 arthas 通过 ByteBuddy 挂 agent 就能够了。
刚开始也是这样想的,甚至代码都写了一半了。直到早晨回家路上,想到上一篇文章中说的,能够通过自定义脚本或者 jattach 的形式来 attach。
第四, 通过 jattach 来加载。
参考 jattach 的文档,如下操作下即可:
# 装置 jattach
$ apk add jattach
# 挂载 arthas-agent.jar
$ jattach 1 load instrument false /opt/arthas/arthas-agent.jar
Connected to remote JVM
JVM response code = 0
return code: 0
# netstat 确认下监听端口
$ netstat -alnp
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:3658 0.0.0.0:* LISTEN 1/java
...
# 连贯对应端口
$ java -jar /opt/arthas/arthas-client.jar 127.0.0.1 3658
通过了如上操作,arthas 就能够畅快执行了:
最终解决方案
咱晓得有的时候,咱们仅仅须要一个答案:
$ pid=1 ;\
jattach ${pid} load instrument false /opt/arthas/arthas-agent.jar && \
java -jar /opt/arthas/arthas-client.jar 127.0.0.1 3658
总结
相比上一次 musl+jdk8+pid 1 的问题,这次咱们用 attach 机制做了更多的事件。开发同学遇到 JRE,再也不必换 JDK、换镜像,可能最大水平的保留现场,问题排查就变得顺畅高效的多了。当然,在容器环境中,Java 利用遇到的奇奇怪怪的状况,不止如此,欲知后事如何,且听《容器中的 Java》系列下回分解吧。