RSocket 基于消息传递的反应式应用层网络协议

RSocket 基于消息传递的反应式应用层网络协议

首先根据RSocket官网的副标题,RSocket的一句话定义是:

Application protocol providing Reactive Streams semantics

Reactive Manifesto中对Reactive System的定义是:

  • Responsive 只要有可能,系统就会及时地作出反应
  • Resilient 系统出现failure时仍然保持响应性,并能够独立恢复
  • Elastic 系统在不断变化的工作负载之下依然能保持即时响应性
  • Message Driven 异步非阻塞消息驱动架构

RSocket即是基于这些基本需求诞生的一种新型反应式网络协议。RSocket借鉴了很多其他协议发展过程中遇到的问题,然后总结归纳进自己的实践当中。

RSocket当前在工业界应用情况

  • Netflix:RSocket同样诞生于微服务老祖Netflix,同样它家出品的微服务框架Spring现在已经集成了RSocket支持响应式的微服务编程
  • Facebook:2017年上下开始在一些Facebook production案列中得到运用,今年开始了Thrit RPC和RSocket的集成工作。以后Facebook内部的Thrift会主要基于RSocket实现(于是Thrift也会支持streaming了)。
  • 阿里巴巴:集成Dubbo RPC和RSocket,在IOT中运用RSocket Broker
  • Netifi: 基于RSocket Broker做微服务的startup,(宣称基于RSocket broker的微服务架构比istio省钱10x)
  • 其他:Pivotal, LightBend(原名Typesafe,Scala背后的公司)

(PS:以上所有网上都是可以搜索到的)

今年下半年RSocket作为Reactive Foundation第一个项目,也算正式加入了Linux Foundation。以上三家巨头也都是Reactive Foundation的成员,总的来说,RSocket前景还是很不错的。

下面基于RSocket的一些主要特性分别做一下介绍,并和HTTP之类的常见协议进行比较:

  • Multiplexed, Binary Protocol 多路复用的二进制协议
  • Bidirectional Streaming 双向流
  • Flow Control 流控制
  • Socket Resumption 连接恢复
  • Message passing 消息传递模型
  • Transport independent 与传输层解耦的应用层协议

一、Multiplexed Binary Protocol

现在Multiplexing,Asynchronous,Non-blocking I/O已经被说烂了,基本上就是标配。没有这些连台面都上不了。这些特性意味着什么,拿HTTP的发展历史来比喻是最好不过(于是稍微扯远一下):

从HTTP1.0到HTTP3.0 在传输性能上的进步

  • 在HTTP1.0时代,每个HTTP request都要新建一个网络连接。网络连接不能复用
  • HTTP1.1时代,一个网络连接仍然在一个时候只能负责一个request,但是整个request/response结束后连接可以得到复用
  • 有一些文章讲到HTTP1.1的核心是pipeline功能,在实际运用中其实不然。pipelining支持一个TCP连接上按照顺序连续发送多个HTTP请求而不需要等待前一个请求的响应,但是它同时要求HTTP response也要按照请求的顺序逐个发送,这对服务器提出了很多要求,而且如果第一个响应很慢会拖累所有的后续响应(pipeling的队头阻塞),所以事实上并没有得到多少运用。即使到今天大部分浏览器仍然是默认关闭HTTP pipelining功能的,所以说HTTP1.1的主要突破还只是连接复用。
  • HTTP2.0是个飞跃,开始支持multiplexing,一个TCP连接上可以同时承载多个request/response,用这种方式替代1.1的pipelining提升HTTP的并行效果,也自然不存在什么队头阻塞了。每一个request/response的信息流,我们把它称作一个HTTP stream。这个时候一个HTTP client对于一个origin,只需要建立一个TCP就够了。(但是multiplexing带来了新的问题)
  • 现在HTTP3.0也差不多了。2.0解决了1.1pipelining的队头阻塞问题,但是却无法解决TCP本身的队头阻塞。而因为TCP/IP在内核协议栈中,简直无法升级,于是HTTP选择了QUIC作为新的传输层协议。 QUIC基于UDP,在用户模式中实现了类似TCP的connection oriented的功能同时解决TCP的队头阻塞,自带multiplexing等等。

