导读:GDP (Go Develop Platform)是百度内使用的 RPC 框架,具备完善的 RPC Client 和 RPC Server 能力,可以用来开发 API、Web 及后端服务等各种应用。GDP Streaming RPC 是基于 GDP RPC 能力开发的流式 RPC 框架,在实现功能基础上设计的一套面向流传输场景的传输框架,提供了流式传输应用场景的方案。百度内使用流式 RPC 方案首选为 baidu-rpc (开源项目为 brpc)streaming,GDP streaming 是 brpc streaming 的 Go 版本,为 Go 的开发者提供的流接口方案。
全文4700字,预计阅读时间12分钟
一、streaming 介绍
1.1 解决的问题
在一些数据传输场景中, client / server 需要向对方发送&接收大量有序数据,这些数据非常大或者持续地在产生以至于无法放在一个 RPC 的消息体中。比如:分布式系统不同节点间传递的副本(replica) 或者语音数据。一个订单导出接口有 10 万条记录,如果使用传统 rpc,那么需要一次性接收到10万记录才能进行下一步的操作。
如果我们使用streaming rpc那么我们就可以接收一条记录处理一条记录,直到所以的数据传输完毕。client / server 虽然可以通过多次RPC把数据切分后传输过去,但存在如下问题:
- 如果并行发送,无法保证接收端有序地收到数据,拼接数据的逻辑相当复杂。
- 如果串行发送,每次传递都得等待一次网络RTT + 处理数据的延时,特别是后者的延时可能是难以预估的。
RPC 框架常见的通信方式有 简单 RPC、服务端流式 RPC、客户端流式 RPC、双向流式 RPC。它们主要的特点是:
简单 RPC:传入一个请求,返回一个响应。
服务端流式 RPC:客户端传入一个请求,服务端可以持续的返回多个响应,一个典型的例子是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。
客户端流式 RPC:客户端传入多个请求,服务端返回一个结束的响应,典型的例子是接收并处理日志文件。
双向流式 RPC结合前两者RPC的特点,双端都可以传入多个请求,返回多个响应。
streaming 交互模型就是为了让大块数据以流水线的方式在 client / server 之间传递。实现的是双向流式 RPC。
1.2 设计目标
- 接收消息的顺序和发送消息的顺序一致,不同流消息并行
因为 brpc 的流式框架在公司内外已经广泛使用,GDP 在落地的实现首选是协议和功能对齐 brpc,目标是实现 brpc 的 Go 版本。一个传输任务可以不是由一个流来完成的,并行处理让效率更高。所有框架需要保证收发消息的顺序以及一致性和可并行特性。很多场景中,收发的数据可能是其他协议服务传入或者转发到其他协议的服务中,这样我们考虑实现自定义的序列化和反序列化方式,可以降低使用成本。对于 IO 密集型的场景,tcp 的连接开销是很大的,我们经常会遇到一个 stocket 连接把网口打满的情况。设计上一个 stocket 可以建立多个流,这个方案使得流的管理逻辑变得复杂,但是却可以做到节省连接的开销。也就是流的建立销毁的成本很小。二、框架设计
2.1 基本概念
stream 流,让用户能够在client/service之间建立用户态连接,称为stream, stream的传输数据以消息为基本单位,输入端可以源源不断的往Stream中写入消息, 接收端会按输入端写入顺序收到消息,用户可以通过 stream client 来建立或者关闭一个流,流的客服端和服务端是一个对称结构。message 消息体,流传输数据以消息为基本单位。stream connector 流连接,数据交互的实体,维护一个 stocket 连接,一个流连接上能同时存在多个 streamc。event 触发事件,包括握手成功/失败,数据处理/处理失败,处理超时,流关闭等。handler 回调方法,不同的 event 对应执行的回调。request 请求,包含流信息和消息体,因为流传输是对称的,所有响应返回也是 reqeust。
2.2 框架功能
功能上来看,streaming rpc 可以划分为以下功能:在服务端处理数据使用 handler 方式回调处理,每次请求时候处理 request,建立 server 时候注册 handler。也可以实现自定义的其他类型 handler,handler 注册后通过事件触发执行对应的回调 handler。Streaming rpc 让用户能够在client/service之间建立用户态连接,称为流。流的传输数据以消息为基本单位。这一层是流处理逻辑的实体,包括事件的注册和分发,流状态查询,触发的回调,判断超时,协议的封装都在这层完成。流组件的公共部分是资源组件,负责流,连接的管理,主要负责创建流时分配给流空闲的流连接,关闭时释放资源等工作。流组件的公共部分是资源组件,负责流,连接的管理,主要负责创建流时分配给流空闲的流连接,关闭时释放资源等工作。一个tcp stocket 可以对应多个流,一个连接一个读写通道,消息体的边界特性可以让多个流共用一个 stocket,多个流并行处理数据,串行消息传输。流管理主要负责创建流时选择流空闲的流连接,关闭时释放资源,在出现连接读写超时或者错误时候记录错误连接并关闭连接上对应流的工作。提供了rpc 的基础能力和依赖的组件,包括服务发现,负载均衡,网络拨号,提供了 stream 之下连接的维护工作。通用组件包括日志,配置,环境信息等基础功能,使 stream 层应用可以无感的与基础设施进行集成。因为流的架构是对称结构,所以流组件层和基础组件层的架构,是服务端和客户端共用的架构。stream 交互
stream client 先在本地创建一个 stream,再通过一次RPC 与指定的 service 建立一个 stream,如果 service 在收到请求之后选择接受这个 stream, 那在 response返回 client 后 stream 就会建立成功。过程中的任何错误都把RPC标记为失败,同时也意味着 stream 创建失败。用linux下建立连接的过程打比方,client先创建 socket(创建stream),再尝试与远端建立连接(通过RPC建立stream),远端accept 后连接就建立了(service 接受后创建成功)。建立好后客户端和服务端分别生成一个 stream id。建立好流后进入通讯状态,使用 RPC(strm协议)双工交互, stream 提供流式发送和接收消息方法,框架负责创建流和 TCP stocket,服务端握手后生成一个 stream,stream id 与端 stream id 一致共用一个 stocket 连接,建立好后执行 accept(context.Context) error 方法与客户端 Stream 交互。在建立或者接受一个 stream 的时候, 用户注册好的 handler 处理对端的写入数据,连接关闭以及空闲超时等。关闭操作由任意一段发起,通过一次RPC 告知对端销毁 stream 并关闭本端资源,当 stocket 上没有流时候,同时关闭连接。如下图表示由 client 端建立并由 client 关闭的流状态图:
2.4 解决的问题
如何保证一个 tcp连接上建立多个 stream
client 和 server 结构对称,都包含有 stream manager, stream connect, stream 结构,分别对应 stream 分配(manager),保存连接(connect),数据交互(stream)的功能。在建立流的过程,一条 tcp 连接由 一个 stream connect 中保存,一个 stream connect 保存多个流,在 client/server 建立 stream 时,manager 会查看哪个 connect 处于未饱和状态,即可以存放 stream,如果没有生成一个 connect,把stream 存放在此 connect 中并申请资源。connect 保证各个 stream 并行收发数据,使用读通道异步发送,消息体保证了数据边界,每次发送一个消息体都是互斥的,读取 stocket 使用异步线程阻塞读取,通过 streamid 把数据分配到对应的 stream。stream 处理异常
大多数情况下,流由 client 端传输完成后主动关闭,或者空闲超时后服务端关闭,但是当出现错误或者网络异常的情况,需要保证双端的流的退出机制。当任意一端处理异常时,流会进入异常状态,进入关闭流程,发送关闭请求到对端,对端接收到请求后,也会进入关闭。
stream 处理网络异常
比如当一个流发现读写失败后,会进入关闭流程,同时通知流的管理者 ,在与这个流共用一个连接的所有流会收到管理者的通知,全部关闭。由于流的对称结构,对端感知到网络异常会进入同样的流程。异常处理保证了错误流不会承接后续的工作,保证异常连接的退出。当对 stream 读写失败后,本端的 stream 会感知到,这样双端 stream 的交互失败并且 stream 自动关闭,释放资源,同时通知 stream manager ,在这个 stream 共用一个 stocket 的 stream 通过 stream manager 通知关闭,同样释放资源。如何注册回调
框架在事件出发时执行对应的回调,服务端自定义回调方法,在框架中定义为 handler 接口,实现对应的接口即实现了回调,这样做的好处在于避免了多种回调方法注册导致的冗余代码以及扩展性的问题。基础的 handler 为服务端处理client request的回调方法, 定义类似:type Handler interface {
Handle(context.Context, Stream, Request) error
}
服务端的 server 结构提供注册方法注册 handler:Handler 为基础回调方法,必须实现,其他类型的实现为可选实现,包括:握手响应、握手成功、握手失败、超时回调、处理错误、关闭流等。不同回调方法分别对应不同的 interface{}, 用户只需要实现对应的 interface{},即完成了自定义的回调。
三、接入案例
我们选取了一个语言接入模块,原架构使用 brpc 实现上下游的语音传输和处理,如图所示分别使用Go 模块替代 client 和 server 端进行小流量验证,最终完全替换,如图:图 6. 使用 GDP streaming 对 brpc streaming 的替换
通过性能测试对比新模块新老模块的差异,其中,成功率、时延基本一致,实现了平滑升级。GDP 的 Straming RPC 的性能指标上:单实例小包(1k 以内)转发达到了 2.5w/s,同环境下达到了 brpc 的水平,可以满足高性能的需求,同吞吐率下 cpu对比brpc增长 20%左右。四、总结
Go 有动态语言不具有的并发与性能,占用系统线程少等优势,对比 C++ ,Go 的语法简单,对公有协议的良好支持,可以让其兼顾性能的基础上可以支持业务高效率的开发,调试和运维。GDP Streaming 提供了 brpc streaming 的 Go 版本,基于 S.O.L.I.D.软件设计原则,使得框架具有很高的可扩展性,为百度内 Gopher 提供了流式应用的实践方案。