arthas icon indicating copy to clipboard operation
arthas copied to clipboard

feat: support local variables at method lines

Open lotabout opened this issue 2 years ago • 16 comments

新特性:支持 watch 方法内的局部变量

背景

在用 arthas 做 debug 时,经常会遇到方法内部逻辑比较复杂,单看输入输出无法定位问题的情况。在调研方案过程中,找到相关问题

  1. https://github.com/alibaba/arthas/issues/486 提到可以用 redefine 的方式添加输出语句。但这种方式对一些 ToB 场景,修改代码加上传的操作很难实现。
  2. https://github.com/alibaba/arthas/issues/1311 提到 bytekit 已经能支持局部变量的绑定

实现方式

该 PR 只是比较简单粗暴地将 bytekit 的 @AtLinewatch 命令做了集成。

感觉集成本身不难,不清楚之前不集成是因为人力问题还是其它深层次的考虑(比如说性能)。

待提升

当前的 Interceptor 会 watch 所有行,然后在 WatchAdviceListener 再 filter 掉不需要的行,这样做可能会有潜在的性能问题。

更好的方式是在 transform 时就指定 filter 的行,只对对应的行做字节码增强。这么做需要将行号动态传递给 @AtLine 注解,且在 transform 时就需要拿到 WatchCommand 的实例,对现有架构会有较大改动,还没有细想实现方案。

lotabout avatar Jan 09 '22 03:01 lotabout

CLA assistant check
All committers have signed the CLA.

CLAassistant avatar Jan 09 '22 03:01 CLAassistant

首先这个PR的工作挺好的。从技术上来说,支持行号的确不是问题。主要有几个考虑:

  1. 每一行都增强,这个生成的字节码会极速膨胀,很可能会超出jvm限制
  2. 在jdk 11开始,jvm字节码的校验严格很多,@AtLine的方式增强的字节码很可能会被认为是非常的
  3. 对于一定要了解某行变量的情况,可以用先本地增加打印语句,编译出新的.class,再用 retransform命令来热更新。个人经验,用retransform足够解决绝大部分问题了。

hengyunabc avatar Jan 10 '22 03:01 hengyunabc

@hengyunabc 多谢回复,关于上面几点考虑

  1. 每一行都增强,这个生成的字节码会极速膨胀,很可能会超出jvm限制

这个问题在之前只考虑过每行都 callback 的性能问题,没有想过 JVM 限制问题。查了下每个方法限制是 64K。

如果能实现只在需要的时候,在需要的行加上 @AtLine 的增强,理论上是可以规避这个问题的。

  1. 在jdk 11开始,jvm字节码的校验严格很多,@AtLine的方式增强的字节码很可能会被认为是非常的

这个问题有具体示例吗?这个 PR 我在 1.8 和 11 上都测试过,当然涉及的变量还只有 primitive type。

  1. 对于一定要了解某行变量的情况,可以用先本地增加打印语句,编译出新的.class,再用 retransform命令来热更新。个人经验,用retransform足够解决绝大部分问题了。

这个理解的,我们也是这么用的,的确能解决绝大多数问题。 现在想解决的是少数的 case,我们是 ToB 的场景,人和环境是隔离的,要上传 .class 文件也挺困难的。有时需要指导交付人员收集关键信息,之前尝试过 jad + recompile,但 jad 的结果并没有办法无脑 compile 成功。

想问下如果解决第一条,能做到按需增强,会考虑增加这个功能吗?

lotabout avatar Jan 10 '22 06:01 lotabout

@lotabout

  1. 这个功能是可以考虑做出来的,值得尝试下。

  2. 如果要实现指定的 @AtLine,可能要直接调bytekit api,或者要写一个新的 annotation 之类的

  3. jdk11字节码校验,这个可能要找一些比较复杂的例子,比如在 for 循环里调用另一个复杂参数的函数

  4. 可能还要考虑类似动态trace的功能。比如watch 了x行,还要再watch y行。 https://arthas.aliyun.com/doc/trace#id9

hengyunabc avatar Jan 10 '22 07:01 hengyunabc

@hengyunabc 麻烦再 Review 下

  1. 如果要实现指定的 @AtLine,可能要直接调bytekit api,或者要写一个新的 annotation 之类的

按需增强已经实现。

  1. 可能还要考虑类似动态trace的功能。比如watch 了x行,还要再watch y行。 https://arthas.aliyun.com/doc/trace#id9

这个功能没有做额外的处理,但是实现后测试了一下是支持的。

  1. jdk11字节码校验,这个可能要找一些比较复杂的例子,比如在 for 循环里调用另一个复杂参数的函数

