V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hengyunabc
V2EX  ›  Java

Arthas 实践--使用 redefine 排查应用奇怪的日志来源

  •  
  •   hengyunabc ·
    hengyunabc · 2018-10-24 00:44:31 +08:00 · 1323 次点击
    这是一个创建于 2005 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    随着应用越来越复杂,依赖越来越多,日志系统越来越混乱,有时会出现一些奇怪的日志,比如:

    [] [] [] No credential found
    

    那么怎样排查这些奇怪的日志从哪里打印出来的呢?因为搞不清楚是什么 logger 打印出来的,所以想定位就比较头疼。

    下面介绍用 Arthas 的 redefine 命令快速定位奇怪日志来源。

    修改 StringBuilder

    首先在 java 代码里,字符串拼接基本都是通过StringBuilder来实现的。比如下面的代码:

    	public static String hello(String world) {
    		return "hello " + world;
    	}
    

    实际上生成的字节码也是用StringBuilder来拼接的:

      public static java.lang.String hello(java.lang.String);
        descriptor: (Ljava/lang/String;)Ljava/lang/String;
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=1, args_size=1
             0: new           #22                 // class java/lang/StringBuilder
             3: dup
             4: ldc           #24                 // String hello
             6: invokespecial #26                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
             9: aload_0
            10: invokevirtual #29                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
            13: invokevirtual #33                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
            16: areturn
    

    在 java 的 logger 系统里,输出日志时通常也是StringBuilder来实现的,最终会调用StringBuilder.toString(),那么我们可以修改StringBuilder的代码来检测到日志来源。

    StringBuilder.toString() 的原生实现是:

        @Override
        public String toString() {
            // Create a copy, don't share the array
            return new String(value, 0, count);
        }
    

    修改为:

        @Override
        public String toString() {
            // Create a copy, don't share the array
        	String result = new String(value, 0, count);
        	if(result.contains("No credential found")) {
        		System.err.println(result);
        		new Throwable().printStackTrace();
        	}
            return result;
        }
    

    增加的逻辑是:当 String 里包含No credential found时打印出当前栈,这样子就可以定位日志输出来源了。

    编绎修改过的 StringBuilder

    其实很简单,在 IDE 里把StringBuilder的代码复制一份,然后贴到任意一个工程里,然后编绎即可。

    也可以直接用 javac 来编绎:

    javac StringBuilder.java
    

    启动应用,使用 Arthas redefine 修改过的 StringBuilder

    启动应用后,在奇怪日志输出之前,先使用 arthas attach 应用,再 redefine StringBuilder:

    $ redefine -p /tmp/StringBuilder.class
    redefine success, size: 1
    

    当执行到输出[] [] [] No credential found的 logger 代码时,会打印当前栈。实际运行结果是:

    [] [] [] No credential found
    java.lang.Throwable
    	at java.lang.StringBuilder.toString(StringBuilder.java:410)
    	at com.taobao.middleware.logger.util.MessageUtil.getMessage(MessageUtil.java:26)
    	at com.taobao.middleware.logger.util.MessageUtil.getMessage(MessageUtil.java:15)
    	at com.taobao.middleware.logger.slf4j.Slf4jLogger.info(Slf4jLogger.java:77)
    	at com.taobao.spas.sdk.common.log.SpasLogger.info(SpasLogger.java:18)
    	at com.taobao.spas.sdk.client.identity.CredentialWatcher.loadCredential(CredentialWatcher.java:128)
    	at com.taobao.spas.sdk.client.identity.CredentialWatcher.access$200(CredentialWatcher.java:18)
    	at com.taobao.spas.sdk.client.identity.CredentialWatcher$1.run(CredentialWatcher.java:58)
    	at java.util.TimerThread.mainLoop(Timer.java:555)
    	at java.util.TimerThread.run(Timer.java:505)
    

    可以看到是spas.sdk打印出了[] [] [] No credential found的日志。

    总结

    • logger 最终会用 StringBuilder 来输出
    • 修改 StringBuilder 来定位输出特定日志的地方
    • 使用 Arthas redefine 命令来加载修改过的 StringBuilder
    • redefine 命令实际上实现了任意代码线上 debug 的功能,可以随意本地修改代码重新编绎,然后线上 redefine 加载
    • redefine 的功能过于强大,所以请小心使用:)
    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1299 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 17:41 · PVG 01:41 · LAX 10:41 · JFK 13:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.