在日常的开发实践中,经常需要跟同部门、不同部门的小伙伴进行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
进行调用时,就是可以如此指定ip
及port
!
四、你说的这种方法(telnet+invoke
),有可能实现吗?
回顾 telnet+invoke
的三要素:
ip
及port
接口名及方法名
序列化后的参数
1.对于参数比较简单的接口
可以直接手撸即可,具体的ip
及port
则让provider
方自己补充
2.对于复杂参数的接口
这个场景我们选择方式是手动触发一次接口,然后使用Arthas
的watch
命令来生成 telnet+invoke
a.首先,需要的信息在服务中都是存在的
三要素中所有的信息在consumer
方的服务中都是能够找到的
b.其次,需要的信息都是在观测接口的上下文中都是能拿到的
在我们使用Arthas
对对应的dubbo
接口进行观测时,除了ip
及port
无法获取到准确的以外,其余信息都是可轻易获取的,具体见后文实现部分!
五、具体怎么实现呢?
回顾 Arthas
的 watch
命令表达式支持关键参数:
target
: 当前方法的对象,相当于方法内部的this
clazz
: 当前方法的类
method
: 当前方法
params
: 方法参数
1.ip
及port
在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()
其中 clazz
为 watch
实际观测的类
#medName=method.methodName,
其中 clazz
为 watch
实际观测的方法
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命令
欢迎交流!