Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: 表达式体验优化与引擎更换 #2849

Open
DQinYuan opened this issue Jun 18, 2024 · 11 comments
Open

Proposal: 表达式体验优化与引擎更换 #2849

DQinYuan opened this issue Jun 18, 2024 · 11 comments

Comments

@DQinYuan
Copy link

DQinYuan commented Jun 18, 2024

背景

@hengyunabc 在去年发起过一个关于考虑不同的表达式引擎 #2747

我们在阿里内部进行了一些简单的交流, 希望能够借着 Arthas 4 的机会, 进行一次表达式体验的优化, 让用户能够更加方便地在黑屏情况下操作 Arthas 表达式。本次优化的主要目标有三个:

  • 让表达式更加接近 Java 程序员的使用习惯
  • 对于复杂表达式, 可以分多步调试和写出, 边写边调才更加顺畅
  • 疑难问题解决: 比如泛型方法调用等

本文希望就这三个问题表达想法, 并与社区的朋友们一起讨论。

提议一: 更符合 Java 程序员的习惯

ognl 语法简洁, 但终究和 Java 的语法相差甚远。虽说现在有工具和 AI 的加持的, 但在需要黑屏操作, 或者微调的时候, 还是相当麻烦。

我是 QLExpress 的开源支持者之一, 并且也计划在今年推出 4.0 版本。我提议将 Arthas 的表达式更换为 QLExpress, 考虑如下:

  • QLExpress 的语法更加贴近 Java, 符合 Java 程序员习惯, Java 开发者可以不需要太多思考就能写出
  • QLExpress 和 Arthas 一样, 都是阿里内外部广泛使用的开源项目, 经过多年检验。相关开发者在阿里座位也邻近, 有利于形成技术和运营上的合力
  • 另外, 通过 QLExpress 自定义函数/操作符/宏的能力, 也更方便丰富 Arthas 表达式的写法

Arthas的一些特殊用法文档说明 这个 issue 中, 大家讨论了 ognl 在各种特殊场景下的写法, 借助 issue 中的一些例子, 展示在 QLExpress 中的写法:

以下写法还处于草稿状态, 修改的弹性很大, 欢迎社区的朋友们提意见

