ITPub博客

首页 > 云计算 > 私有云实践 > 听说你没法在 JRE 中使用 arthas?不,你可以

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

原创 私有云实践 作者:阿里巴巴云原生 时间:2023-03-02 18:14:04 0 删除 编辑

*作者:卜比*


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


-   [JVM如何获取当前容器的资源限制?](http://mp.weixin.qq.com/s?__biz=MzUzNzYxNjAzMg==&mid=2247548101&idx=2&sn=9b2a810941d5d61f45abbecced9667b3&chksm=fae6370acd91be1c046e6a6e9642ba3fd51156741d65b6aadbf966f7f25d426f6c71e44a7980&scene=21#wechat_redirect)——容器中的Java 1

-   [Java Agent踩坑之appendToSystemClassLoaderSearch问题](http://mp.weixin.qq.com/s?__biz=MzUzNzYxNjAzMg==&mid=2247548252&idx=2&sn=fc1e31f55115315118998022b31c527d&chksm=fae63693cd91bf8501752194ae24f8e5d81394e05d9e8f10b1f5f3f6387da36e3b29a7f38a7a&scene=21#wechat_redirect)——容器中的Java 2

-   [让 Java Agent 在 Dragonwell 上更好用](http://mp.weixin.qq.com/s?__biz=MzUzNzYxNjAzMg==&mid=2247548616&idx=2&sn=8c6334dd63d76b5a79b5f1bf6a510d49&chksm=fae63507cd91bc11276837fae96d54b829dc87e58501a5d62be34b31f383d9ccf0293d1e6433&scene=21#wechat_redirect)——容器中的Java 3

-   [为什么在容器中1号进程挂不上arthas?](http://mp.weixin.qq.com/s?__biz=MzUzNzYxNjAzMg==&mid=2247549897&idx=2&sn=937b177af89547ef229bd3900476f93b&chksm=fae60806cd918110eadba3a359a02d6b073d8ee46f453d6e0a710facc1acb79c12dd4281e373&scene=21#wechat_redirect)——容器中的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(重点代码都已经标记):


![1.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9476d0ceddf4adf8924c9d7e884fe34~tplv-k3u1fbpfcp-zoom-1.image "1.png")


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


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


## 如何去除对 JDK 的依赖


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


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


![2.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/29c548e459a7418c9292c0d0c03b2bdf~tplv-k3u1fbpfcp-zoom-1.image "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](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cc288841c4854cbe9594ecc28eba6184~tplv-k3u1fbpfcp-zoom-1.image "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.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就可以畅快执行了:


![4.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e952a45e4060475485df1f04afe5bffa~tplv-k3u1fbpfcp-zoom-1.image "4.png")

![5.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98c8fe0c88a54084924bdf1c4bb1cc5c~tplv-k3u1fbpfcp-zoom-1.image "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》系列下回分解吧。


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69953029/viewspace-2937930/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
阿里云云原生公众号(ID:Alicloudnative)发布云原生技术最新资讯、汇集云原生技术最全内容,定期举办云原生活动、直播,阿里产品及用户最佳实践发布。与你并肩探索云原生技术点滴,分享你需要的云原生内容。

注册时间:2019-11-07

  • 博文量
    1497
  • 访问量
    710961