所以,HTTP/2具有的优点,RSocket都有。另外,RSocket是一个二进制协议,也就是说在一个RSocket连接上传输的消息体对数据格式没有任何要求,应用程序可以为所欲为的压缩数据量的大小。

这样的二进制协议通常来说能给性能带来极大的提升,但是产生的代价是,网络中间件也会因为无法解读消息体中的数据,丧失了在对具体应用流量进行监控,日志和路由的能力。RSocket通过把每个消息体分成data和metadata的方式,在保证高效传输的前提下,提供了暴露少量元数据给网络中间件的能力

struct Payload {
    iobuf metadata;
    iobuf data;
};

对于每个data和metadata,应用可以采用不同的序列化方法。

  • data一般作为应用本身需要传递的业务数据,采取自定义的高效序列化方式,且对网络基础设施不可见
  • metadata可以采用网络基础设施一致默认的格式。在分布式传输的过程中,这些中间件可以按需求对metadata进行读写,然后监控应用健康状况或者调整路由。

二、Real Bidirectional Streaming

上面提到,HTTP这几年在传输性能上进步了很多。但说到底在应用层仍然仅支持client request/server response的交互模型。

这里一些同学可能有疑问,比如:

那HTTP/2推出的Server Push是什么

HTTP2.0推出了一个新的Server Push功能,但这个功能通常只是用来提前将一些静态资源返还给用户而已。举个例子:一个简单的网站有三个静态资源组成: index.html, index.css, index.js。 我们打开浏览器打开index.html,就会发起一个HTTP request拿到index.html。在不使用server push的情况下,我们要等浏览器解析出index.css 和 index.js之后才会再次向服务器发起请求。而运用server push,服务器可以根据一些规则预知到浏览器也需要index.css和index.js,并在客户端发送新的请求之前直接推送给该客户端。

所以,这个功能的使用场景非常有限,而且也不是一个真正双向的交互模式。

gRPC基于HTTP/2实现了双向流,那它是怎么实现的。

gRPC在HTTP/2 的body当中做了gRPC自己的framing处理。为了能够做到这一点,需要底层的HTTP接口能够暴露byte stream层的API,比如在gRPC client上如何接受stream response:

// Pseudo-code: parse gRPC frame from HTTP response body
struct HTTPResponseHandler {
    grpcCallback* cb;
    Buffer buffer;

    // http response header complete
    void onHeader(HTTPHeader header) {}

    // a new response body chunk arrives
    void onBody(Buffer payload) {
        buffer += payload;
        if (frame = extractGRPCFrame(buffer)) {
            cb->onNextResponse(frame);
        }
    }

    // end of HTTP response 
    void onEOM() {}
};

以上这是server streaming的处理。client streaming也很简单。只要不停的发送序列化后的gRPC frame,但就是不发EOM就可以了,server那边通过同样的方法把http request body转换成一个个gRPC request frame,直到收到客户端发出的EOM,代表请求体全部发送完毕。

所以说,gRPC之所以能实现双向流,是因为HTTP在传输层的确有这个概念,如果http library暴露了底层API那么可以通过做额外framing的方式在应用层实现双向流。如果http API将请求响应看作一个整体(HTTP语义本身),那么是无法实现应用层的双向流交互模式的。

结论:仅仅使用HTTP/2协议,不在其基础之上再加一层其他协议的情况下是无法在应用层实现双向流的。

回到交互模式,RSocket和gRPC在这点上非常类似,总共支持4种模式:

  • Request -> Response
  • Fire and Forget
  • Request -> Stream Response
  • Channel (bidirectional stream)

Channel和gRPC种的bidirectional streaming基本一致。通常来说,越复杂的交互模式,为了保存交互状态,就需要占用更多的内存和计算资源。这也是为什么RSocket会分成四种模式,提供四种不同的API而不是仅仅用最高级的Channel替代一切。

不过RSocket和gRPC有一个不同之处在于,当RSocket的client和server建立了长连接之后,任何一方都可以是Requester或是Responder。服务器也可以扮演Requester的角色,首先发起Request。这在基于HTTP/2的gRPC上是做不到的。

三、Application Layer Flow Control

