在 QCon2019 北京大会上第一次得知 RSocket。印象深刻的是 Netifi 公司通过他们研发的 RSocket 帮助企业实现微服务,在 40,000RPS 的场景下,Istio 需要每月 3495 美金,而 Netifi 每月只要 388 美金,同时性能提升 10 倍,这无疑对任何企业都是极具吸引力的。
Netiffi 的创始人在会上也谈到,使用 Netiffi 的 Broker,得益于 RSocket 协议,无需独立部署监控、服务发现、健康检查、负载均衡等等中间件。如果是跨云部署,例如谷歌云与亚马逊云之间,或者亚马逊云与企业本地数据中心,都只要通过 Netiffi 的 Broker 即可无缝沟通,无需处理复杂的适配问题。
回来查询了不算丰富的资料后发现,Istio 的技术专家发文称 RSocket Broker 的 service mesh 比 Istio 有将近 10 倍的速度提升。考虑到 Istio 专家的观点还有一定说服力的,那么 RSocket 真的有那么厉害?
我们决定到生产上面去实践 RSocket,看看性能到底如何。现在已经支持 RSocket 的 service 框架有 Spring Flux:
Dubbo3.0 snapshot
压测对比的是 Dubbo2.7。Dubbo2.7 的样例代码如下:
Dubbo3 的样例代码如下:
SpringFlux 的样例代码如下:
分别压测 QPS200,400 和 800 的情况,结果显示 CPU、内存和响应时间基本一致。是不是压测方式有问题,为什么性能一点也没有提高?
在解答上面的问题之前,我们先来看看 RSocket 到底是什么?
官方定义:RSocket 是基于 reactive stream flow control 的双向的、多路的、基于消息的、二进制通讯协议。它提供了 4 种交互模式:
几个特点:
上面只是定义了 RSocket 协议,在具体的实现上面是非常灵活的。
进一步查询资料后发现,现有 JAVA 的 RSocket 实现一般都是基于 TCP 长连接。熟悉 Dubbo 的朋友,立刻就会想到 Dubbo 不就是基于 TCP 长连接进行服务调用的么。是的,但是不同之处在于 RSocket 是一系列的协议规范,原先的 Dubbo 虽然也是基于 TCP 长连接实现的,但是并没有完全按照 RSocket 的规范来进行实现。
更加确切的来说,那个时候应该还没有 RSocket。这个也就帮助我们理解为什么 Dubbo3 开始接入 RSocket,以及阿里为什么也是 RSocket 的拥护者之一。至此就能理解为什么性能没提高了,在我们实践的场景中,只是把原来基于 HTTP 的请求方式变成了基于 TCP 的实现。就生产结果而言,并没有性能大幅提升,更别提 10 倍的提升。
那 RSocket 只有 TCP 长连接的优势?
作为一名一线业务开发者,可能更关心的是使用 RSocket 协议写业务代码时的优劣势。就我个人而言,感觉还是很棒的。例如下面这个传入参数为 Mono,返回也为 Mono 类型的接口定义方式。
熟悉响应式编程的同学应该知道 Mono 是 Pivotal Reactor Core 中的一种类型。是一种特殊的发布者,最多只发布一次。
如果应用本身就是以非堵塞的方式写的,那这里就可以直接使用 reactor core 的所有 API。当然也有同学会说,即使不返回 Mono,例如 Dubbo2.7 中的返回 CompletableFuture,我们只要自己内部转换一下即可。然后应用里面照样可以使用 reactor core 或者 rxjava 等响应式编程的框架。
的确如此,但是如果是 Flux 呢?可以多次、不断地往流里面写入结果的呢?CompletableFuture 还能支持么?显然不行。因为 flux 本身就定义为最多可以发布 n 次。
那为什么要用 flux?
flux 是响应式编程中的一种常用对象,其实就是 request -> stream 一种实现。一个请求发起后,结果可以分批写回。好处是什么?例如:A 服务调用 B 服务,B 服务调用 C 和 D 服务,但是 D 服务很慢,如果是 request -> response 模式,那必须要等到 C 和 D 完成后,才能返回结果给 A。如果是 Request -> stream 模式,则可以先把 C 的结果返回给 A,然后等 D 的结果拿到了,在返回给 A。这样就可以高效的利用系统资源,减少等待。
熟悉 Dubbo 的同学可能会说,request -> stream 这种模式也不是 RSocket 独有的吧,例如 Dubbo 就可以使用下面的方式来实现:
看完上面的代码,然后我们可以思考一下如何用上面提供的 API 去实现下面的功能。你就会发现 flux 的这种 Reactive Functional Programming 的编程方式大大降低了编程的难度与代码量,提升了代码的可读性:
但是这还不是全部的好处,下面我们来看看 RSocket 的另外一种使用场景。
响应式编程中有一个比较有名的功能叫背压。例如:当上游服务调用下游服务,而下游服务来不及处理的时候,可以选择性的限制上游服务的调用。
而我们日常在刷手机的时候,经常会由于手机卡顿,无论是 APP 导致的还是网络导致的,重复点击或者刷新页面的情况。而 HTTP 本身是无状态的,所以只要有请求,无论是有效的还是无效的,服务器都会进行处理直到完成。
但是如果有背压,那我们就可以一定程度上减少 APP 的无效和重复的请求。
例如:用户查看订单列表,如果一下子过来 10 个请求,其实只要返回最后一个即可,前面 9 个都可以忽略。如果实现了,服务器就会减少流量,对硬件成本的控制有着非常积极的作用。InfoQ 的文章中就提到:Facebook 的工程师现在就是这样实现 APP 与服务器之间的通讯。
之所以称之为展望,因为这个也是我们下一个实践的目标。
刘诚,携程酒店研发性能架构师。2014 年加入携程,致力于通过架构的演进,控制企业硬件成本。
https://mp.weixin.qq.com/s/lk4iG_ayC3sYVbkgz72ZEg