前言
当你兴冲冲地开始运行自己的 Java 项目时,你是否遇到过如下问题:
1.程序在稳定运行了,可是实现的功能点了没反应。
2.为了修复 Bug 而上线的新版本,上线后发现 Bug 依然在,却想不通哪里有问题?
3.想到可能出现问题的地方,却发现那里没打日志,没法在运行中看到问题,只能加了日志输出重新打包——部署——上线
4.程序功能正常了,可是为啥响应时间这么慢,在哪里出现了问题?
5.程序不但稳定运行,而且功能完美,但跑了几天或者几周过后,发现响应速度变慢了,是不是内存泄漏了?
以前,你碰到这些问题,解决的办法大多是,修改代码,重新上线。但是在目前公司里,上线的流程是非常繁琐的,如果为了多加一行日志而重新发布版本,无疑是非常折腾人的。
现在,我们有了更为优雅的线上调试方法 - 来自阿里巴巴开源的 Arthas。
Arthas是Alibaba开源的Java诊断工具,当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
1.这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
2.我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
3.遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
4.线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
5.是否有一个全局视角来查看系统的运行状况?
6.有什么办法可以监控到JVM的实时运行状态?
7.怎么快速定位应用的热点,生成火焰图?
在本篇文章中,你能够了解:
1.Arthas 使用实例:帮助你快速让你上手,拯救你的低效率 Debug
2.使用 Arthas 解决具体问题:看一下 Arthas 帮我拯救了多少时间
3.原理浅谈:莫在浮沙筑高阁!你需要大概了解下 Arthas 的原理
Arhas 绝对是你提升效率的利器,适合各种阶段的开发者!
线上 Debug 神器 Arthas
Arthas 使用实例
命令的详细文档请参考:alibaba.github.io/arthas/comm…
快速安装
你只需要两行命令:
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
启动arthas
在命令行下面执行(使用和目标进程一致的用户启动,否则可能attach失败):
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
基本使用
Arthas 有如下功能:
"上帝视角"指令:dashboard
当前系统的实时数据面板,按 ctrl+c 退出。
当运行在Ali-tomcat时,会显示当前tomcat的实时信息,如HTTP请求的qps, rt, 错误数, 线程池信息等等。
线程调试相关指令:thread
1. thread -n x :一键展示当前最忙的前X个线程并打印堆栈
2. thread id :显示指定线程的运行堆栈
3. thread -b :找出当前阻塞其他线程的线程
4.thread -i x :指定采样时间间隔
5.thread --state x :查看指定状态的线程
jvm监控相关指令:jvm sysprop sysenv vmoption
1.jvm: 查看当前JVM信息
THREAD相关
COUNT: JVM当前活跃的线程数
DAEMON-COUNT: JVM当前活跃的守护线程数
PEAK-COUNT: 从JVM启动开始曾经活着的最大线程数
STARTED-COUNT: 从JVM启动开始总共启动过的线程次数
DEADLOCK-COUNT: JVM当前死锁的线程数
2.sysprop: 查看当前JVM的系统属性
查看单个属性
sysprop java.version
修改单个属性
sysprop user.country CN
3.sysenv: 查看当前JVM的环境属性
查看单个环境变量
$ sysenv USER
USER=admin
4.vmoption: 查看,更新VM诊断相关的参数
查看指定的option
vmoption PrintGCDetails
更新指定的option
vmoption PrintGCDetailstrue
类加载问题相关指令: sc sm
1.sc: 查看JVM已加载的类信息
通过 SC 我们可以看到我们这个类的详细信息,包括是从哪个 jar 包读取的,他是不是接口/枚举类等,甚至包括他是从哪个类加载器加载的。
SC 也可以查看已加载的类,帮助你看是否有没有纳入进来的类,尤其是在 Spring 中,可以判断的你的依赖有没有正确的进来。
2.sm: 查看已加载类的方法信息
这个命令能搜索出所有已经加载了 Class 信息的方法信息。
sm 命令只能看到由当前类所声明 (declaring) 的方法,父类则无法看到。
方法运行相关指令:monitor watch trace stack
1.monitor:方法执行监控
monitor 命令是一个非实时返回命令.
实时返回命令是输入之后立即返回,而非实时返回的命令,则是不断的等待目标 Java 进程返回信息,直到用户输入 Ctrl+C 为止。
服务端是以任务的形式在后台跑任务,植入的代码随着任务的中止而不会被执行,所以任务关闭后,不会对原有性能产生太大影响,而且原则上,任何Arthas命令不会引起原有业务逻辑的改变。
计算条件表达式过滤统计结果(方法执行完毕之后):
monitor -c 5 demo.MathGame primeFactors "params[0] <= 2"
计算条件表达式过滤统计结果(方法执行完毕之前):
monitor -b -c 5 com.test.testes.MathGame primeFactors"params[0] <= 2"
2.watch:方法执行数据观测
你可以通过 watch 指令,来监控某个类,监控后,运行下你的功能,复现下场景,arthas 会提供给你具体的出参和入参,帮助你排查故障。
特别说明:
1.watch 命令定义了4个观察事件点,即 -b 方法调用前,-e 方法异常后,-s 方法返回后,-f 方法结束后
2.4个观察事件点 -b、-e、-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出
3.这里要注意方法入参和方法出参的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表方法入参外,其余事件都代表方法出参
4.当使用 -b 时,由于观察事件点是在方法调用前,此时返回值或异常均不存在
观察方法出参和返回值:
watch demo.MathGame primeFactors "{params,returnObj}" -x 2
观察方法入参:
watch demo.MathGame primeFactors "{params,returnObj}" -x 2 -b
同时观察方法调用前和方法返回后:
watch demo.MathGame primeFactors "{params,target,returnObj}" -x 2 -b -s -n 2
调整-x的值,观察具体的方法参数值:
watch demo.MathGame primeFactors "{params,target}" -x 3
条件表达式:
watch demo.MathGame primeFactors "{params[0],target}" "params[0]<0"
观察异常信息:
watch demo.MathGame primeFactors "{params[0],throwExp}" -e -x 2
按照耗时进行过滤:
watch demo.MathGame primeFactors '{params, returnObj}' '#cost>200' -x 2
3.trace: 方法内部调用路径,并输出方法路径上的每个节点上耗时
这个指令对于优化代码非常的有用,可以看出具体每个方法执行的时间,如果是 for 循环等重复语句,还能看出 n 次循环中的最大耗时,最小耗时,和平均耗时。
据调用耗时过滤:
trace demo.MathGame run '#cost > 10'
4.stack:输出当前方法被调用的调用路径
很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令。
据条件表达式来过滤:
stack demo.MathGame primeFactors 'params[0]<0' -n 2
据执行时间来过滤:
stack demo.MathGame primeFactors '#cost>5'
强大的 ognl 表达式:
众所周知,一般来说,表达式都是调试工具里最强的指令。
在 Arthas 中你可以利用 ognl 表达式语言做很多事,比如执行某个方法,获取某个信息,甚至进行修改。
使用 Arthas 解决具体问题
原理浅谈
启动:
使用了阿里开源的组件 cli,对参数进行了解析:com.taobao.arthas.boot.Bootstrap
在传入参数中没有 pid,则会调用本地 jps 命令,列出 java 进程。
进入主逻辑,会在用户目录下建立 .arthas 目录,同时下载 arthas-core 和 arthas-agent 等 lib 文件,最后启动客户端和服务端。
通过反射的方式来启动字符客户端。
服务端——前置准备
看服务端启动命令可以知道 从 arthas-core.jar开始启动,arthas-core 的 pom.xml 文件里面指定了 mainClass 为 com.taobao.arthas.core.Arthas,使得程序启动的时候从该类的 main 方法开始运行。
首先解析入参,生成 com.taobao.arthas.core.config.Configure 类,包含了相关配置信息;
使用 jdk-tools 里面的 VirtualMachine.loadAgent,其中第一个参数为 agent 路径, 第二个参数向 jar 包中的 agentmain() 方法传递参数(此处为 agent-core.jar 包路径和 config 序列化之后的字符串),加载 arthas-agent.jar 包;
运行 arthas-agent.jar 包,指定了 Agent-Class为com.taobao.arthas.agent.AgentBootstrap。
服务端——监听客户端请求
如果是 exit,logout,quit,jobs,fg,bg,kill 等直接执行;
如果是其他的命令,则创建 Job,并运行;
创建 Job 时,会根据具体客户端传递的命令,找到对应的 Command,并包装成 Process, Process 再被包装成 Job;
运行 Job 时,反向先调用 Process,再找到对应的 Command,最终调用 Command 的 process 处理请求。
服务端——Command 处理流程
不需要使用字节码增强的命令
其中 JVM 相关的使用 java.lang.management 提供的管理接口,来查看具体的运行时数据。
需要使用字节码增强的命令
字节码增加的命令统一继承 EnhancerCommand 类,process 方法里面调用 enhance 方法进行增强。调用 Enhancer 类 enhance 方法,该方法内部调用 inst.addTransformer 方法添加自定义的 ClassFileTransformer,这边是 Enhancer 类。
Enhancer 类使用 AdviceWeaver(继承 ClassVisitor),用来修改类的字节码。重写了 visitMethod 方法,在该方法里面修改类指定的方法。visitMethod 方法里面使用了 AdviceAdapter(继承了 MethodVisitor类),在 onMethodEnter 方法, onMethodExit 方法中,把 Spy 类对应的方法(ON_BEFORE_METHOD, ON_RETURN_METHOD, ON_THROWS_METHOD 等)编织到目标类的方法对应的位置。
在前面 Spy 初始化的时候可以看到,这几个方法其实指向的是 AdviceWeaver 类的 methodOnBegin, methodOnReturnEnd 等。在这些方法里面都会根据 adviceId 查找对应的 AdviceListener,并调用 AdviceListener 的对应的方法,比如 before,afterReturning, afterThrowing。
客户端
客户端代码在 arthas-client 模块里面,入口类是 com.taobao.arthas.client.TelnetConsole。
主要使用 apache commons-net jar 进行 telnet 连接,关键的代码有下面几步:
构造 TelnetClient 对象,并初始化
构造 ConsoleReader 对象,并初始化
调用 IOUtil.readWrite(telnet.getInputStream(), telnet.getOutputStream(), System.in, consoleReader.getOutput()) 处理各个流,一共有四个流:
telnet.getInputStream()
telnet.getOutputStream()
System.in
consoleReader.getOutput()
请求时:从本地 System.in 读取,发送到 telnet.getOutputStream(),即发送给远程服务端。 响应时:从 telnet.getInputStream() 读取远程服务端发送过来的响应,并传递给 consoleReader.getOutput(),即在本地控制台输出。
参考文献
开源地址:
https://github.com/alibaba/arthas
官方文档:
https://arthas.aliyun.com/doc/
文章借鉴:
https://www.cnblogs.com/alisystemsoftware/p/13107988.html