系统流量不是一成不变的。在分布式应用当中会有各种各样的原因让单个节点的流量发生波动。如果生产者的生产速度过快导致消费者跟不上处理,可能会出现内存爆满cpu爆满,服务/客户端无响应等各种状况,如果故障隔离做的不好经常会因此产生cascading failure影响整个应用集群。为了避免这种状况,需要一套Flow control机制来协调生产者和消费者处理的速度。

TCP Flow Control

作为传输层协议,TCP自身就带有基于sliding window的流控实现。

TCP Sliding Window

TCP接收方会为建立的连接分配一个缓存空间MaxRcvBuffer。如上图Receiver中,应用在左边不断消费数据流,系统在有边不断接受新的数据流。蓝色的部分是Receiver全部已经收到的字节,因为数据包发送可能会乱序,所以他们未必是连贯的。在LastByteReadNextByteExpected之间的部分是已经确认收到并且保证顺序的信息流(但还没有被应用程序消费)。

那么此时Receiver能够继续接受的窗口大小即为 AdvertisedWindow = MaxRcvBuffer - (NextByteExpected - LastByteRead - 1) 对于Sender来说,同样的表述就是此时LastByteSent <= LastByteAcked + AdvertisedWindow

Receiver会间断性的向Sender汇报sliding window的状况,Sender根据Receiver发送的这些数据,计算出最多能够发送的字节数,从而不会overwhelm Sender.

总之,TCP自带Connection level的流控策略,且流控的对象是可发送字节数

HTTP/2 Flow Control

在第一个章节中我们提到过,HTTP/2采用了多路复用技术,即在同一个TCP连接上会同时存在多个HTTP stream。HTTP/2中加入了针对HTTP stream的同样基于字节流窗口大小的flow control。既然TCP已经在连接层实现了flow control,那HTTP/2的flow control的意义是什么?

TCP就好像是一个双向单车道。数据可以从双向发送,但各自方向都只有一条车道。TCP的flow control的意图就是控制每个方向上的单向流量,也就是单车道的流量。在HTTP1.1中,每个TCP连接上的HTTP request都顺序执行,不存在对连接资源的争抢问题。所以有连接层的流控就能达到和TCP一样的目的。

Multiplexed stream就像双向多车道的马路,每个方向都有多个车道共存。TCP的流控只对单方向的总流量负责,而因为总流量存在上限,那如果一条车道上的流量实在太多的话就会阻塞其他的车道。即每个TCP连接上并行的HTTP stream互相之间都存在竞争关系。如果不对每个HTTP stream进行流控,就可能出现其中一个stream占用了所有TCP链接资源从而阻塞了所有其他HTTP stream的情况。这时HTTP/2的性能甚至会比不上HTTP1.1,因为HTTP/1.1中新的HTTP请求可以直接使用其他TCP连接避免这种阻塞的发生。

RSocket 的应用层flow control

RSocket 同样是一个多路复用的协议,所以和HTTP2一样自然也需要stream level的流量控制。不过RSocket作为一个应用层协议,采取的并不是基于字节的网络层流控,而是基于应用层帧数的流量控制。

以 Request -> Stream Response 的交互模式为例,Requester可以通过给Responder发送credit的方式,告诉Responder能够发送的Response frame的数量。例:

// 发起一个request stream
requester->requestStream(request, callback);
// 发送给responder3个credit
requester->request(3)
...
// 收到一个response,credit: 2
// 收到一个response,credit: 1
// 收到一个response,credit: 0
// responder 发现没有credit了,触发相应应用逻辑,暂停计算新的response
...
// requester觉得自己没啥问题,再发送3个credit
requester->request(3)
...

在传输层像TCP/HTTP这样的基于字节流的流控方案,保证了系统网络模块的基本稳定,但是并不能从根本上终止新的响应数据的产生。说到底,只是把风险从系统层转移到了应用层。RSocket控制的不是网络层的Sender能够发送的字节数,而是能够接受的应用层的Producer应当继续产生的消息体的数量,可以从根本上杜绝压力的产生

想象一个静态video streaming的应用,客户端通常不会一步到位完成整个video的加载,而是根据用户观看的进度(消费者消费数据的速度),来决定是否需要服务端继续发送后面的数据。

以上说的是requester对responder进行的流控,是基于ReactiveStream的语义的。RSocket同样还支持responder对requester请求速率的控制,称作Lease。Lease可以控制Requester在接下来一段时间(TTL)内发出的请求数上限(Number of Requests)。

因为RSocket是一个应用层协议,它还需要一个底层传输协议,所以通常一个完整的RSocket实现当中同时存在应用层和传输层的流控功能。End-to-End Arguments in System design 这篇经典文章中探讨了应用和基础设施在功能上的界限的哲学。文章认为,很多底层基础设施中提供的功能通常都需要应用层的帮助才能被完全正确的实现,我认为这里提到的应用层Flow Control的设计及也是一个很好的例子。

四、Socket Resumption

在我看来,连接状态恢复(Resumption)才是RSocket最有意思的一个功能。

RSocket的设计充分考虑到了网络环境的不稳定性,为了做到对网络故障的Resilience(Reactive System的特性之一),RSocket支持connection resumption功能。从用户API的角度来说,RSocket想达到以下的效果:

  • 连接恢复成功
RSocketStream stream = client.requestStream(callback);
触发 callback->onNext(); // client 收到一个response,
// 连接中断。。。
// 重新建立底层连接,RSocket 层连接状态恢复成功
触发 callback->onNext(); // client 继续收到下一个response
  • 连接恢复失败
RSocketStream stream = client.requestStream(callback);
触发 callback->onNext(); // client 收到一个response
// 连接中断。。。
// 重建底层连接失败 或者 成功重建底层连接但RSocket层连接状态恢复失败 (client/server状态机不同步)
触发 callback->onError();

若Connection Resumption成功,用户不会有任何感知。RSocket成功的将这些细节隐藏在了其实现内部。若Connection Resumption失败,则触发onError回调函数。这样提供了两个好处:

  • 自动故障恢复,应用层stream对象能够保持响应
  • 减少需要重传的数据量。

Trade-off: Memory VS Networking

如果使用的传统的通讯手段比如RPC和HTTP,我们一般通过不断的重传请求来应对应用层/网络层的各种故障。对于HTTP/2这样的多路复用协议来说,如果一个连接发生了中断,连接上的多个stream都会受到影响,而我们则需要对每个stream分别重发HTTP request。这个问题在HTTP/2之前还不算严重因为通常每个连接同时只处理一个HTTP请求,而在多路复用的协议中重传整个TCP连接上的请求意味着剧增的网络压力,尤其是在mobile,IOT场景或者网络基础设施不发达的国家和地区。如果在重新建立网络连接后不用从头开始处理所有的请求,而只是进行一些基于增量的状态机同步,那么对于网络传输是一个很大的优化。

RSocket的Socket Resumption就是基于这个逻辑设计的。RSocket要求支持resumption功能的client和server需要能够在连接中断后的一个时间段(TTL)内继续保存connection及connection上所有stream的状态。再重新建立底层可靠连接(比如TCP)之后,client会发送一个包含连接状态机的基本信息的RESUME帧给服务器,服务器负责判断是否可以恢复连接(返回RESUME_OK或者REJECTED_RESUME)。若成功复连则该连接上的所有stream可以继续工作。

所以,如何选择一个合适的连接状态的TTL就变得尤为重要。如果TTL过长,则客户端和服务器会积累大量的内存,尤其是高并发的服务器。

另外如果client和server之间存在其他的网络中间件,那么连接恢复的成功率还会受到中间件路由策略的影响。如果client发起的新的连接被中间件匹配了另外一个服务节点,Resumption就是必然失败的。 (在我们的实际运用中,端到端连接恢复的成功率还是比较高的)

(具体Resumption过程中每一帧是怎么操作的:rsocket.io/docs/Protoco)

五、Async Message Passing Model

RSocket还有另外一个非常重要的概念使之完全区分于类HTTP协议,那就是异步消息传递。

不同于HTTP当中存在Request,Response,etc..,RSocket在网络传输上只有Frame这一个消息格式。RSocket-cpp: Frame.h该文件定义了RSocket支持的各种不同Frame,这些Frame囊括了整个RSocket协议。另外一个不同点在于,类HTTP协议通常拥有一个显式的destination (URL),而RSocket并并没有这样的要求,这也和他Message Passing的特性相吻合。

如果Requester的RSocket消息R首先通过了一个网络中间件,那么请求者(Requester)并不关心该消息的最终目的地在哪里,网络中间件可以全权负责路由模块的实现(比如使用之前提到的Frame_Payload中的metadata)。这就是Netifi和Alibaba都在做的RSocket Broker模式。前者用该架构去支持微服务,后者用它去支持IOT场景。

RSocket Broker

这种架构很有意思,不仅能在微服务中加入streaming支持,还有如下特点:

1.客户端也可以暴露服务

由于RSocket的Bidirectional streaming特性,和Broker建立连接的客户端即可以做Requester也可以做Responder,比如图中的Device A和Device B虽然是移动终端或者IOT设备,但仍然可以向其他设备或数据中心的主机提供服务

2.自动服务注册/发现

RSocket在成功建立连接之后client会首先发给服务器一个setup frame。如果该client想要暴露一个服务,就可以在setup frame中填写该服务的定义。连接成功建立之后,RSocket Broker可以直接通过记录网络连接状况来达到服务注册和发现的效果。

3.基于消息Metadata实现请求路由

前面说过,RSocket是一个二进制协议,但仍然可以在消息体中通过metadata来暴露信息和网络中间件。这里RSocket Broker就可以通过读取消息的metadata 实现微服务的请求路由

4.暴露服务不需要Ip和Port

整个架构当中,所有节点都只需要知道Broker的地址和服务端口即可。只要成功连接信息流就是双向的。Borker可以直接通过建立的Connection寻址服务节点,所有的服务调用者都不需要知道服务暴露方的地址

5.天生的中心化管理

管理微服务集群往往需要有一个中心化的控制中心,在这个架构中RSocket Broker就是自然而然的中心,知道整个集群中所有的情况。

不过对于这样的架构,如何做到RSocket Broker这个有状态集群的水平扩展才应该是其难点。

RSocket vs MQTT vs Kafka

当发现RSocket还有这样的运用场景之后,我们会马上联想到MQTT和Kafka。

过去一段时间对MQTT的接触也比较多。很多同学看到IOT可能马上就会联想到MQTT的pub/sub模式,的确这里面有很多相似的地方。

不过RSocket和MQTT有两个根本的不同:

1. QOS

QoS(Quality of Service)等级定义了Message delivery guarantee的严格程度。在MQTT中分为3个等级。Qos=0最多一次,就是FireAndForget;Qos=1最少一次,就是要等待接受方确认收到不然重来;Qos=2正好一次,需要多次交互。在RSocket中并没有实现QoS的概念,而是把这是否有实现的必要的判断交给应用开发者。如果需要,同样可以基于Metadata消息来实现。

2. Flow Control

MQTT协议不存在自己的Flow Control实现仅仅只是依赖底层的TCP协议在连接层做基于字节的流控,非常基本。在流量高峰期,MQTT 消费者和MQTT Broker经常会压力山大。同时因为通常MQTT会采用QoS=1,即MQTT broker需要把每个消息保存在内存中直到确认接收方收到为止,若本身处于流量高峰,而且单个消息体的体积又比较大,后果不堪设想。RSocket则完全不会出现这类问题。

那Kafka呢? Kafka这样的消息队列系统确实能够做到削峰,但是他并不能降低生产者生产新事件的速率。在极端的场景中,突然爆炸的流量仍然会突破写队列的极限。除此之外,kafka自身的一些缺陷比如对topic数量的限制,,在RSocket中也不会遇到类似的问题。

六、Transport Agnostic, Application Protocol

最后忘了提一点,RSocket是一个应用层网络协议,并不依赖于底层传输层的协议(只要它是面向连接的,保证delivery order的即可,比如pure UDP就不行)。所以可以根据不同场景和设备选择不同的协议,e.g:

  • 在数据中心中可以使用RSocket on TCP
  • 在浏览器JS中可以使用RSocket on WebSocket (现在用WebAssembly也可以基于其他实现了吧)
  • 要和HTTP网络中间件保持兼容可以用RSocket on HTTP/2
  • 也可以基于QUIC。(从特性匹配上看QUIC很适合RSocket)

总之花样很多。更重要的是,不管你怎么改变传输层协议,对于应用开发者来说都没差。基于ReactiveStream semantic的API是不会变的,用户只需要在create client/server的时候换一个transport类即可。

就先这样吧,欢迎补充指正讨论。

发布于 01-01

文章被以下专栏收录