字节码校验之前有遇到具体的 case 吗?我可以来测一下。

lotabout avatar Jan 16 '22 12:01 lotabout

这个功能我也在考虑,需求挺高的

wwulfric avatar Mar 20 '22 14:03 wwulfric

nice addition.

zinking avatar May 21 '22 04:05 zinking

感谢你的分享,请问你的实现中,是不是不支持局部变量和返回值、抛出异常同时观测?

Mengleijin avatar Jun 06 '22 06:06 Mengleijin

感谢你的分享,请问你的实现中,是不是不支持局部变量和返回值、抛出异常同时观测?

支持同时观测,只是目前只是 OR 的关系,做不到 AND 关系(即在抛出异常的前提下,才输出每行的值)

lotabout avatar Jun 07 '22 01:06 lotabout

感谢你的分享,请问你的实现中,是不是不支持局部变量和返回值、抛出异常同时观测?

支持同时观测,只是目前只是 OR 的关系,做不到 AND 关系(即在抛出异常的前提下,才输出每行的值)

sorry,我昨天看了一下WatchAdviceListener的源码,应该是支持同时观测的吧,如果想同时观测,把这几个观察点(-l -b -e -s)都选上就可以了吧。然后请教一个问题,如果并发量比较高的情况下,这几个观察点的观察结果是不是可能是不连续的,比如正常情况:b l s b l s,并发量高的情况b b l s l s

Mengleijin avatar Jun 09 '22 08:06 Mengleijin

@Mengleijin

应该是支持同时观测的吧

是的,就是我上面说的 OR 的关系,可以同时打开

如果并发量比较高的情况下,这几个观察点的观察结果是不是可能是不连续的

是的,当前 instrument 的机制可以理解成在一行后执行一个 print,这个机制决定了它没有函数级别的“事务”。

lotabout avatar Jun 09 '22 14:06 lotabout

我自己的实现方式是换了一个新的break命令,这样不会影响原始的watch。不过看作者 @hengyunabc 是什么意见吧

wwulfric avatar Jun 10 '22 03:06 wwulfric

目前这个PR没被处理有几个原因:

  • 生成的字节码会增强所有的line,这个感觉消耗太大
  • 目前我在考虑做一个内部的调整,把arthas内部的实现改为面向service的方式,统一一层抽象,先从功能层考虑,再对接命令行和HTTP API调用。简而言之,arthas很多开发都假定是命令行,所以很多功能都只考虑了命令行的操作方式,这个导致很多不合理的地方。

hengyunabc avatar Jun 13 '22 02:06 hengyunabc

我自己的实现方式是换了一个新的break命令,这样不会影响原始的watch。不过看作者 @hengyunabc 是什么意见吧

这个实现也不会影响原始的watch吧,只是对watch的观察事件点进行了扩展,这样设计是不是更合理一些,因为观察局部变量的同时经常也需要观察入参,返回值和异常

Mengleijin avatar Jun 13 '22 03:06 Mengleijin

@hengyunabc

  • 生成的字节码会增强所有的line,这个感觉消耗太大

这部分在最新的代码只会按需增强了,watch 命令里指定 line range,如 -l 53-55,则只会 enhance 对应的行数,例如

watch demo.MathGame primeFactors '{line, varMap}' -l 53-55 -x 2

