天天看點

當Dubbo遇上Arthas - 排查問題的實踐

Apache Dubbo是Alibaba開源的高性能RPC架構,在國内有非常多的使用者。

Arthas是Alibaba開源的應用診斷利器,9月份開源以來,Github Star數三個月超過6000。

當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 
   Browser           Dubbo Client          Dubbo Server           

Client端:

@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);
    }           

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級别?

在排查問題時,需要檢視到更多的資訊,如果可以把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? 耗時是多少?

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動态代理是怎樣實作的?

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裡的各種指令,可以發揮出神奇的效果。

總結

本篇文章來自杭州Dubbo Meetup的分享《當DUBBO遇上Arthas - 排查問題的實踐》,希望對大家線上排查Dubbo問題有幫助。