Arthas 进阶教程
1. 创建资源
开始实验之前,您需要先创建实验相关资源。
- 在实验室页面,单击创建资源。
- (可选)在实验室页面左侧导航栏中,单击云产品资源列表,可查看本次实验资源相关信息(例如IP地址、子用户信息等)。
说明:资源创建过程需要3~5分钟(视资源不同开通时间有所差异,ACK等资源开通时间较长)。完成实验资源的创建后,您可以在云产品资源列表查看已创建的资源信息,例如:子用户名称、子用户密码、AK ID、AK Secret、资源中的项目名称等。
实验环境一旦开始创建则进入计时阶段,建议学员先基本了解实验具体的步骤、目的,真正开始做实验时再进行创建。
资源创建成功,可在左侧的资源卡片中查看相关资源信息以及RAM子账号信息
2. 启动demo
- 执行如下命令,下载demo-arthas-spring-boot.jar,再用java -jar命令启动。
wget https://code.aliyun.com/middleware-container/handsonLabExternedFiles/raw/master/demo-arthas-spring-boot.jar java -jar demo-arthas-spring-boot.jar
demo-arthas-spring-boot是一个很简单的spring boot应用,源代码:查看
- 点击页面右上角 号,新建终端。
- 启动之后,可以访问61000端口查看。
curl http://127.0.0.1:61000
3. 启动arthas-boot
- 执行如下命令,在新的Terminal 2里,下载arthas-boot.jar,再用java -jar命令启动。
wget https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar --target-ip 0.0.0.0
arthas-boot是Arthas的启动程序,它启动后,会列出所有的Java进程,用户可以选择需要诊断的目标进程。
- 选择第一个进程,输入 1 ,再Enter/回车。
1
- Attach成功之后,会打印Arthas LOGO。输入 help 可以获取到更多的帮助信息。
help
4. 查看JVM信息
下面介绍Arthas里查看JVM信息的命令。
- sysprop。
- 执行如下命令, 可以打印所有的System Properties信息。
sysprop
- 执行如下命令,可以指定单个key。
sysprop java.version
- 执行如下命令,可以通过grep来过滤。
sysprop | grep user
- 执行如下命令,可以设置新的value。
sysprop testKey testValue
- sysenv。
- 执行如下命令,可以获取到环境变量。该命令和sysprop命令类似。
sysenv
- jvm。
- 执行如下命令,会打印出JVM的各种详细信息。
jvm
- dashboard。
- 执行如下命令,可以查看当前系统的实时数据面板。
dashboard
- 输入 q 或者 Ctrl+C 可以退出dashboard命令。
q
5. 使用技巧
为了更好使用Arthas,下面先介绍Arthas里的一些使用技巧。
- help。
Arthas里每一个命令都有详细的帮助信息。可以用-h来查看。帮助信息里有EXAMPLES和WIKI链接。比如:
sysprop -h
- 自动补全。
Arthas支持丰富的自动补全功能,在使用有疑惑时,可以输入Tab来获取更多信息。
比如输入 sysprop java. 之后,再输入Tab,会补全出对应的key:
- readline的快捷键支持。
Arthas支持常见的命令行快捷键,比如Ctrl + A跳转行首,Ctrl + E跳转行尾。
执行如下命令,可查看更多的快捷键。
keymap
- 历史命令的补全。
如果想再执行之前的命令,可以在输入一半时,按Up/↑ 或者 Ddown/↓,来匹配到之前的命令。
比如之前执行过sysprop java.version,那么在输入sysprop ja之后,可以输入Up/↑,就会自动补全为sysprop java.version。
执行如下命令,可查看所有的历史命。
history
- pipeline。
Arthas支持在pipeline之后,执行一些简单的命令,比如。
sysprop | grep java sysprop | wc -l
6. sc/sm 查看已加载的类
本步骤指导您如何Arthas里查找已加载类的命令。
- sc。
sc 命令可以查找到所有JVM已经加载到的类。 如果搜索的是接口,还会搜索所有的实现类。比如:
- 执行如下命令,查看所有的Filter实现类。
sc javax.servlet.Filter
- 执行如下命令,通过-d参数,可以打印出类加载的具体信息,很方便查找类加载问题。
sc -d javax.servlet.Filter
- sc支持通配,比如搜索所有的StringUtils。
sc *StringUtils
- sm。
- sm命令则是查找类的具体函数。比如:
sm java.math.RoundingMode
- 执行如下命令,通过-d参数可以打印函数的具体属性。
sm -d java.math.RoundingMode
- 执行如下命令,可以查找特定的函数,比如查找构造函数。
sm java.math.RoundingMode <init>
7. Jad命令
- 执行如下命令,可以通过 jad 命令来反编译代码。
jad com.example.demo.arthas.user.UserController
- 执行如下命令,通过--source-only参数可以只打印出在反编译的源代码。
jad --source-only com.example.demo.arthas.user.UserController
8. Ognl命令
在Arthas里,有一个单独的ognl命令,可以动态执行代码。
- 执行如下命令,调用static函数。
ognl '@java.lang.System@out.println("hello ognl")'
可以检查Terminal 1里的进程输出,可以发现打印出了hello ognl。
- 执行如下命令,查找UserController的ClassLoader。
sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
- 注意hashcode是变化的,需要先查看当前的ClassLoader信息,提取对应ClassLoader的hashcode。如果你使用-c,你需要手动输入hashcode:-c <hashcode>。
ognl -c 5674cd4d @com.example.demo.arthas.user.UserController@logger
--classLoaderClass 的值是ClassLoader的类名,只有匹配到唯一的ClassLoader实例时才能工作,目的是方便输入通用命令,而-c <hashcode>是动态变化的。
- 获取静态类的静态字段。
- 获取UserController类里的logger字段。
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader @com.example.demo.arthas.user.UserController@logger
- 执行如下命令,通过-x参数控制返回值的展开层数。
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -x 2 @com.example.demo.arthas.user.UserController@logger
- 执行多行表达式,赋值给临时变量,返回一个List。
ognl '#value1=@System@getProperty("java.home"), #value2=@System@getProperty("java.runtime.name"), {#value1, #value2}'
1. 案例:排查函数调用异常
- 点击页面右上角 号,新建终端。
- 目前,访问如下链接 ,会返回500异常。
curl http://localhost:61000/user/0
- 执行如下命令,查看UserController的 参数/异常。
说明:该命令需在Arthas里执行。
watch com.example.demo.arthas.user.UserController * '{params, throwExp}'
- 第一个参数是类名,支持通配。
- 第二个参数是函数名,支持通配访问该链接,watch命令会打印调用的参数和异常。
在Terminal 3中执行该命令。
curl http://localhost:61000/user/0
运行结果Terminal 2中显示如下。
可以看到实际抛出的异常是IllegalArgumentException。
- 可以输入 q 或者 Ctrl+C 退出watch命令。
q
- 如果想把获取到的结果展开,可以用-x参数。
watch com.example.demo.arthas.user.UserController * '{params, throwExp}' -x 2
- 返回值表达式。
在上面的例子里,第三个参数是返回值表达式,它实际上是一个ognl表达式,它支持一些内置对象。
- loader
- clazz
- method
- target
- params
- returnObj
- throwExp
- isBefore
- isThrow
- isReturn
- 执行如下命令,可以利用这些内置对象来组成不同的表达式。比如返回一个数组。
watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}'
- 条件表达式。
- watch命令支持在第4个参数里写条件表达式,比如。
watch com.example.demo.arthas.user.UserController * returnObj 'params[0] > 100'
- 执行如下命令时,watch命令没有输出。
curl http://127.0.0.1:61000/user/1
- 执行如下命令时,watch命令会打印出结果。
curl http://127.0.0.1:61000/user/101
- 当异常时捕获,watch命令支持-e选项,表示只捕获抛出异常时的请求。
watch com.example.demo.arthas.user.UserController * "{params[0],throwExp}" -e
- 按照耗时进行过滤,watch命令支持按请求耗时进行过滤,比如。
watch com.example.demo.arthas.user.UserController * '{params, returnObj}' '#cost>200'
2. 案例: 热更新代码
下面介绍通过jad/mc/redefine 命令实现动态更新代码的功能。
- 目前,访问 http://localhost:61000/user/0 ,会返回500异常。
curl http://localhost:61000/user/0
下面通过热更新代码,修改这个逻辑。
- jad反编译UserController。
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
jad反编译的结果保存在 /tmp/UserController.java文件里了。
在Terminal 3,然后用vim来编辑/tmp/UserController.java。
vim /tmp/UserController.java
比如当 user id 小于1时,也正常返回,不抛出异常。
- 在Terminal 2中,执行如下命令,sc查找加载UserController的ClassLoader。
sc -d *UserController | grep classLoaderHash
执行结果如下。
可以发现是 spring boot LaunchedURLClassLoader@1be6f5c3 加载的。
请记下你的classLoaderHash,后面需要使用它。在这里,它是 5674cd4d。
- mc。
保存好/tmp/UserController.java之后,使用mc(Memory Compiler)命令来编译,并且通过-c或者--classLoaderClass参数指定ClassLoader。
mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserController.java -d /tmp
执行结果如下。
- redefine。
再使用redefine命令重新加载新编译好的UserController.class。
redefine /tmp/com/example/demo/arthas/user/UserController.class
执行结果如下。
- 热修改代码结果。
redefine成功之后,再次访问 user/0 ,结果是如下。
curl http://localhost:61000/user/0
3. 案例:动态更新应用Logger Level
在这个案例里,动态修改应用的Logger Level。
- 查找UserController的ClassLoader。
sc -d com.example.demo.arthas.user.UserController | grep classLoaderHash
执行结果如下。
- 用ognl获取logger。
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger'
可以知道UserController@logger实际使用的是logback。可以看到level=null,则说明实际最终的level是从root logger里来的。
- 单独设置UserController的logger level。
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'
再次获取UserController@logger,可以发现已经是DEBUG了:
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger'
- 修改logback的全局logger level。
通过获取root logger,可以修改全局的logger level。
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
4. 案例:排查logger冲突问题
在这个案例里,展示排查logger冲突的方法。
- 确认应用使用的logger系统。
以UserController为例,它使用的是slf4j api,但实际使用到的logger系统是logback。
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger'
执行结果如下。
- 获取logback实际加载的配置文件。
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '#map1=@org.slf4j.LoggerFactory@getLogger("root").loggerContext.objectMap, #map1.get("CONFIGURATION_WATCH_LIST")'
- 使用classloader命令查找可能存在的logger配置文件。
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r logback-spring.xml
执行结果如下。
可以知道加载的配置的具体来源。
- 执行如下命令,可以尝试加载容易冲突的文件。
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r logback.xml
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader -r log4j.properties
5. 案例:获取Spring Context
在这个案例里,展示获取spring context,再获取bean,然后调用函数。
- 在Terminal 2使用tt命令获取到spring context。
tt即 TimeTunnel,它可以记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
执行如下命令,在Terminal 3访问:user/1。
curl http://localhost:61000/user/1
在Terminal 2可以看到tt命令捕获到了一个请求:
- 使用tt命令从调用记录里获取到spring context。
输入 q 或者 Ctrl + C 退出上面的 tt -t命令。
q
tt -i 1000 -w 'target.getApplicationContext()'
执行结果如下。
- 执行如下命令,获取spring bean,并调用函数。
tt -i 1000 -w 'target.getApplicationContext().getBean("helloWorldService").getHelloMessage()'
执行结果如下。
6. 案例:排查HTTP请求返回401
在这个案例里,展示排查HTTP 401问题的技巧。
- 执行如下命令,在Terminal 3中访问admin。
curl http://127.0.0.1:61000/admin
访问结果如下。
我们知道401通常是被权限管理的Filter拦截了,那么到底是哪个Filter处理了这个请求,返回了401?
- 跟踪所有的Filter函数。
- 执行如下命令,开始trace。
trace javax.servlet.Filter *
- 执行如下命令,访问admin。
curl http://127.0.0.1:61000/admin
可以在调用树的最深层,找到AdminFilterConfig$AdminFilter返回了401:
- 通过stack获取调用栈。
- 上面是通过trace命令来获取信息,从结果里,我们可以知道通过stack跟踪HttpServletResponse:sendError(),同样可以知道是哪个Filter返回了401。
stack javax.servlet.http.HttpServletResponse sendError 'params[0]==401'
- 执行如下命令,访问admin。
curl http://127.0.0.1:61000/admin
- 可在Terminal 2中看到反馈如下。
7. 案例:排查HTTP请求返回404
在这个案例里,展示排查HTTP 404问题的技巧。
- 在Terminal 3中,执行如下命令访问a.txt。
curl http://127.0.0.1:61000/a.txt
执行结果如下。
那么到底是哪个Servlet处理了这个请求,返回了404?
- 跟踪所有的Servlet函数。
- 执行如下命令,开始trace。
trace javax.servlet.Servlet * > /tmp/servlet.txt
- 执行如下命令,访问a.txt。
curl http://127.0.0.1:61000/a.txt
- 在Terminal 3里,查看/tmp/servlet.txt的内容:
cat /tmp/servlet.txt
/tmp/servlet.txt里的内容会比较多,需要耐心找到调用树里最长的地方。
可以发现请求最终是被freemarker处理的:
8. 案例:理解Spring Boot应用的ClassLoader结构
下面介绍classloader命令的功能。
- 在Terminal 3里,执行如下命令,访问一个jsp网页,触发jsp的加载。
curl http://127.0.0.1:61000/hello
- 在Terminal 2里,列出所有ClassLoader。
classloader -l
执行结果如下。
- TomcatEmbeddedWebappClassLoader 加载的class数量是0,所以在spring boot embedded tomcat里,它只是一个空壳,所有的类加载都是LaunchedURLClassLoader完成的。
- 列出ClassLoader里加载的所有类。
执行如下命令,列出上面的org.apache.jasper.servlet.JasperLoader加载的类。
classloader -a -c 105d147a
执行结果如下。
- 注:同ognl, 也可用-c <hashcode>而不用--classLoaderClass指定。
- 执行如下命令,反编译jsp的代码。
jad org.apache.jsp.jsp.hello_jsp
执行结果如下。
- 执行如下命令,查看ClassLoader树。
classloader -t
执行结果如下。
- 注意:请使用你的classLoaderHash值覆盖 <classLoaderHash> ,然后手动执行下面相关命令。
- 列出ClassLoader的urls。
比如上面查看到的spring LaunchedURLClassLoader的 hashcode是1be6f5c3,可以通过-c或者--classLoaderClass参数来列出它的所有urls。
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader
执行结果如下。
- 尝试加载指定的类。
比如用上面的spring LaunchedURLClassLoader 尝试加载 java.lang.String 。
classloader --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --load java.lang.String
执行结果如下。
9. 案例:查找Top N线程
- 查看所有线程信息。
thread
- 查看具体线程的栈。
- 查看线程ID 16的栈。
thread 16
- 查看CPU使用率top n线程的栈。
thread -n 3
- 查看5秒内的CPU使用率top n线程栈。
thread -n 3 -i 5000
- 查找线程是否有阻塞。
thread -b
1. Web Console
Arthas支持通过Web Socket来连接。
本地体验。
当在本地启动时,可以访问 http://127.0.0.1:8563/ ,通过浏览器来使用Arthas。
推荐通过“快速入门”来体验: https://arthas.aliyun.com/doc/quick-start.html。
2. Exit/Stop
- reset。
Arthas在 watch/trace 等命令时,实际上是修改了应用的字节码,插入增强的代码。显式执行 reset 命令,可以清除掉这些增强代码。
reset
- 退出Arthas。
- 用 exit 或者 quit 命令可以退出Arthas。
exit
- 退出Arthas之后,还可以再次用 java -jar arthas-boot.jar 来连接。
java -jar arthas-boot.jar
- 彻底退出Arthas。
exit/quit命令只是退出当前session,arthas server还在目标进程中运行。
- 想完全退出Arthas,可以执行 stop 命令。
stop
3. arthas-boot支持的参数
说明:以下命令需在Terminal 3中执行。
- arthas-boot.jar 支持很多参数,可以执行 java -jar arthas-boot.jar -h 来查看。
java -jar arthas-boot.jar -h
- 允许外部访问。
默认情况下, arthas server侦听的是 127.0.0.1 这个IP,如果希望远程可以访问,可以使用--target-ip的参数。
java -jar arthas-boot.jar --target-ip
- 列出所有的版本。
java -jar arthas-boot.jar --versions
使用指定版本需执行如下命令。
java -jar arthas-boot.jar --use-version 3.1.0
- 只侦听Telnet端口,不侦听HTTP端口。
java -jar arthas-boot.jar --telnet-port 9999 --http-port -1
- 打印运行的详情。
java -jar arthas-boot.jar -v
实验地址:https://developer.aliyun.com/adc/scenario/b15a41fe955a43cb9ba3a503ae488621