则 enhanced class 如下(jad-gui 反编译之后),只有 3 个 atLine

      while (i <= SYNTHETIC_LOCAL_VARIABLE_1) {
        if (SYNTHETIC_LOCAL_VARIABLE_1 % i == 0) {
          (new Object[1])[0] = new Integer(SYNTHETIC_LOCAL_VARIABLE_1);
          (new Object[4])[0] = this;
          (new Object[4])[1] = new Integer(SYNTHETIC_LOCAL_VARIABLE_1);
          (new Object[4])[2] = result;
          (new Object[4])[3] = new Integer(i);
          String[] arrayOfString1 = { "this", "number", "result", "i" };
          Object[] arrayOfObject3 = new Object[4];
          byte b1 = 53;
          Object[] arrayOfObject2 = new Object[1];
          String str2 = "primeFactors|(I)Ljava/util/List;";
          Class<MathGame> clazz2 = MathGame.class;
          MathGame mathGame2 = this;
          SpyAPI.atLine(clazz2, str2, mathGame2, arrayOfObject2, b1, arrayOfString1, arrayOfObject3);
          result.add(Integer.valueOf(i));
          (new Object[1])[0] = new Integer(SYNTHETIC_LOCAL_VARIABLE_1);
          (new Object[4])[0] = this;
          (new Object[4])[1] = new Integer(SYNTHETIC_LOCAL_VARIABLE_1);
          (new Object[4])[2] = result;
          (new Object[4])[3] = new Integer(i);
          String[] arrayOfString2 = { "this", "number", "result", "i" };
          Object[] arrayOfObject5 = new Object[4];
          byte b2 = 54;
          Object[] arrayOfObject4 = new Object[1];
          String str3 = "primeFactors|(I)Ljava/util/List;";
          Class<MathGame> clazz3 = MathGame.class;
          MathGame mathGame3 = this;
          SpyAPI.atLine(clazz3, str3, mathGame3, arrayOfObject4, b2, arrayOfString2, arrayOfObject5);
          number = SYNTHETIC_LOCAL_VARIABLE_1 / i;
          (new Object[1])[0] = new Integer(number);
          (new Object[4])[0] = this;
          (new Object[4])[1] = new Integer(number);
          (new Object[4])[2] = result;
          (new Object[4])[3] = new Integer(i);
          String[] arrayOfString3 = { "this", "number", "result", "i" };
          Object[] arrayOfObject7 = new Object[4];
          byte b3 = 55;
          Object[] arrayOfObject6 = new Object[1];
          String str4 = "primeFactors|(I)Ljava/util/List;";
          Class<MathGame> clazz4 = MathGame.class;
          MathGame mathGame4 = this;
          SpyAPI.atLine(clazz4, str4, mathGame4, arrayOfObject6, b3, arrayOfString3, arrayOfObject7);
          i = 2;
          continue;
        } 
        i++;
      } 

目前我在考虑做一个内部的调整,把arthas内部的实现改为面向service的方式,统一一层抽象,先从功能层考虑,再对接命令行和HTTP API调用。简而言之,arthas很多开发都假定是命令行,所以很多功能都只考虑了命令行的操作方式,这个导致很多不合理的地方。

期待新的成果~

lotabout avatar Jun 14 '22 01:06 lotabout

@lotabout 想确定一下,现在针对某一行观察局部变量时,我试了一下是可以重复增强的,这个是bug还是本来就是这样设计的? 我看Enhancer代码里,也加了atLine过滤器,但是还是重复增强了

