实操案例
排查函数调用异常
通过curl可以看到异常,但是可以看到具体的请求参数和信息。
shell@Alicloud:~$ curl http://localhost:61000/user/0 {"timestamp":1655435063042,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"id < 1","path":"/user/0"}
查看UserController的参数/异常
在Arthas里执行:
watch com.example.demo.arthas.user.UserController * '{params, throwExp}'
- 第一个参数是类名,支持通配
- 第二个参数是函数名,支持通配访问
curl http://localhost:61000/user/0
,watch命令会打印调用的参数和异常
再次通过curl调用可以在arthas里面查看到具体的异常信息。
获取到的结果展开,可以用-x参数:
watch com.example.demo.arthas.user.UserController * '{params, throwExp}' -x 2
返回值表达式
在上面的例子里,第三个参数是返回值表达式,它实际上是一个表达式,它支持一些组合对象:
- 装载机
- 克拉兹
- 方法
- 目标
- 参数
- 返回对象
- throwExp
- 之前
- 是抛出
- 是返回
返回目录:
watch com.example.demo.arthas.user.UserController * '{params[0], target, returnObj}'
表达式条件
看命令支持在第4个参数里写条件表达式,比如:
- 当访问
user/1
时,观察命令没有输出 - 当访问
user/101
时,手表会打印出结果。
当异常时姓名
观看命令支持-e选项,表示只发射异常时的请求:
watch com.example.demo.arthas.user.UserController * "{params[0],throwExp}" -e
自动进行过滤
观看命令支持按请求进行过滤,例如:
watch com.example.demo.arthas.user.UserController * '{params, returnObj}' '#cost>200'
热更新代码
这个人也是真的秀。
访问http://localhost:61000/user/0
,会返回500个异常:
shell@Alicloud:~$ curl http://localhost:61000/user/0 {"timestamp":1655436218020,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"id < 1","path":"/user/0"}
通过热更新代码,修改这个逻辑。
jad反编译UserController
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
jad反编译的结果保存在/tmp/UserController.java
文件里了。
再打开一个终端窗口,然后用vim来编辑/tmp/UserController.java
:
vim /tmp/UserController.java
比如说当user id
小于1时,也正常返回,不抛出异常:
@GetMapping(value={"/user/{id}"}) public User findUserById(@PathVariable Integer id) { logger.info("id: {}", (Object)id); if (id != null && id < 1) { return new User(id, "name" + id); // throw new IllegalArgumentException("id < 1"); } return new User(id.intValue(), "name" + id); }
sc查找加载UserController的ClassLoader
[arthas@1266]$ sc -d *UserController | grep classLoaderHash classLoaderHash 19469ea2
classLoaderHash 是 19469ea2,后面需要使用它。
麦克
保存好/tmp/UserController.java
之后,使用mc(Memory Compiler)命令来编译,并通过-c或–classLoaderClass
参数指定ClassLoader
:
mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserController.java -d /tmp [arthas@1266]$ mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserController.java -d /tmp Memory compiler output: /tmp/com/example/demo/arthas/user/UserController.class Affect(row-cnt:1) cost in 2879 ms.
也可以通过mc -c /tmp/UserController.java -d /tmp
,使用 -c 参数指定ClassLoaderHash
:
mc -c 19469ea2 /tmp/UserController.java -d /tmp
重新定义
再使用redefine命令重新加载新编译好的UserController.class
:
[arthas@1266]$ redefine /tmp/com/example/demo/arthas/user/UserController.class redefine success, size: 1, classes: com.example.demo.arthas.user.UserController
热修改代码结果
重新定义成功之后,再次访问user/0
,结果正常
shell@Alicloud:~$ curl http://localhost:61000/user/0 {"id":0,"name":"name0"}
动态更新应用Logger Level
查找UserController的ClassLoader
[arthas@1266]$ sc -d *UserController | grep classLoaderHash classLoaderHash 19469ea2
用ognl获取logger
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader ‘@com.example.demo.arthas.user.UserController@logger’ [arthas@1266]$ ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger' @Logger[ serialVersionUID=@Long[5454405123156820674], FQCN=@String[ch.qos.logback.classic.Logger], name=@String[com.example.demo.arthas.user.UserController], level=null, effectiveLevelInt=@Integer[20000], parent=@Logger[Logger[com.example.demo.arthas.user]], childrenList=null, aai=null, additive=@Boolean[true], loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]], ]
可以知道UserController@logger
实际使用的是logback。可以看到level=null
,则说明实际最终的level是从root logger里来的。
单独设置UserController的logger级别
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.demo.arthas.user.UserController@logger.setLevel(@ch.qos.logback.classic.Level@DEBUG)'
再次获取UserController@logger
,可以发现已经是DEBUG了。
修改logback的记录器级别
通过获取root logger
,可以修改的logger level
:
ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)'
获取Spring Context
,在获取bean,再调用函数
使用tt命令获取到spring上下文
tt即TimeTunnel它可以记录下指定的方法,每次调用的入能和返回信息,并在下调用的时间对不同的进行调用。
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
访问用户/1:
curl http://localhost:61000/user/1
可以看到父母到了一个请求:
输入 q 或者 Ctrl + C 退出的 tt -t 命令。
使用tt命令从调用记录里获取到spring上下文
tt -i 1000 -w 'target.getApplicationContext()'
获取spring bean,并调用函数
tt -i 1000 -w ‘target.getApplicationContext().getBean(“helloWorldService”).getHelloMessage()’
结果如下:
[arthas@1266]$ tt -i 1000 -w 'target.getApplicationContext().getBean("helloWorldService").getHelloMessage()' @String[Hello World] Affect(row-cnt:1) cost in 1 ms.
排查HTTP请求返回401
请求接口没有权限的时候一般就返回401 Unauthorized。
401是被权限管理的过滤器拦截了,到底是哪个过滤器处理了这个请求,返回401?
监视所有的过滤功能
开始追踪:
trace javax.servlet.Filter *
可以在调用树的最全集,找到AdminFilterConfig$AdminFilter
返回的401
+---[3.806273ms] javax.servlet.FilterChain:doFilter() | `---[3.447472ms] com.example.demo.arthas.AdminFilterConfig$AdminFilter:doFilter() | `---[0.17259ms] javax.servlet.http.HttpServletResponse:sendError()
通过栈调用栈
上面是通过trace命令来获取信息,从里,我们可以知道通过stack跟踪HttpServletResponse:sendError()
结果,同样可以知道返回的是哪个Filter了401
执行:
stack javax.servlet.http.HttpServletResponse sendError 'params[0]==401'
可以查看以下信息:
寻找顶部 N 线程
查看所有地址
thread
查看具体的线程栈
查看线程ID 2的栈:
thread 2
查看CPU使用率最高的线程的栈
thread -n 3
查看5秒内的CPU使用率顶部线程栈
thread -n 3 -i 5000
寻找是否有拒绝
thread -b
更多使用查看: