写点什么

听说你没法在 JRE 中使用 arthas?不,你可以

  • 2023-03-02
    浙江
  • 本文字数:3083 字

    阅读完需:约 10 分钟

作者:卜比


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



之前经常遇到的问题是,排查问题需要挂 arthas,但客户用的是 JRE,没法挂载 arthas。就只能让客户更换成 JDK,再重新部署、排查问题。


很多有用的现场,在这个过程中也会丢失,最终导致问题排查效率降低。于是就探索了下如何在 JRE 环境中,使用 artahs。

复现问题

如果一个 Bug 没法复现,研发大概率是无法修复的。—— by 网友


我们写一个 Java 例子和 Dockerfile:


// ./src/main/java/Main.javapublic class Main {  public static void main(String[] args) throws Exception {    while (true) {      System.out.println("hello!");      Thread.sleep(30 * 1000);    }  }}
复制代码


# ./DockerfileFROM openjdk:8-jdk-alpine as builderCOPY ./ /appWORKDIR /app/src/main/java/# 编译java文件RUN javac Main.java
# 运行时容器使用JREFROM openjdk:8-jre-alpineRUN apk add bash curl busybox-extrasWORKDIR /app/src/main/java/# 将arthas copy 到容器中COPY --from=hengyunabc/arthas:latest /opt/arthas /opt/arthasCOPY --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.shArthas script version: 3.6.7tools.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.jarError: A JNI error has occurred, please check your installation and try againException 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(重点代码都已经标记):


1.png


熟悉类加载机制的同学们可能猜到了,Arthas.class 中依赖了 com.sun.tools.的一些类,所以上面的报错其实是在类链接的时候就报错了。这也是为什么报错的 stacktrace 中没有任何 arthas 的包出现。


看着上面 arthas 的代码,就不得不思考下如何规避掉对 tools.jar 的依赖了。

如何去除对 JDK 的依赖

第一 像图中这样,直接调用 com.sun.tools.attach.*相关类、方法,是肯定不行的,上面的报错其实已经很说明情况了。另外,通过反射也不行,tools.jar 就不存在,自然无法加载这些类。


第二, 能不能通过我们手动把 tools.jar 放到容器中的方式呢?理论上确实可以,相关 issue 也说了具体的操作和注意事项:


2.png


理论上这样确实能工作,但其一,tools.jar 是根据不同的 jdk 发行版、不同的 jdk 版本而不同的。比如,同样在 eclipse-temurin:11-jre-alpine 里面也挂不上 arthas,你就不能 copy jdk8 的 tools.jar 来处理。


我们在继续看下有没有其他方式来挂 agent。


第三, 看了一圈,ByteBuddy 实现了 attach agent 的功能。但 ByteBuddy 是通过逐个尝试的方式来尝试 attach,而且几乎都依赖 tools.jar,大家感兴趣的话,可以看下下面几个策略的实现:


3.png


看起来我们可以自己实现一个 AttachmentProvider,然后改造 arthas 通过 ByteBuddy 挂 agent 就可以了。


刚开始也是这样想的,甚至代码都写了一半了。直到晚上回家路上,想到上一篇文章中说的,可以通过自定义脚本或者 jattach 的方式来 attach。


第四, 通过 jattach 来加载。


参考 jattach 的文档,如下操作下即可:


# 安装 jattach$ apk add jattach
# 挂载arthas-agent.jar$ jattach 1 load instrument false /opt/arthas/arthas-agent.jarConnected to remote JVMJVM response code = 0return code: 0
# netstat确认下监听端口$ netstat -alnpActive Internet connections (servers and established)Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program nametcp 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 就可以畅快执行了:


4.png


5.png

最终解决方案

咱知道有的时候,我们仅仅需要一个答案:


$ 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》系列下回分解吧。

发布于: 2 小时前阅读数: 13
用户头像

阿里云云原生 2019-05-21 加入

还未添加个人简介

评论

发布
暂无评论
听说你没法在 JRE 中使用 arthas?不,你可以_Java_阿里巴巴云原生_InfoQ写作社区