今天碰到一个dubbo异步调用引起的bug,特记录下来,以供其他人参考。
现象
现有3个服务,关系如下,serviceA异步调用serviceB,serviceB同步调用serviceC。其中serviceB暴露出的接口为异步方式。表现的现象为,serviceB每次调用serviceC时,第一次的返回结果为null,后面几次调用时均能正常返回结果。
问题排查
项目中对于所有的dubbo调用均有记录日志,每次调用主要包含2条日志,CS和CR日志。CS为consumer send,开始调用时日志。CR为接收到provider的应答日志。
通过观察日志发现。调用serviceC的日志有不合理的地方。在正常的同步调用中,如有多次调用,日志现实顺序为CS,CR,CS,CR。实际出现的日志顺序为CS,CS,CR,CR。初步怀疑第一次返回为null是由于异步调用所致。
通过dubbo admin查看接口的配置信息,发现参数均无异常。并且在serviceC端的provider上观察到每次调用均有返回数据。所以排除了serviceC的provider的问题。
再次编写testcase单独测试serviceC服务,发现testcase返回数据均正常。也证实了serviceC服务没有问题。
最后通过询问是否有异步配置时,发现serviceB暴露的接口是异步方式。修改serviceB为同步方式,重新测试,发现问题解决。每次调用均正常接收到结果。调整serviceB为异步,问题重现。
最终确认了问题发生场景,即serviceB为异步服务,在serviceB里面同步调用serviceC,最终表现为serivceC的调用是异步,导致问题产生。
代码分析
dubbo的provider和consumer均由Invoker演变而来。并且都是AbstractInvoker的实现类。
AbstractInvoker
Map<String, String> context = RpcContext.getContext().getAttachments();
if (context != null) {
// A点
invocation.addAttachmentsIfAbsent(context);
}
// B点
if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)){
invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
}
DubboInvoker
// C点
boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
....
if (isAsync) {
ResponseFuture future = currentClient.request(inv, timeout) ;
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
// D点
return new RpcResult();
}
- 异步调用serviceB时,此时B点的判断为true,会在attachment中设置async的值为true,RpcContext中设置了async的值。
- 随后调用serviceB的实际方法。serviceB调用serviceC是,代码在A点,会将RpcContext的值设置到当前的invocation中
- 由于RpcContext是采用ThreadLocal保存的数据,并且在第一步时设置了async值,导致在2方法执行后,invocation中的async的值为true。
- 在进行serviceC的远程调用时,由于invocation中async值为true,导致C点判断为异步调用,在D点时new了一个RpcResult。所以在第一次调用serviceC时,返回结果为null。
- 第一次调用serviceC结束时,在consumer的filter chain中,有一个ConsumerContextFilter,在调用结束后会执行
RpcContext.getContext().clearAttachments()
方法,清除RpcContext中的信息,也就清除了async标识。 - 第二次调用serviceC时,由于RpcContext中没有了async标识,判断为同步调用,所以会正常返回结果。
解决方式
分析了问题产生的原因后,在不修改dubbo源码的情况,可以有一下几种处理方式。
1. 将serviceB改为同步调用,如果业务上确实需要异步调用,有以下2种处理方式
* serviceB的方法无需返回值,可采用oneway的方式
* 有返回值,并且需要异步,最简单的方式为在实现中使用线程池执行业务。
2. 增加一个Provider端的Filter,保证在filter链的结尾,在执行方法前,清除attachment中的async标志。也可达到同样的效果。