a, 增强前反编译:

        public Result getTopTerm(TrendRequest trendRequest, boolean isDeviceBrand) throws Exception {
            String requestBody;
            ResponseEntity<String> esResponse;
            JSONObject object;
/*116*/     if (BuglyStatInfoService.checkRequest((TrendRequest)trendRequest)) {
                return Result.error((Status)Status.REQUEST_PARAMS_NOT_VALID_ERROR);
            }
/*117*/     AppInfo appInfo = this.appInfoDao.getByAppId(trendRequest.getAppId());

b, 第一次增强后反编译:

        public Result getTopTerm(TrendRequest trendRequest, Boolean bl) throws Exception {
            Object[] objectArray = new Object[]{trendRequest, bl};
            String string = "getTopTerm|(Lcn/jj/cmec/jjbugly/subbugly/model/request/TrendRequest;Ljava/lang/Boolean;)Lcn/jj/cmec/jjbugly/subbugly/model/common/Result;";
            Class<StatService> clazz = StatService.class;
            StatService statService = this;
            SpyAPI.atEnter(clazz, (String)string, (Object)statService, (Object[])objectArray);
            try {
                String requestBody;
                ResponseEntity<String> esResponse;
                JSONObject object;
                Boolean isDeviceBrand;
                void trendRequest2;
/*116*/         if (BuglyStatInfoService.checkRequest((TrendRequest)trendRequest2)) {
                    Result result = Result.error((Status)Status.REQUEST_PARAMS_NOT_VALID_ERROR);
                    Result result2 = result;
                    Object[] objectArray2 = new Object[]{trendRequest2, isDeviceBrand};
                    String string2 = "getTopTerm|(Lcn/jj/cmec/jjbugly/subbugly/model/request/TrendRequest;Ljava/lang/Boolean;)Lcn/jj/cmec/jjbugly/subbugly/model/common/Result;";
                    Class<StatService> clazz2 = StatService.class;
                    StatService statService2 = this;
                    SpyAPI.atExit(clazz2, (String)string2, (Object)statService2, (Object[])objectArray2, (Object)result2);
                    return result;
                }
/*117*/         AppInfo appInfo = this.appInfoDao.getByAppId(trendRequest2.getAppId());
/*118*/         trendRequest2.setAppId(Integer.valueOf(100000));
/*119*/         isDeviceBrand = false;
                String[] stringArray = new String[]{"this", "trendRequest", "isDeviceBrand", "appInfo"};
                Object[] objectArray3 = new Object[]{this, trendRequest2, isDeviceBrand, appInfo};
                int n = 120;
                Object[] objectArray4 = new Object[]{trendRequest2, isDeviceBrand};
                String string3 = "getTopTerm|(Lcn/jj/cmec/jjbugly/subbugly/model/request/TrendRequest;Ljava/lang/Boolean;)Lcn/jj/cmec/jjbugly/subbugly/model/common/Result;";
                Class<StatService> clazz3 = StatService.class;
                StatService statService3 = this;
                SpyAPI.atLine(clazz3, (String)string3, (Object)statService3, (Object[])objectArray4, (int)n, (String[])stringArray, (Object[])objectArray3);

c, 第二次增强后反编译:

        public Result getTopTerm(TrendRequest trendRequest, boolean bl) throws Exception {
            Object[] objectArray = new Object[]{trendRequest, new Boolean(bl)};
            String string = "getTopTerm|(Lcn/jj/cmec/jjbugly/subbugly/model/request/TrendRequest;Z)Lcn/jj/cmec/jjbugly/subbugly/model/common/Result;";
            Class<StatService> clazz = StatService.class;
            StatService statService = this;
            SpyAPI.atEnter(clazz, (String)string, (Object)statService, (Object[])objectArray);
            try {
                String requestBody;
                ResponseEntity<String> esResponse;
                JSONObject object;
                boolean isDeviceBrand;
                void trendRequest2;
/*116*/         if (BuglyStatInfoService.checkRequest((TrendRequest)trendRequest2)) {
                    Result result = Result.error((Status)Status.REQUEST_PARAMS_NOT_VALID_ERROR);
                    Result result2 = result;
                    Object[] objectArray2 = new Object[]{trendRequest2, new Boolean(isDeviceBrand)};
                    String string2 = "getTopTerm|(Lcn/jj/cmec/jjbugly/subbugly/model/request/TrendRequest;Z)Lcn/jj/cmec/jjbugly/subbugly/model/common/Result;";
                    Class<StatService> clazz2 = StatService.class;
                    StatService statService2 = this;
                    SpyAPI.atExit(clazz2, (String)string2, (Object)statService2, (Object[])objectArray2, (Object)result2);
                    return result;
                }
/*117*/         AppInfo appInfo = this.appInfoDao.getByAppId(trendRequest2.getAppId());
/*118*/         trendRequest2.setAppId(Integer.valueOf(100000));
/*119*/         isDeviceBrand = false;
/*120*/         int i = 1 / 0;
                String[] stringArray = new String[]{"this", "trendRequest", "isDeviceBrand", "appInfo", "i"};
                Object[] objectArray3 = new Object[]{this, trendRequest2, new Boolean(isDeviceBrand), appInfo, new Integer(i)};
                int n = 121;
                Object[] objectArray4 = new Object[]{trendRequest2, new Boolean(isDeviceBrand)};
                String string3 = "getTopTerm|(Lcn/jj/cmec/jjbugly/subbugly/model/request/TrendRequest;Z)Lcn/jj/cmec/jjbugly/subbugly/model/common/Result;";
                Class<StatService> clazz3 = StatService.class;
                StatService statService3 = this;
                SpyAPI.atLine(clazz3, (String)string3, (Object)statService3, (Object[])objectArray4, (int)n, (String[])stringArray, (Object[])objectArray3);
                String[] stringArray2 = new String[]{"this", "trendRequest", "isDeviceBrand", "appInfo", "i"};
                Object[] objectArray5 = new Object[]{this, trendRequest2, new Boolean(isDeviceBrand), appInfo, new Integer(i)};
                int n2 = 121;
                Object[] objectArray6 = new Object[]{trendRequest2, new Boolean(isDeviceBrand)};
                String string4 = "getTopTerm|(Lcn/jj/cmec/jjbugly/subbugly/model/request/TrendRequest;Z)Lcn/jj/cmec/jjbugly/subbugly/model/common/Result;";
                Class<StatService> clazz4 = StatService.class;
                StatService statService4 = this;
                SpyAPI.atLine(clazz4, (String)string4, (Object)statService4, (Object[])objectArray6, (int)n2, (String[])stringArray2, (Object[])objectArray5);

Mengleijin avatar Jun 15 '22 10:06 Mengleijin