相关文章推荐
坚强的香蕉  ·  SQL Server Compact ...·  11 月前    · 
忐忑的感冒药  ·  java - Closing JPA ...·  1 年前    · 
憨厚的熊猫  ·  SQL ...·  1 年前    · 

在日常的开发实践中,经常需要跟同部门、不同部门的小伙伴进行dubbo接口联调、排查问题等,尤其是目前用得比较多的资源挂靠类接口,因为其中涉及到同步、审核、上下架等问题,所以经常出现一顿操作猛如虎,一看接口啥都无的情况.

此情此景,我们需要怎么告诉被调用方我们的参数及返回值呢?又能怎样多次触发呢?

二、现有方案

1.手动再次触发或者等待用户触发

容易理解,手动触发请求可能是点个按钮、也可能是一系列操作,有可能是在网页端、也有可能是安卓端等,还是比较麻烦的;而等待用户触发就更看运气了,因为往往跟参数相关,如果不是热门接口,可能等个十几分钟也没有想要的参数进来,就比较难搞.

2.先用 Arthas tt 命令记录,再回放

用了 tt 命令之后,只需要触发一次,不管是手动触发还是用户触发,只要记录成功了,那么就能够直接回放请求.还是非常方便的.

Arthas的tt命令教程

3.使用 Arthas vmtool 命令触发

有时候往往事不遂人愿,不管是手动触发、还是等待用户触发都比较艰难的情况下(如:一个超长 xxl-job 脚本,可能要半个钟才能执行到你想要的参数),那么就需要用到 vmtool 命令了,理论上配合好序列化,能调用绝大多数方法了!

Arthas的vmtool命令教程

三、有没有更好的办法呢?

如上,现有的3种方案都是需要在 consumer 端执行的,并且具体 dubbo 调用到哪台机子也不确定,所以排查起来还是挺不方便的!还有没有其他方法呢?

1. dubbo 有哪些常用的调用方法呢?

a.在 springboot 服务中配合 @Reference 注解调用

最常见的调用方式,不多做介绍

b.泛化调用

可以在没有事先引入接口依赖的时候进行调用,但是也需要进行编码

网上有很多相关解释: dubbo高级用法之泛化与接口自适应

c. telnet+invoke 命令调用 (最适合)

只要能telnet进对应的容器,就能进行调用,其形如:
telnet 127.0.0.1 20880
invoke com.seewo.xxxApi.query("abc", 123)

2. telnet+invoke 命令在联调、复现场景下的优势

为什么说 telnet+invoke 最适合呢?

a.参数是基础类型或者 json ,易看、容易理解

当参数是基本类型的时候,那么肉眼看到的就是基本类型, 当参数是一个对象的时候,可见的是一个 json ,如:

invoke com.seewo.xxxApi.query({"relation":1,"relationUids":["pyzpghluunpqmlcflmwhzrll7735m261"],"class":"com.seewo.api.XxxDto"})

其中参数就是一个 XxxDto 对象:

