作者:卜比

本文是《容器中的 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.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(重点代码都曾经标记):

相熟类加载机制的同学们可能猜到了,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.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就能够畅快执行了:


最终解决方案

咱晓得有的时候,咱们仅仅须要一个答案:

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