微信号:QunarTL

介绍:Qunar技术沙龙是去哪儿网工程师小伙伴以及业界小伙伴们的学习交流平台.我们会分享Qunar和业界最前沿的热门技术趋势和话题;为中高端技术同学提供一个自由的技术交流和学习分享平台.

在Dubbo中实现更简单易用的异步

2016-04-28 09:33 余昭辉

Dubbo是Alibaba开源的一款很棒的SOA框架,被国内很多公司所采用。去哪儿网也早在2011年就开始采用Dubbo作为我们服务化拆分的基础,并且在Dubbo停止维护后自己fork了分支继续维护,也在Dubbo上投入了大量的精力。

作为服务拆分的基础设施,随着调用链路日趋复杂化,异步调用越来越重要了。

Dubbo默认配置下采用的是同步调用方式。也就是consumer调用一个服务的时候,当前线程会block住,block的线程将不能干其他事情。而线程是昂贵而有限的资源,比如在链路中consumer往往又是其他服务的provider: a -> b -> c。如果这个时候c因为某种原因响应时间恶化,使用同步调用的b将会受到严重的影响,最坏的情况下可能将b的服务线程耗光,最后b拒绝服务,进而对a造成影响。而如果c是一个非关键服务,而b提供的是关键服务,这个时候你肯定后悔莫及。而如果b是异步去调用c就不同了,b发起异步调用之后调用线程即可释放,可以继续处理别的事情了,这样如果是非关键服务可以干脆不用关心它的返回结果了。

上面描述的是consumer异步调用的情况,而provider也有异步处理的场景,比如上面的b如果采用异步调用c,那么如果c有返回结果了,什么时候将结果返回给a呢?Dubbo默认配置是同步等待provider的处理结果,然后将其写回。但是provider端如果是一个异步调用则无法等待结果了,因为发起异步调用后hold住连接的线程就离开了。所以provider端也需要一种机制,处理结果完成时将结果返回。

为了区分上面的两种情况我将其称为consumer端异步和provider端异步。

Dubbo默认也提供了『多种多样』的异步方式,但个人觉得这些异步方式略显混乱和难用。下面将简单的过一下Dubbo现有的一些异步方式。

consumer端异步

1. 使用Future的方式

a. 先要将该服务配置成异步调用

这也是我不喜欢的地方之一。是否异步调用应该是根据当前场景做出的选择,一个服务可能在有的地方使用同步调用,而在另外一些地方使用异步。而这种配置死的方式就很麻烦了,如果有两种用途还需要配置两个。

b. 调用


一直觉得上面的API很反人类。service.longTask()是实际调用的地方,因为是异步调用你不能从这里拿返回结果,所以就孤孤单单的留在这里。还有上面的ResponseCallback,你千万别被那个caught所迷惑,这里只有超时以及框架异常才会调用,而业务异常是不会调用的,业务异常也是走done。而done里的response也不是你期望的返回结果,而是Dubbo里一个Result对象。


2. 配置回调的方式


第二种方式就不详细介绍了,就是异步调用结果返回的时候会调用callback对象的onResult方法。

上面就是Dubbo原有的consumer端异步的方式,然后我们再来看看provider端异步。

provider端异步

如果说consumer端异步的实现方式不友好,那么provider异步的方式就更加繁琐了,繁琐到你用一次就需要查一次文档。

首先如果要使用provider端异步,那么服务的API就需要做出改变。比如你原有的服务接口是:

String sayHello(String name);

那么你需要修改为:

void sayHello(String name, ResultListener listener);

这个ResultListener是你自定义的一个接口,而不是真正的参数。然后对服务的配置要做进一步修改:


这上面的参数还有一些坑,懒得介绍了。

然后consumer调用的时候:


provider端的实现逻辑:


这种方式,每次使用都很不舒服。首先我觉得provider是否异步并不关consumer的事情,不应该对consumer产生任何影响。比如我有个抓取服务,原来使用的是同步的httpclient去访问别的系统,然后有一天我修改为异步httpclient,那我是否要consumer协助我一起修改呢?

以上就是Dubbo的provider异步了。

有了这么多痛点,我们决定重构一下Dubbo的异步实现方式。


新的异步

consumer端异步

实现一套新的API,我还是喜欢从上而下,也就是先写出我喜欢的调用方式,然后再去想想如何去实现:


我希望consumer端异步最好就这样了,而且不用任何预先配置,我想异步就异步,想同步就同步。这样一个API还有一个好处是可以和现有的工具很好的结合,比如google Guava里一整套针对ListenableFuture的处理类。

但是,sayHello的原始签名是这样的: String sayHello(String name)。怎么让它返回一个Future呢?难道每个API都要写两个版本么?写两个版本那provider如何去实现呢?

我们在前文已经说过了,consumer端的异步和provider端并无关系,只是consumer在调用的时候决定是否等待。那么我们现在我们要做的就是怎么弄出这样一个API出来,然后在调用的时候里面偷偷的异步去调用provider。

我们的实现方式是使用java的annotation processor,你只需要在你原有的API上添加这么一个注解:


然后我们实现一个annotation processor自动给你生成一个这个接口的实现接口:


这个接口是自动生成的,provider提供这个接口的实现,甚至不用关心这个接口的存在。然后consumer端使用的时候就可以直接用这个接口了:




因为这个接口实现了同步版本的接口,所以即可以调用异步,也可以调用同步。这样一来,API的变通就搞定了,实现起来也很简单。Dubbo是通过字节码生成的方式来生成网络代理,进行rpc调用的。所以给异步的方法生成的代理只要是异步调用就行了,然后其实里面实际调用的还是同步版本的接口,只不过调用方式变了,就是调用的时候不去wait,而是返回一个Future。而Dubbo里只是用API去做服务发现,去找到provider的地址列表,所以在服务发现的时候我们从AsyncImpl里取出对应的同步接口去找provider列表。

这个的方式是不是比Dubbo原生的方式漂亮多了呢?遵循习惯的模式,API所见即所得。解决了consumer端异步,我们再来看看provider端异步吧。


provider端异步

其实provider端异步也已经有可参照的API了: servlet 3.0里的async servlet。类似下面的方式(伪代码):


那么我们就可以模仿这个对Dubbo的API做一下改进:


这样的API对consumer完全是透明的,consumer可以同步调用这个方法,也可以异步调用这个方法。而Dubbo内部的改造也很简单。其实AsyncContext就像是一个ListenableFuture,当执行完provider的逻辑后,给这个Future注册一个回调,在回调里将结果写回consumer即可(伪代码):


OK,这就是我们改造后的Dubbo异步版本API了,其实对Dubbo本身的改造才几行代码,很容易就实现了。

实际上,在设计API的时候,我们应该更多的从使用方考虑,先将使用API的demo写出来,然后再思考实现。另外,实现API的时候尽量遵循惯用法,可以看看有没有其他的项目里已有这种做法,特别是那些广为人知的项目,比如这里的guava和servlet 3.0之类的。


长按识别二维码,浏览器打开

秒下大讲堂在线课程APP

支持Android和IOS

Qunar最新最热门的在线视频课程尽在其中!


 
Qunar技术沙龙 更多文章 CTO寄语应届生 高效能人士的七个习惯 百度员工离职总结:如何做个靠谱好员工? 学不进去?!去看看大脑怎么想 一个故事教你轻松学会课程设计
猜您喜欢 App项目实战之路(三):原型篇 为什么我们选择Docker来构建Crayon的数据处理平台 Android 下午茶:Hack Retrofit 之 增强参数 Android面试二三事儿 程序员都不读书,但你应该读