"relation": 1, "relationUids": [ "pyzpghluunpqmlcflmwhzrll7735m261" "class": "com.seewo.api.XxxDto"

b.由provider方自行触发!

这是一个非常非常重要的地方,因为整个流程就可以减少consumer方的参与,不用一直守着配合provider方进行无止境的触发.

c.锦上添花:可以指定ip

基本在所有的调用问题排查里边,我们都希望请求可以进入指定的机子,这样排查起来会极为方便,dubbo也是如此! 而在使用 telnet+invoke 进行调用时,就是可以如此指定ipport!

四、你说的这种方法(telnet+invoke),有可能实现吗?

回顾 telnet+invoke 的三要素:

  • ipport
  • 接口名及方法名
  • 序列化后的参数
  • 1.对于参数比较简单的接口

    可以直接手撸即可,具体的ipport则让provider方自己补充

    2.对于复杂参数的接口

    这个场景我们选择方式是手动触发一次接口,然后使用Arthaswatch命令来生成 telnet+invoke

    a.首先,需要的信息在服务中都是存在的

    三要素中所有的信息在consumer方的服务中都是能够找到的

    b.其次,需要的信息都是在观测接口的上下文中都是能拿到的

    在我们使用Arthas对对应的dubbo接口进行观测时,除了ipport无法获取到准确的以外,其余信息都是可轻易获取的,具体见后文实现部分!

    五、具体怎么实现呢?

    回顾 Arthaswatch 命令表达式支持关键参数:

  • target : 当前方法的对象,相当于方法内部的this
  • clazz : 当前方法的类
  • method : 当前方法
  • params : 方法参数
  • 1.ipport

    dubbo生成的代理类中,是包含了一些额外的信息的,经过debug可以找到,其可达路径为: target.handler.invoker.directory.urlInvokerMap.values().toArray()[0].providerUrl 其中target即为dubbo的生成的代理类对象

    2.接口名及方法名

    这个可以理解为前置的信息了,因为我们watch的时候,就是以这两个作为入参数的! 并且dubbo生成代理类的时候,是会实现对应接口的,所以可达路径为:
    #clzName=clazz.getInterfaces().{^ !#this.toString().contains("com.alibaba")}[0].getName()
    其中 clazzwatch 实际观测的类
    #medName=method.methodName, 其中 clazzwatch 实际观测的方法

    3.参数及其序列化

    watch命令可以获取到方法的参数,但是要怎么将其进行序列化呢? 答案是:com.alibaba.dubbo.common.utils.PojoUtils#generalize, dubbo的依赖中已经提供了对应的工具类,调用即可!

    六、问题及解决

    1.转义问题

    转义问题,我们生成命令时选择用单引号作为分界,那么在调用的参数中含有单引号的时候,就会出现转义问题,所以需要手动进行转义.

    2.序列化工具类未加载问题

    工具类未加载问题,一般情况下,如果是没有使用这种方式调用过的话,jvm是不会加载这个类的,需要我们手动触发一下.
    vmtool --action getInstances --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --className org.springframework.context.ApplicationContext --express '@com.alibaba.dubbo.common.utils.PojoUtils@generalize(1)'

    3.使用vmtool命令作为触发点时的类加载器问题

    因为vmtool命令默认使用的类加载器与dubbo中的存在不一致的情况,所以使用时需要手动指定:

    vmtool --action getInstances \
    --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader \
    --className com.seewo.XxxService \
    --express '#clzLoader=@org.springframework.context.ApplicationContext@class.getClassLoader(),@java.lang.Thread@currentThread().setContextClassLoader(#clzLoader),instances[0].callXxxApi("5a0b676379af11e8a10c1c1b0d1c49aa")'
    

    观测表达式: (后面可以接条件表达式)

    watch com.seewo.XxxApi query \
    @com.alibaba.dubbo.common.utils.PojoUtils@generalize(1),
    #clzName=clazz.getInterfaces().{^ !#this.toString().contains("com.alibaba")}[0].getName(),
    #medName=method.methodName,
    #pams=@com.alibaba.fastjson.JSON@toJSONString(@com.alibaba.dubbo.common.utils.PojoUtils@generalize(params).{#this}).replaceAll("\\u005B(.*)]","$1").replace("'"'"'","'"'"'\"'"'"'\"'"'"'"),
    #invokeCmd=@java.lang.String@format("invoke %s.%s(%s)",#clzName,#medName,#pams),
    #url=target.handler.invoker.directory.urlInvokerMap.values().toArray()[0].providerUrl,#providerIp=#url.host,#providerPort=#url.port,
    @java.lang.String@format("{ echo '"'"%s"'"'; sleep 3;} | telnet %s %s",#invokeCmd,#providerIp,#providerPort)
    

    输出结果实例: telnet+invoke 命令 (具体ip可以自行修改)

    { echo 'invoke com.seewo.XxxApi.query("pyzpghluunpqmlcflmwhzrll7735m261")'; sleep 3;} | telnet 172.1.2.34 20880
    

    provider方自行调用 telnet+invoke 命令
    相当于普通的shell命令

    欢迎交流!

    分类:
    后端