天天看點

Java 診斷工具 Arthas-實操案例

實操案例

排查函數調用異常

通過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}'
           
  1. 第一個參數是類名,支援通配
  2. 第二個參數是函數名,支援通配 通路 curl http://localhost:61000/user/0 ,watch指令會列印調用的參數和異常

再次通過curl 調用可以在arthas裡面檢視到具體的異常資訊。

Java 診斷工具 Arthas-實操案例

把擷取到的結果展開,可以用-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個參數裡寫條件表達式,比如:

當通路 user/1 時,watch指令沒有輸出

當通路 user/101 時,watch會列印出結果。

Java 診斷工具 Arthas-實操案例

當異常時捕獲

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'
           

熱更新代碼

這個也是真的秀。

Java 診斷工具 Arthas-實操案例

通路 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檔案裡了。

再打開一個Terminal 視窗,然後用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,後面需要使用它。

mc

儲存好/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 <classLoaderHash> /tmp/UserController.java -d /tmp,使用-c參數指定ClassLoaderHash:

mc -c 19469ea2 /tmp/UserController.java -d /tmp
           

redefine

再使用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
           

熱修改代碼結果

redefine成功之後,再次通路 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是從r

oot logger

裡來的。

單獨設定UserController的logger level

ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@[email protected](@ch.qos.logback.classic.Level@DEBUG)'
           

再次擷取UserController@logger,可以發現已經是DEBUG了。

修改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)'
           

擷取Spring Context,在擷取 bean,再調用函數

Java 診斷工具 Arthas-實操案例

使用tt指令擷取到spring context

tt即 TimeTunnel,它可以記錄下指定方法每次調用的入參和傳回資訊,并能對這些不同的時間下調用進行觀測。官方tt說明:https://arthas.aliyun.com/doc/tt.html

tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
           

通路user/1,

curl http://localhost:61000/user/1
           

可以看到tt指令捕獲到了一個請求:

Java 診斷工具 Arthas-實操案例

輸入

q

或者

Ctrl + C

退出上面的

tt -t

指令。

使用tt指令從調用記錄裡擷取到spring context

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通常是被權限管理的Filter攔截了,那麼到底是哪個Filter處理了這個請求,傳回了401?

跟蹤所有的Filter函數

開始trace:

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()
           

通過stack擷取調用棧

上面是通過trace指令來擷取資訊,從結果裡,我們可以知道通過stack跟蹤HttpServletResponse:sendError(),同樣可以知道是哪個Filter傳回了401

執行:

stack javax.servlet.http.HttpServletResponse sendError 'params[0]==401'
           

通路可以看到如下堆棧資訊:

Java 診斷工具 Arthas-實操案例

查找Top N線程

檢視所有線程資訊

thread
           

檢視具體線程的棧

檢視線程ID 2的棧:

thread 2
           

檢視CPU使用率top n線程的棧

thread -n 3
           

檢視5秒内的CPU使用率top n線程棧

thread -n 3 -i 5000
           

查找線程是否有阻塞

thread -b
           

更多使用檢視:

Github位址: https://github.com/alibaba/arthas

文檔位址: https://arthas.aliyun.com/doc/