當Dubbo遇上Arthas:排查問題的實踐
Apache Dubbo是Alibaba開源的高效能RPC框架,在國內有非常多的使用者。
- Github:ofollow,noindex">https://github.com/apache/incubator-dubbo
- 文件:http://dubbo.incubator.apache.org/zh-cn/
Arthas是Alibaba開源的應用診斷利器,9月份開源以來,Github Star數三個月超過6000。
- Github:https://github.com/alibaba/arthas
- 文件:https://alibaba.github.io/arthas/
- Arthas開源交流QQ群: 916328269
- Arthas開源交流釘釘群: 21965291
當Dubbo遇上Arthas,會碰撞出什麼樣的火花呢?下面來分享Arthas排查Dubbo問題的一些經驗。
dubbo-arthas-demo
下面的排查分享基於這個dubbo-arthas-demo
,非常簡單的一個應用,瀏覽器請求從Spring MVC到Dubbo Client,再發送到Dubbo Server。
Demo裡有兩個spring boot應用,可以先啟動server-demo
,再啟動client-demo
。
/user/{id}->UserService->UserServiceImpl BrowserDubbo ClientDubbo Server
Client端:
Server端:
@Service(version = "1.0.0") public class UserServiceImpl implements UserService { @Override public User findUser(int id) { if (id < 1) { throw new IllegalArgumentException("user id < 1, id: " + id); } for (User user : users) { if (user.getId() == id) { return user; } } throw new RuntimeException("Can not find user, id: " + id); }
Arthas快速開始
$ wget https://alibaba.github.io/arthas/arthas-boot.jar $ java -jar arthas-boot.jar
啟動後,會列出所有的java程序,選擇1,然後回車,就會連線上ServerDemoApplication
$ java -jar arthas-boot.jar * [1]: 43523 ServerDemoApplication [2]: 22342 [3]: 44108 ClientDemoApplication 1 [INFO] arthas home: /Users/hengyunabc/.arthas/lib/3.0.5/arthas [INFO] Try to attach process 43523 [INFO] Attach process 43523 success. [INFO] arthas-client connect 127.0.0.1 3658 ,---.,------. ,--------.,--.,--.,---.,---. /O\ |.--. ''--..--'|'--'| /O\ '.-' |.-.||'--'.'|||.--.||.-.|`.`-. || ||||\\|||||||| ||.-'| `--' `--'`--' '--'`--'`--'`--'`--' `--'`-----' wiki: https://alibaba.github.io/arthas version: 3.0.5 pid: 43523 time: 2018-12-05 16:23:52 $
Dubbo線上服務丟擲異常,怎麼獲取呼叫引數?
當線上服務丟擲異常時,最著急的是什麼引數導致了拋異常?
在demo裡,訪問http://localhost:8080/user/0
,UserServiceImpl
就會丟擲一個異常,因為user id不合法。
在Arthas裡執行watch com.example.UserService * -e -x 2 '{params,throwExp}'
,然後再次訪問,就可以看到watch命令把引數和異常都打印出來了。
$ watch com.example.UserService * -e -x 2 '{params,throwExp}' Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:4) cost in 230 ms. ts=2018-12-05 16:26:44; [cost=3.905523ms] result=@ArrayList[ @Object[][ @Integer[0], ], java.lang.IllegalArgumentException: user id < 1, id: 0 at com.example.UserServiceImpl.findUser(UserServiceImpl.java:24) at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java) at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:45) at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:71) at com.alibaba.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:48) at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:52) at com.alibaba.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:61)
怎樣線上除錯Dubbo服務程式碼?
在本地開發時,可能會用到熱部署工具,直接改程式碼,不需要重啟應用。但是在線上環境,有沒有辦法直接動態除錯程式碼?比如增加日誌。
在Arthas裡,可以通過redefine
命令來達到線上不重啟,動態更新程式碼的效果。
比如我們修改下UserServiceImpl
,用System.out
打印出具體的User
物件來:
public User findUser(int id) { if (id < 1) { throw new IllegalArgumentException("user id < 1, id: " + id); } for (User user : users) { if (user.getId() == id) { System.out.println(user); return user; } } throw new RuntimeException("Can not find user, id: " + id); }
本地編繹後,把server-demo/target/classes/com/example/UserServiceImpl.class
傳到線上伺服器,然後用redefine
命令來更新程式碼:
$ redefine -p /tmp/UserServiceImpl.class redefine success, size: 1
這樣子更新成功之後,訪問http://localhost:8080/user/1
,在ServerDemoApplication
的控制檯裡就可以看到打印出了user資訊。
怎樣動態修改Dubbo的logger級別?
- https://alibaba.github.io/arthas/ognl.html
- https://alibaba.github.io/arthas/sc.html
- https://commons.apache.org/proper/commons-ognl/language-guide.html
在排查問題時,需要檢視到更多的資訊,如果可以把logger級別修改為DEBUG
,就非常有幫助。
ognl
是apache開源的一個輕量級表示式引擎。下面通過Arthas裡的ognl
命令來動態修改logger級別。
首先獲取Dubbo裡TraceFilter
的一個logger物件,看下它的實現類,可以發現是log4j。
$ ognl '@com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter@logger.logger' @Log4jLogger[ FQCN=@String[com.alibaba.dubbo.common.logger.support.FailsafeLogger], logger=@Logger[org.apache.log4j.Logger@2f19bdcf], ]
再用sc
命令來檢視具體從哪個jar包里加載的:
$ sc -d org.apache.log4j.Logger class-infoorg.apache.log4j.Logger code-source/Users/hengyunabc/.m2/repository/org/slf4j/log4j-over-slf4j/1.7.25/log4j-over-slf4j-1.7.25.jar nameorg.apache.log4j.Logger isInterfacefalse isAnnotationfalse isEnumfalse isAnonymousClassfalse isArrayfalse isLocalClassfalse isMemberClassfalse isPrimitivefalse isSyntheticfalse simple-nameLogger modifierpublic annotation interfaces super-class+-org.apache.log4j.Category +-java.lang.Object class-loader+-sun.misc.Launcher$AppClassLoader@5c647e05 +-sun.misc.Launcher$ExtClassLoader@59878d35 classLoaderHash5c647e05 Affect(row-cnt:1) cost in 126 ms.
可以看到log4j是通過slf4j代理的。
那麼通過org.slf4j.LoggerFactory
獲取root
logger,再修改它的level:
$ ognl '@org.slf4j.LoggerFactory@getLogger("root").setLevel(@ch.qos.logback.classic.Level@DEBUG)' null $ ognl '@org.slf4j.LoggerFactory@getLogger("root").getLevel().toString()' @String[DEBUG]
可以看到修改之後,root
logger的level變為DEBUG
。
怎樣減少測試小姐姐重複發請求的麻煩?
在平時開發時,可能需要測試小姐姐發請求過來聯調,但是我們在debug時,可能不小心直接跳過去了。這樣子就尷尬了,需要測試小姐姐再發請求過來。
Arthas裡提供了tt
命令,可以減少這種麻煩,可以直接重放請求。
$ tt -t com.example.UserServiceImpl findUser Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 145 ms. INDEXTIMESTAMPCOST(ms)IS-RETIS-EXPOBJECTCLASSMETHOD ---------------------------------------------------------------------------------------------------------------- 10002018-12-05 17:47:521.56523truefalse0x3233483UserServiceImplfindUser 10012018-12-05 17:48:030.286176falsetrue0x3233483UserServiceImplfindUser 10022018-12-05 17:48:1190.324335truefalse0x3233483UserServiceImplfindUser
上面的tt -t
命令捕獲到了3個請求。然後通過tt --play
可以重放請求:
$ tt --play -i 1000 RE-INDEX1000 GMT-REPLAY2018-12-05 17:55:50 OBJECT0x3233483 CLASScom.example.UserServiceImpl METHODfindUser PARAMETERS[0]@Integer[1] IS-RETURNtrue IS-EXCEPTIONfalse RETURN-OBJ@User[ id=@Integer[1], name=@String[Deanna Borer], ] Time fragment[1000] successfully replayed. Affect(row-cnt:1) cost in 4 ms.
Dubbo執行時有哪些Filter? 耗時是多少?
Dubbo執行時會載入很多的Filter,那麼一個請求會經過哪些Filter處理,Filter裡的耗時又是多少呢?
通過Arthas的trace
命令,可以很方便地知道Filter的資訊,可以看到詳細的呼叫棧和耗時。
$ trace com.alibaba.dubbo.rpc.Filter * Press Ctrl+C to abort. Affect(class-cnt:19 , method-cnt:59) cost in 1441 ms. `---ts=2018-12-05 19:07:26;thread_name=DubboServerHandler-30.5.125.152:20880-thread-10;id=3e;is_daemon=true;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@5c647e05 `---[8.435844ms] com.alibaba.dubbo.rpc.filter.EchoFilter:invoke() +---[0.124572ms] com.alibaba.dubbo.rpc.Invocation:getMethodName() +---[0.065123ms] java.lang.String:equals() `---[7.762928ms] com.alibaba.dubbo.rpc.Invoker:invoke() `---[7.494124ms] com.alibaba.dubbo.rpc.filter.ClassLoaderFilter:invoke() +---[min=0.00355ms,max=0.049922ms,total=0.057637ms,count=3] java.lang.Thread:currentThread() +---[0.0126ms] java.lang.Thread:getContextClassLoader() +---[0.02188ms] com.alibaba.dubbo.rpc.Invoker:getInterface() +---[0.004115ms] java.lang.Class:getClassLoader() +---[min=0.003906ms,max=0.014058ms,total=0.017964ms,count=2] java.lang.Thread:setContextClassLoader() `---[7.033486ms] com.alibaba.dubbo.rpc.Invoker:invoke() `---[6.869488ms] com.alibaba.dubbo.rpc.filter.GenericFilter:invoke() +---[0.01481ms] com.alibaba.dubbo.rpc.Invocation:getMethodName()
Dubbo動態代理是怎樣實現的?
- https://alibaba.github.io/arthas/jad.html
- com.alibaba.dubbo.common.bytecode.Wrapper
通過Arthas的jad
命令,可以看到Dubbo通過javaassist動態生成的Wrappr類的程式碼:
$ jad com.alibaba.dubbo.common.bytecode.Wrapper1 ClassLoader: +-sun.misc.Launcher$AppClassLoader@5c647e05 +-sun.misc.Launcher$ExtClassLoader@59878d35 Location: /Users/hengyunabc/.m2/repository/com/alibaba/dubbo/2.5.10/dubbo-2.5.10.jar package com.alibaba.dubbo.common.bytecode; public class Wrapper1 extends Wrapper implements ClassGenerator.DC { public Object invokeMethod(Object object, String string, Class[] arrclass, Object[] arrobject) throws InvocationTargetException { UserServiceImpl userServiceImpl; try { userServiceImpl = (UserServiceImpl)object; } catch (Throwable throwable) { throw new IllegalArgumentException(throwable); } try { if ("findUser".equals(string) && arrclass.length == 1) { return userServiceImpl.findUser(((Number)arrobject[0]).intValue()); } if ("listUsers".equals(string) && arrclass.length == 0) { return userServiceImpl.listUsers(); } if ("findUserByName".equals(string) && arrclass.length == 1) { return userServiceImpl.findUserByName((String)arrobject[0]); } }
獲取Spring context
除了上面介紹的一些排查技巧,下面分享一個獲取Spring Context,然後“為所欲為”的例子。
在Dubbo裡有一個擴充套件com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory
,把Spring Context儲存到了裡面。
因此,我們可以通過ognl
命令獲取到。
$ ognl '#context=@com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory@contexts.iterator.next, #context.getBean("userServiceImpl").findUser(1)' @User[ id=@Integer[1], name=@String[Deanna Borer], ]
-
`[email protected]
獲取到
SpringExtensionFactory`裡儲存的spring context物件 -
#context.getBean("userServiceImpl").findUser(1)
獲取到userServiceImpl
再執行一次呼叫
只要充分發揮想像力,組合Arthas裡的各種命令,可以發揮出神奇的效果。