arthas
arthas copied to clipboard
feat: support local variables at method lines
新特性:支持 watch 方法内的局部变量
背景
在用 arthas 做 debug 时,经常会遇到方法内部逻辑比较复杂,单看输入输出无法定位问题的情况。在调研方案过程中,找到相关问题
- https://github.com/alibaba/arthas/issues/486 提到可以用 redefine 的方式添加输出语句。但这种方式对一些 ToB 场景,修改代码加上传的操作很难实现。
- https://github.com/alibaba/arthas/issues/1311 提到 bytekit 已经能支持局部变量的绑定
实现方式
该 PR 只是比较简单粗暴地将 bytekit 的 @AtLine
与 watch
命令做了集成。
感觉集成本身不难,不清楚之前不集成是因为人力问题还是其它深层次的考虑(比如说性能)。
待提升
当前的 Interceptor 会 watch 所有行,然后在 WatchAdviceListener
再 filter 掉不需要的行,这样做可能会有潜在的性能问题。
更好的方式是在 transform 时就指定 filter 的行,只对对应的行做字节码增强。这么做需要将行号动态传递给 @AtLine
注解,且在 transform 时就需要拿到 WatchCommand
的实例,对现有架构会有较大改动,还没有细想实现方案。
首先这个PR的工作挺好的。从技术上来说,支持行号的确不是问题。主要有几个考虑:
- 每一行都增强,这个生成的字节码会极速膨胀,很可能会超出jvm限制
- 在jdk 11开始,jvm字节码的校验严格很多,
@AtLine
的方式增强的字节码很可能会被认为是非常的 - 对于一定要了解某行变量的情况,可以用先本地增加打印语句,编译出新的
.class
,再用retransform
命令来热更新。个人经验,用retransform
足够解决绝大部分问题了。
@hengyunabc 多谢回复,关于上面几点考虑
- 每一行都增强,这个生成的字节码会极速膨胀,很可能会超出jvm限制
这个问题在之前只考虑过每行都 callback 的性能问题,没有想过 JVM 限制问题。查了下每个方法限制是 64K。
如果能实现只在需要的时候,在需要的行加上 @AtLine
的增强,理论上是可以规避这个问题的。
- 在jdk 11开始,jvm字节码的校验严格很多,@AtLine的方式增强的字节码很可能会被认为是非常的
这个问题有具体示例吗?这个 PR 我在 1.8 和 11 上都测试过,当然涉及的变量还只有 primitive type。
- 对于一定要了解某行变量的情况,可以用先本地增加打印语句,编译出新的.class,再用 retransform命令来热更新。个人经验,用retransform足够解决绝大部分问题了。
这个理解的,我们也是这么用的,的确能解决绝大多数问题。 现在想解决的是少数的 case,我们是 ToB 的场景,人和环境是隔离的,要上传 .class 文件也挺困难的。有时需要指导交付人员收集关键信息,之前尝试过 jad + recompile,但 jad 的结果并没有办法无脑 compile 成功。
想问下如果解决第一条,能做到按需增强,会考虑增加这个功能吗?
@lotabout
-
这个功能是可以考虑做出来的,值得尝试下。
-
如果要实现指定的
@AtLine
,可能要直接调bytekit api,或者要写一个新的 annotation 之类的 -
jdk11字节码校验,这个可能要找一些比较复杂的例子,比如在 for 循环里调用另一个复杂参数的函数
-
可能还要考虑类似动态trace的功能。比如watch 了x行,还要再watch y行。 https://arthas.aliyun.com/doc/trace#id9
@hengyunabc 麻烦再 Review 下
- 如果要实现指定的
@AtLine
,可能要直接调bytekit api,或者要写一个新的 annotation 之类的
按需增强已经实现。
- 可能还要考虑类似动态trace的功能。比如watch 了x行,还要再watch y行。 https://arthas.aliyun.com/doc/trace#id9
这个功能没有做额外的处理,但是实现后测试了一下是支持的。
- jdk11字节码校验,这个可能要找一些比较复杂的例子,比如在 for 循环里调用另一个复杂参数的函数
字节码校验之前有遇到具体的 case 吗?我可以来测一下。
这个功能我也在考虑,需求挺高的
nice addition.
感谢你的分享,请问你的实现中,是不是不支持局部变量和返回值、抛出异常同时观测?
感谢你的分享,请问你的实现中,是不是不支持局部变量和返回值、抛出异常同时观测?
支持同时观测,只是目前只是 OR 的关系,做不到 AND 关系(即在抛出异常的前提下,才输出每行的值)
感谢你的分享,请问你的实现中,是不是不支持局部变量和返回值、抛出异常同时观测?
支持同时观测,只是目前只是 OR 的关系,做不到 AND 关系(即在抛出异常的前提下,才输出每行的值)
sorry,我昨天看了一下WatchAdviceListener的源码,应该是支持同时观测的吧,如果想同时观测,把这几个观察点(-l -b -e -s)都选上就可以了吧。然后请教一个问题,如果并发量比较高的情况下,这几个观察点的观察结果是不是可能是不连续的,比如正常情况:b l s b l s,并发量高的情况b b l s l s
@Mengleijin
应该是支持同时观测的吧
是的,就是我上面说的 OR 的关系,可以同时打开
如果并发量比较高的情况下,这几个观察点的观察结果是不是可能是不连续的
是的,当前 instrument 的机制可以理解成在一行后执行一个 print,这个机制决定了它没有函数级别的“事务”。
我自己的实现方式是换了一个新的break命令,这样不会影响原始的watch。不过看作者 @hengyunabc 是什么意见吧
目前这个PR没被处理有几个原因:
- 生成的字节码会增强所有的line,这个感觉消耗太大
- 目前我在考虑做一个内部的调整,把arthas内部的实现改为面向service的方式,统一一层抽象,先从功能层考虑,再对接命令行和HTTP API调用。简而言之,arthas很多开发都假定是命令行,所以很多功能都只考虑了命令行的操作方式,这个导致很多不合理的地方。
我自己的实现方式是换了一个新的break命令,这样不会影响原始的watch。不过看作者 @hengyunabc 是什么意见吧
这个实现也不会影响原始的watch吧,只是对watch的观察事件点进行了扩展,这样设计是不是更合理一些,因为观察局部变量的同时经常也需要观察入参,返回值和异常
@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 想确定一下,现在针对某一行观察局部变量时,我试了一下是可以重复增强的,这个是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);