Apache Dubbo是Alibaba開源的高性能RPC框架,在國內有很是多的用戶。html
Arthas是Alibaba開源的應用診斷利器,9月份開源以來,Github Star數三個月超過6000。java
當Dubbo趕上Arthas,會碰撞出什麼樣的火花呢?下面來分享Arthas排查Dubbo問題的一些經驗。git
下面的排查分享基於這個dubbo-arthas-demo
,很是簡單的一個應用,瀏覽器請求從Spring MVC到Dubbo Client,再發送到Dubbo Server。github
Demo裏有兩個spring boot應用,能夠先啓動server-demo
,再啓動client-demo
。spring
/user/{id} -> UserService -> UserServiceImpl
Browser Dubbo Client Dubbo Server
複製代碼
Client端:apache
@RestController
public class UserController {
@Reference(version = "1.0.0")
private UserService userService;
@GetMapping("/user/{id}")
public User findUserById(@PathVariable Integer id) {
return userService.findUser(id);
}
複製代碼
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);
}
複製代碼
$ wget https://alibaba.github.io/arthas/arthas-boot.jar
$ java -jar arthas-boot.jar
複製代碼
啓動後,會列出全部的java進程,選擇1,而後回車,就會鏈接上ServerDemoApplication
bash
$ 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
$
複製代碼
當線上服務拋出異常時,最着急的是什麼參數致使了拋異常?服務器
在demo裏,訪問 http://localhost:8080/user/0 ,UserServiceImpl
就會拋出一個異常,由於user id不合法。app
在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)
複製代碼
在本地開發時,可能會用到熱部署工具,直接改代碼,不須要重啓應用。可是在線上環境,有沒有辦法直接動態調試代碼?好比增長日誌。
在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信息。
在排查問題時,須要查看到更多的信息,若是能夠把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-info org.apache.log4j.Logger
code-source /Users/hengyunabc/.m2/repository/org/slf4j/log4j-over-slf4j/1.7.25/log4j-over-slf4j-1.7.25.jar
name org.apache.log4j.Logger
isInterface false
isAnnotation false
isEnum false
isAnonymousClass false
isArray false
isLocalClass false
isMemberClass false
isPrimitive false
isSynthetic false
simple-name Logger
modifier public
annotation
interfaces
super-class +-org.apache.log4j.Category
+-java.lang.Object
class-loader +-sun.misc.Launcher$AppClassLoader@5c647e05
+-sun.misc.Launcher$ExtClassLoader@59878d35
classLoaderHash 5c647e05
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.
INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD
----------------------------------------------------------------------------------------------------------------
1000 2018-12-05 17:47:52 1.56523 true false 0x3233483 UserServiceImpl findUser
1001 2018-12-05 17:48:03 0.286176 false true 0x3233483 UserServiceImpl findUser
1002 2018-12-05 17:48:11 90.324335 true false 0x3233483 UserServiceImpl findUser
複製代碼
上面的tt -t
命令捕獲到了3個請求。而後經過tt --play
能夠重放請求:
$ tt --play -i 1000
RE-INDEX 1000
GMT-REPLAY 2018-12-05 17:55:50
OBJECT 0x3233483
CLASS com.example.UserServiceImpl
METHOD findUser
PARAMETERS[0] @Integer[1]
IS-RETURN true
IS-EXCEPTION false
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,那麼一個請求會通過哪些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()
複製代碼
經過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,而後「隨心所欲」的例子。
在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],
]
複製代碼
SpringExtensionFactory@contexts.iterator.next
獲取到SpringExtensionFactory
裏保存的spring context對象#context.getBean("userServiceImpl").findUser(1)
獲取到userServiceImpl
再執行一次調用只要充分發揮想像力,組合Arthas裏的各類命令,能夠發揮出神奇的效果。
本篇文章來自杭州Dubbo Meetup的分享《當DUBBO趕上Arthas - 排查問題的實踐》,但願對你們線上排查Dubbo問題有幫助。
分享的PDF能夠在 github.com/alibaba/art… 裏下載。