操作 OGNL QLExpress
投影操作 l.{ #this.name } l*.name
按条件过滤 l.{? #this.name == null } l.filter(i -> i.name == null)
过滤后统计 l.{? #this.age > 10 }.size() l.filter(i -> i.name == null).size()
复合表达式 l.{? #this.age > 10 }.size().(#this > 20 ? #this - 10 : #this + 10) l.filter(i -> i.age > 10).size().let(i -> i > 20? i - 10: i + 10)
找到第一个符合条件的元素 l.{^ #this.name != null} l.findFirst(i -> i.name != null)
找到最后一个符合条件的元素 l.{$ #this.name != null} l.findLast(i -> i.name != null)
静态变量访问 @com.taobao.container.Test@m com.taobao.container.Test.m
静态方法调用 @java.lang.Thread@currentThread() java.lang.Thread.currentThread()
内部类 @com.taobao.container.Test$InnerClassName@m com.taobao.container.Test.InnerClassName.m
复杂对象调用 @com.alibaba.fastjson.JSON@parseObject("{\"name\":\"aaa\"}", xxxx.class) service.method({'a':1,'b':2, 'class':a.b.c}, {'a':1,'b':2, 'class':a.b.c.d})

提议二: 多步表达式

即使使用了 QLExpress, 以上的某些写法也有些太复杂。我们在咨询了一些用户后发现, 表达式难以编写的原因, 只有部分是因为对 ognl 语法不了解, 更深层次的原因还是在于: 让开发者一次性写对复杂的逻辑

因此我们提议在 Arthas 中支持多步表达式: 即支持将某个命令的结果保存下来, 然后用类似 REPL(Read–eval–print loop) 的表达式交互界面逐步完成复杂的数据操作。

这里给两个例子说明操作过程。

多步表达式的用法也还处于草稿状态, 环境社区的朋友们一起脑洞大开, 提意见

getStatic 获取静态变量并处理

假设在 com.alibaba.arthas.Test 类中有一个静态变量 n, 它是一个 Person 列表, 我从中筛选出姓名为 li 的人的年龄。

原本 OGNL 表达式的写法如下:

$ getstatic com.alibaba.arthas.Test n 'iterator.{? #this.name=="li"}.age'

如果使用 QLExpress 一次性完成, 则写法如下:

$ getstatic com.alibaba.arthas.Test n 'this.filter(i -> i.name=="li").age'

整个处理过程, 其实分为筛选元素, 和取 age 属性两步完成。下面演示两种多步表达式用法:

多步表达式用法一:

# 将 getstatic 命令的结果赋值给变量 n
# 变量 n 会被保存在会话中
$ n=$(getstatic com.alibaba.arthas.Test n)
[
  {
    "name": "li",
    "age": 30
  },
  {
    "name": "wang",
    "age": 11
  }
]
# 新增一个单独的 eval 命令用于计算表达式
$ p=$(eval 'n.filter(it.name=="li")')
[
  {
    "name": "li",
    "age": 30
  }
]
$ eval 'p.age'
30

多步表达式用法二:

$ n=$(getstatic com.alibaba.arthas.Test n)
$ eval
# 进入一个表达式上下文, 编写表达式更加方便
eval$ p=n.filter(it.name=='li')
{
  "name": "li",
  "age": 30
}
eval$ p.age
30
eval$ exit()

watch 获得方法参数/返回值并且处理

实践中, 有时候会碰到, 业务列表/对象实在太大了, 连一屏幕都显示不下, 此时去抠关键信息就如同大海捞针。有了多步表达式, 就可以一点点去筛选关键信息的位置。

watch 也比较特殊, 因为它执行后会等待在那里, 直到用户按下 Ctrl + C 或者超过最大数目限制。因为我们将 watch 命令的返回值定义为最近 5 个 expression 参数的返回值(小于 5 个则保存全部请求)。

举个例子, 对于

$ n=${watch demo.MathGame primeFactors "{params,returnObj}" -x 2}

假设 watch 到 10 个请求后, 用户按 Ctrl + C 停止, 则 n 中保存的值为:

下方为伪代码, 比如 params10 表示第 10 次请求的参数, returnObj10 表示第 10 次请求的返回值

[
  {params10,returnObj10},
  {params9,returnObj9},
  {params8,returnObj8},
  {params7,returnObj7},
  {params6,returnObj6}
]

假设 com.taobao.container.Test 类的 test 方法的第一个参数为 Person 类型的列表。 我们想从最近一次调用该方法的参数中, 筛选出姓名为 li 的人的年龄, 则多步式写法如下:

$ n=$(watch com.taobao.container.Test test "{params,returnObj}" "returnObj.age==10&&params[0]==3" -x 2)
[
  //...
]
# 等待,直到用户 Ctrl+C 暂停,只保存最近 5 个,作为一个 list 保存在变量 n 中
$ eval
# 取最近一次 watch 的结果
eval$ last=n[0]
[
   [
     {
        "name": "wang",
        "age": 11
     },
     {
       "name": "li",
       "age": 23
     }
   ]
]
eval$ p=last[0].filter(it.name.age>20)
[
 {
   "name": "li",
   "age": 23
 }
]
eval$ p[0].age
23

提议三: 疑难问题解决

arthas-idea-plugin 的作者 @WangJi92, 以及其他用户的交流中, 我们找到下面几个 arthas 表达式使用起来复杂, 甚至无法解决的问题。

这里罗列的不一定完整, 欢迎大家在继续补充

调用含有复杂对象的方法

假设有一个 a.b.c.MyService 中有一个方法为 myMethod, 它接受单个类型为 Person 的参数, Person 中只有一个属性为 name。则调用该方法在目前 Arthas 中只有两个方式:

  • 方式一: 新建 Person 类, 逐个 set 属性完成后, 再调用方法
vmtool -x 3 --action getInstances --className a.b.c.MyService --express '#p=new a.b.c.Person(),#p.setName("li"),instances[0].myMethod(#p)'

这个方法仅仅是可行, 实际用起来体验很差。特别是对象嵌套层次复杂时, 因为没法格式化, 让广大程序员的近视度数又增加了。

  • 方式二: 利用应用中 json 序列化库, 比如 fastjson, 将 json 反序列化成需要的对象, 再调用方法
vmtool -x 3 --action getInstances --className a.b.c.MyService  --express 'instances[0].myMethod(@com.alibaba.fastjson.JSON@parseObject("{\"name\":\" li\"}",@a.b.c.Person@class))'

这里的表达式也很复杂, 需要转义 Json, 并且依赖应用中包含的 Json 序列化库。但是比较模板化, 只需要在别的编辑工具中将 Json 编辑好, 转义后复制到模板中即可。

arthas-idea-plugin 就利用类似的表达式模板, 自动生成包含复杂对象的调用, 如下图:

image

在表达式替换为 QLExpress4 后, 我们计划的写法如下:

vmtool -x 3 --action getInstances --className a.b.c.MyService  --express 'instances[0].myMethod({"name":"li", "class": "a.b.c.Person"}))'

模板化的表达式更少了, 不再需要烦人的转义, 也不依赖应用中的 Json 序列化框架。书写嵌套对象也更加 easy, 一个嵌套对象的案例如下:

{
    "name": "li",
    "age": {
        "year": 31,
        "mouth": 5,
        "class": "a.b.c.Age"
    },
    "class": "a.b.c.Person"
}

方法参数为泛型类

OGNL 不支持泛型, 如果参数类中含有泛型, 甚至连序列化也帮不了你。

因为泛型的序列化需要借助带泛型的 TypeReference, 而 OGNL 中无法书写泛型。相关的表达式生成工具只能绕道, 先将属性序列化出来后, 再调用 set 方法设置进对象中。

假设方法签名是这样的:

public class MyService {  
    public String myMethodGeneric(Pair<Person, Home> pair) {  
        return "";  
    }  
}

Pair 类的定义如下:

public class Pair<L, R> {

    private int num;

    private L left;

    private R right;
    // 省略 get set ...
}

想用 vmtool 调用它, 采用 OGNL + 序列化的写法就特别麻烦。必须要先序列化 left 属性, 再序列化 right 属性, 最后再设置进 Pair 对象中:

vmtool -x 3 --action getInstances --className a.b.c.MyService  --express 'instances[0].myMethodGeneric((#p=@com.alibaba.fastjson.JSON@parseObject("{\"num\":2}",@a.b.c.Pair@class),(#p.setLeft(@com.alibaba.fastjson.JSON@parseObject("{\"name\":\" li\"}",@a.b.c.Person@class))),(#p.setRight(@com.alibaba.fastjson.JSON@parseObject("{\"address\":\"street 404\"}",@a.b.c.Home@class))),#p))'

这还只是在嵌套了一层的情况下, 如果对象层次更多, 即使工具能够生成, 也复杂得根本看不明白, 修改不了, 也没有任何工具可以将其结构化。

如果使用 QLExpress4, 则可以将其写得非常清晰:

vmtool -x 3 --action getInstances --className a.b.c.MyService  --express 'instances[0].myMethodGeneric({"name":"li","age":{"year":31,"mouth":5,"class":"a.b.c.Age"},"class":"a.b.c.Person"})'

调用泛型方法

上一种情况虽然复杂, 至少还能借助工具生成。

和相关工具作者交流后, 他们表示, 如果调用的方法本身带有泛型方法的话, 则工具无法生成, 只能让开发者手工做微调, 比如下面的泛型方法:

public class MyService<R> {  
    public <L> String myMethodGeneric2(Pair<L, R> pair) {  
        return "";  
    }  
}

因为不知道开发者想要赋予泛型的类型是什么, 工具无法生成准确的表达式。

如果让开发者手工微调的话, 因为表达式复杂度很高, 也几乎不可能, 于是陷入了两难, 几乎是不可用的状态。

当然, 这个问题如果工具作者想解的话, 也不是不能解, 比如加一个交互步骤, 让用户先手动选择想要的泛型类, 之后再执行生成。

不过直接采用上面 QLExpress4 的写法更简洁。

常见 Q & A

问: Arthas 经常会在线上使用, 较复杂的表达式体验能够防止线上的误操作, 或者某些恶意操作。多步式表达式会不会导致这些行为过于容易呢?

: Arthas 能够做任何想得到的 Java 调用, 不是不能做, 只是做起来比较复杂。那自然会有人开发脚本生成工具来简化操作, 比如 @WangJi92arthas-idea-plugin, 就能够自动生成任意复杂的 Java 调用脚本。与其让别人来简化, 不如 Arthas 自己来简化, 还更可控一些, 未来用更有效的机制保证安全。

问: 多步式体验优化和 QLExpress 的关系是什么?

: 没有技术上的关系, OGNL 也可以开发多步式的交互。但是既然已经用了 QLExpress4 做表达式体验优化, 就希望一步到位, 将体验优化到极致。提供一个新的功能特性, 也能够吸引用户升级, 并且尝试了解新的表达式引擎。

问: 复杂表达式编写是个低频操作, 有必要优化低频操作的体验吗?

: 高频的简单表达式写法我们维持不变, 比如最常用的 watch 表达式 {params,returnObj,throwExp}, 或者简单的对象取属性, 写法和原来保持一致。仅仅在需要更加复杂的处理时, 才需要了解 QLExpress4 的写法, 整体上是一个帕累托改进, 即在没有损失高频操作体验的情况下, 提升低频操作体验。而且之前的 "低频" 也不一定是真的 "低频", 可能是因为太麻烦, 开发者懒得这么用, 现在我们将它变简单了, 用户说不定能想出更多玩法呢。

如果有更多问题, 也欢迎大家在评论区讨论。

@zgxkbtl
Copy link

zgxkbtl commented Jun 19, 2024

多步表达式的方式也是我一直赞成的方式,现有热门的脚本语言都有REPL,使用方式简单而且深入人心。

  • 为了不影响现有使用习惯,可以将类似subprocess作为函数引入,执行命令,subprocess.run(["watch", "demo.MathGame", "primeFactors", "-x", "5"], out=PIPE, text=True) 。其中PIPE可以是一个outputStream,这样不用考虑如何表达将watch的结果存起来。
  • 基于表达式和语句还可以做成jupter notebook的操作界面,编写自动化流程

@taokan
Copy link

taokan commented Jun 20, 2024

Arthas和QLExpress的用户,看到了内部技术产品联合很兴奋!可以支持需求的快速迭代,这个效率是用外部工具比不了的。
对于用户说,定位排查要快速,数据分析要容易上手,REPL模式是能够让数据分析的成本降低的,可以考虑支持一个快速导出导入脚本到REPL的能力,对于场景问题定位的话能够类似像批量跑场景自动化case一样,做到问题的快速归类和定位

@lijunfeng722
Copy link

idea的插件,生成的表达式 后面也会改成QLExpress 吗

@mzhiman
Copy link

mzhiman commented Jun 20, 2024

Arthas 较复杂的表达式体验确实有待优化, 引入脚本语言确实是一个好的idea,能解决一些痛点问题

@WangJi92
Copy link
Contributor

idea的插件,生成的表达式 后面也会改成QLExpress 吗

这个要看一下后续如何演进 ,ognl 和 QLExpress 应该都会支持,可能独立一个插件出来。

@DQinYuan
Copy link
Author

多步表达式的方式也是我一直赞成的方式,现有热门的脚本语言都有REPL,使用方式简单而且深入人心。

  • 为了不影响现有使用习惯,可以将类似subprocess作为函数引入,执行命令,subprocess.run(["watch", "demo.MathGame", "primeFactors", "-x", "5"], out=PIPE, text=True) 。其中PIPE可以是一个outputStream,这样不用考虑如何表达将watch的结果存起来。
  • 基于表达式和语句还可以做成jupter notebook的操作界面,编写自动化流程

我在正文中用的方式是类似 shell 的赋值语句:

n=${原生Arthas命令}

如果是类似 PIPE 的方式, 在命令行上要如何表达呢? 我理解类似管道命令? 但是在表达式场景下也需要绑定某个变量, 才能拿这个变量使用

@DQinYuan
Copy link
Author

Arthas和QLExpress的用户,看到了内部技术产品联合很兴奋!可以支持需求的快速迭代,这个效率是用外部工具比不了的。 对于用户说,定位排查要快速,数据分析要容易上手,REPL模式是能够让数据分析的成本降低的,可以考虑支持一个快速导出导入脚本到REPL的能力,对于场景问题定位的话能够类似像批量跑场景自动化case一样,做到问题的快速归类和定位

这个想法很好啊, 听起来是一个更加上层的诊断平台了

@hengyunabc
Copy link
Collaborator

感觉可以分开为两个事情来讨论:

  1. 在现在的基础上增加 QLExpress 支持
  2. Arthas 是否要考虑增加功能:表达式执行之后,把结果保存到某些 缓存/临时对象

QLExpress 支持

  1. 在 arthas 的表达式引擎里,增加 QLExpress 的实现。通过一个全局的配置,用options命令可以切换。默认的表达式还是用 ognl
  2. 收集真实的用户反馈,再决定是否把默认的表达式引擎 替换为 QLExpress

表达式执行之后,把结果保存到某些 缓存/临时对象

这个很久之前就有这个想法。但也有一些可能出问题的地方。

  • 比如对象是否线程安全的。比如调用对象的 函数,解析展示对象的某些 Feild

我建议是先实现第一个 QLExpress 支持,可以分多步来演进。

@DQinYuan
Copy link
Author

DQinYuan commented Jun 25, 2024

感觉可以分开为两个事情来讨论:

  1. 在现在的基础上增加 QLExpress 支持
  2. Arthas 是否要考虑增加功能:表达式执行之后,把结果保存到某些 缓存/临时对象

QLExpress 支持

  1. 在 arthas 的表达式引擎里,增加 QLExpress 的实现。通过一个全局的配置,用options命令可以切换。默认的表达式还是用 ognl
  2. 收集真实的用户反馈,再决定是否把默认的表达式引擎 替换为 QLExpress

表达式执行之后,把结果保存到某些 缓存/临时对象

这个很久之前就有这个想法。但也有一些可能出问题的地方。

  • 比如对象是否线程安全的。比如调用对象的 函数,解析展示对象的某些 Feild

我建议是先实现第一个 QLExpress 支持,可以分多步来演进。

@hengyunabc 多步演进的话,实现层面没啥问题。推广层面存在两个重大问题:

  1. 没有新功能吸引用户来尝试新表达式,大多数用户肯定不会去升级新版本,或者尝试新表达式。既然没有用户,那也不可能收集到反馈
  2. 缺少一个重大新功能更新一起运营宣传,吸引不了用户注意力,大多数用户都很难知道这个更新。也就很难有真实用户的反馈

多步演进也得考虑这两个推广问题,不然很容易流产。至少要有足够的运营素材,方便推广。这也是我建议把两件事一起做的原因。

@hengyunabc
Copy link
Collaborator

@hengyunabc 多步演进的话,实现层面没啥问题。推广层面存在两个重大问题:

  1. 没有新功能吸引用户来尝试新表达式,大多数用户肯定不会去升级新版本,或者尝试新表达式。既然没有用户,那也不可能收集到反馈
  2. 缺少一个重大新功能更新一起运营宣传,吸引不了用户注意力,大多数用户都很难知道这个更新。也就很难有真实用户的反馈

多步演进也得考虑这两个推广问题,不然很容易流产。至少要有足够的运营素材,方便推广。这也是我建议把两件事一起做的原因。

  1. arthas的首要目标是保持稳定。我相信好的功能用户会自发推广使用的,arthas本身便是如此。
  2. 推广和多步演进 并不冲突。
  3. ognl 在集成到 arthas 之后,也发现了不少问题。同样 QLExpress 应该也会遇到问题,我们需要保持足够的耐心来解决这些问题。

@DQinYuan
Copy link
Author

@hengyunabc 多步演进的话,实现层面没啥问题。推广层面存在两个重大问题:

  1. 没有新功能吸引用户来尝试新表达式,大多数用户肯定不会去升级新版本,或者尝试新表达式。既然没有用户,那也不可能收集到反馈
  2. 缺少一个重大新功能更新一起运营宣传,吸引不了用户注意力,大多数用户都很难知道这个更新。也就很难有真实用户的反馈

多步演进也得考虑这两个推广问题,不然很容易流产。至少要有足够的运营素材,方便推广。这也是我建议把两件事一起做的原因。

  1. arthas的首要目标是保持稳定。我相信好的功能用户会自发推广使用的,arthas本身便是如此。
  2. 推广和多步演进 并不冲突。
  3. ognl 在集成到 arthas 之后,也发现了不少问题。同样 QLExpress 应该也会遇到问题,我们需要保持足够的耐心来解决这些问题。

@hengyunabc

ognl 和 arthas 本身都是增量市场,不需要考虑推广问题。ognl 是 arthas 第一次集成表达式,属于新增功能,肯定会有用户尝试。QLE 做存量功能替换的话,用户动力没这么大。不会有用户为了新引擎,看文档,知道并且加这个选项的,第 1 步最大的可能没用户知道选项,然后没用户用,这是最大的可能。

大佬肯定也不希望这变成一个烂尾工程,如果要多步演进的话,避免烂尾,我觉得要考虑得更周到点:

  1. 增加 options 新选项,可能没有用户用和反馈,需要有这个预期。这个也是正常的,不影响我们下一步推进。
  2. 多步式作为 QLE 独占的新功能推进,可以成熟后,晚些上线。然后以这个为契机,会有更多用户尝试新表达式引擎。
  3. 然后再根据用户反馈,决定是否将默认的表达式也切成 QLE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants