一、引言
在当今的软件开发领域,尤其是在处理大规模数据传输、实时通信场景时,流式 API & RPC 的应用越来越广泛。理解它们的原理和应用场景有助于我们开发人员构建更加高效、灵活的软件系统。本文将简单探讨流式 API 和流式 RPC 的原理,并结合四个大型开源项目中的应用案例进行分析,为赋能业务提供更多思路。
二、流式API
实现机制
分块传输编码(Chunked)
Chunked编码是 HTTP/1.1 RFC9112中定义的一种HTTP流式数据传输机制。在这种机制下,HTTP 响应主体被分割成一系列的块(chunk)。每个块有自己的长度标识(以十六进制表示),后面跟着对应长度的数据内容,最后一个块的长度为 0,表示数据传输结束。
当服务器返回 Transfer-Encoding: chunked 时,表明此时服务器会对返回的包体进行 chunked 编码,每个 chunk 的格式如下所示:
${chunk-length}\r\n${chunk-data}\r\n
其中,{chunk-data} 为 chunk 的内容。
当 chunk 都传输完,需要额外传输 0\r\n\r\n 表示结束。
下面是一个例子:
HTTP/1.1 200 OK
Date: Mon, 07 Oct 2024 02:17:04 GMT
Connection: keep-alive
Transfer-Encoding: chunked
1\r\n
a\r\n
2\r\n
bc\r\n
5\r\n
hello\r\n
0\r\n\r\n
Server - Sent Event(SSE)
SSE(Server-Sent Events)是一种基于 HTTP 协议的协议,它允许服务器向客户端推送事件(单向)。这意味着客户端不再需要不断地向服务器请求数据,服务器可以主动将数据推送给客户端。SSE 通常用于实时更新的数据,例如新闻更新、聊天信息或股票价格。
**工作原理:**客户端通过普通的 HTTP 请求连接到服务器,并通过特定的 HTTP 头信息( Accept: text/event-stream)告知服务器它希望保持连接以便接收实时数据。服务器接收到这个请求后,保持连接不断开,并设置响应头 Content-Type: text/event-stream,告诉客户端后续的内容将是事件流,并周期性地向客户端发送消息,每条消息都是纯文本,以 "data: " 开头,后面是消息内容,并以连续的两个换行符 \n\n 结束。。
HTTP/2 的Stream并发传输
HTTP/1.1 的实现是基于请求-响应模型的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了HTTP经典的队头阻塞问题。而 HTTP/2 通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,实现HTTP/2的并发传输。
Stream核心设计如下:
-
一个 TCP 连接包含一个或者多个 Stream,Stream 是 HTTP/2 并发的关键技术;
-
客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接。
-
Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;
-
Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体);
-
不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream 。
-
同一 Stream 内部的帧必须是严格有序的。
-
在 Nginx 中,可以通过 http2_max_concurrent_streams 配置来设置 Stream 的上限,默认是 128 个。
HTTP/2通过Stream实现的并发,比HTTP/1.1通过TCP连接实现并发的效果要好很多。因为当HTTP/2实现1000个并发Stream时,只需建立一次TCP连接,而HTTP/1.1需要建立1000个TCP连接,每个TCP连接都需要经过TCP三次握手、慢启动、TLS握手过程,这些都是耗时操作。
小结
- 什么时候用Chunked,什么时候用SSE?
| 特性 | HTTP Chunked | SSE |
|---|---|---|
| 传输方向 | 服务端->客户端 | 服务端->客户端 |
| 长连接支持 | 支持 | 支持 |
| 自动重连 | 不支持 | 支持 |
| 浏览器支持 | 原生支持 | 现代浏览器支持,一些老版本不支持 |
| 传输内容格式 | 任意,如某些HTTP接口是application/x-protobuf,就无法使有SSE | text/event-stream(文本事件流) |
- 什么时候用HTTP2?
个人调研的结果是目前裸用HTTP2的场景很少,一般都是RPC内置,如GRPC底层是HTTP2,下文会介绍。也欢迎大家评论区补充直接用HTTP2的场景。
应用案例1:Kubernetes中基于HTTP Chunked实现Watch机制
在 Kubernetes (简称 K8S,一个可移植容器的编排管理工具)中,有5个主要的组件,分别是 master 节点上的 kube-api-server、kube-controller-manager 和 kube-scheduler,node 节点上的 kubelet 和kube-proxy 。这其中 kube-apiserver 是对外和对内提供资源的声明式 API 的组件,其它4个组件都需要和它交互。为了保证消息的实时性,有两种方式:
-
List:客户端组件 (kubelet, scheduler, controller-manager 等) 轮询 kube-apiserver
-
Watch:客户端实时监听kube-apiserver,kube-apiserver将资源变更等信息实时下发到客户端。本质上是客户端调用kube-apiserver提供的Watch API建立HTTP长链接,当kube-apiserver感知到资源变更后,封装为事件消息通过chunked机制实时下发。
当集群规模较大时,为了降低 kube-apiserver 的压力,Kubernetes将二者结合为list-watch机制,定期轮询+实时监听去保证客户端的资源信息实时性。
应用案例2:基于SSE实现chatgpt打字机效果
chatgpt相信大家都用过,当我们向机器人发送一条指令后,机器人会按照【打字机】将答案一个个打出来,看起来非常不错,这其实就是基于HTTP SSE实现。
以coze为例,当我发送【请说出Golang中channel的使用注意事项】命令后,可以看到请求的响应头有Content-Type: text/event-stream,代表这是SSE响应。
然后点击右侧的【EventStream】,可以看到消息确实是按照流式下发的,前端按照规定的格式解析,实现打字机效果。
三、流式RPC
实现机制
GRPC Stream
gRPC 使用 HTTP/2 网络协议进行服务间通信。 HTTP/2 的一个关键优势是它支持Stream, 每个Stream都可以在单个连接上复用多个双向消息。
在 gRPC 中,我们可以具有三种功能调用类型的流:
-
SERVER_STREAM(服务端流):客户端向服务器发送单个请求,并获取回几条它顺序读取的消息。
-
CLIENT_STREAM(客户端流):客户端向服务器发送一系列消息。客户端等待服务器处理消息并读取返回的响应。
-
BIDIRECTIONAL_STREAM(双向流):客户端和服务器可以双向发送多条消息。消息的接收顺序与发送顺序相同。但是,服务器或客户端可以选择回复接收到的消息的顺序。
Dubbo Triple Streaming
Dubbo Streaming 是 Dubbo3 中 Triple 协议新提供的一种 RPC 数据传输模式,适用于以下场景:
-
接口需要发送大量数据,这些数据无法被放在一个 RPC 的请求或响应中,需要分批发送,但应用层如果按照传统的多次 RPC 方式无法解决顺序和性能的问题,如果需要保证有序,则只能串行发送
-
流式场景,数据需要按照发送顺序处理, 数据本身是没有确定边界的
-
推送类场景,多个消息在同一个调用的上下文中被发送和处理
Triple 协议是 Dubbo3 设计的基于 HTTP 的 RPC 通信协议规范,它完全兼容 gRPC 协议,支持 Request-Response、Streaming 流式等通信模型,可同时运行在 HTTP/1 和 HTTP/2 之上。
Dubbo Streaming沿用GRPC Stream的三种模式:服务端流、客户端流、双向流,支持基于Java Interface、Protobuf IDL开发,详见官网。
本质上Dubbo Steaming仍是基于HTTP/2实现,复用各语言强大的HTTP网络基础库,如Java的Netty,Go的原生http库等,详见官网文档。
Kitex Streaming
Kitex Streaming也是基于HTTP/2实现,并对齐GRPC Stream的三种模式,支持基于Thrift、Protobuf IDL开发,详见官网。
小结
各RPC框架Stream实现的相同点:
-
GRPC、Dubbo Triple、Kitex均是基于HTTP/2实现的流式RPC
-
都是支持服务端流、客户端流、双向流三种模式
不同点:
- Kitex支持基于Thrift、Protobuf IDL的Streaming定义和使用;Dubbo Triple支持基于Java Interface、Protobuf IDL的Streaming定义和使用;GRPC只支持基于Protobuf IDL的Stream定义和使用
应用案例3:ETCD中基于GRPC双向流实现Watch机制
etcd是一种开源的分布式统一键值存储,用于分布式系统或计算机集群的共享配置、服务发现和的调度协调。最值得注意的是,它是 Kubernetes 的首要数据存储,也是容器编排的实际标准系统。使用 etcd,云原生应用可以保持更为一致的运行时间,而且在个别服务器发生故障时也能正常工作。应用从 etcd 读取数据并写入到其中;通过分散配置数据,为节点配置提供冗余和弹性。
etcd v2 和 v3 版本之间的重要变化之一就是 watch 机制的优化。etcd v2 watch 机制采用的是基于 HTTP/1.x 协议的客户端轮询机制,历史版本存储则是通过滑动窗口。在大量的客户端连接的场景或者集群规模较大的场景,导致 etcd 服务端的扩展性和稳定性都无法保证。etcd v3 在此基础上进行优化,使用gRPC双向流的Watch API设计,实现了一个client/TCP连接支持多gRPC Stream,一个gRPC Stream支持多个watcher,如下图所示。同时事件通知模式也从client轮询优化成server流式推送,极大降低了server端socket、内存等资源,满足了 Kubernetes Pods 部署和状态管理等业务场景诉求。
应用案例4:Nacos中基于GRPC双向流实现配置推送
Nacos可以简单理解为字节内部的Consul+TCC(服务注册中心+配置中心)。配置推送是指在控制台修改完某配置项,下发给各订阅端,类似TCC中发布变更的配置,各TCC Client可实时感知到配置变化。
该功能在Nacos 1.x中基于UDP实现,而在2.0及之后的版本中,转向了更为稳定和高效的gRPC双向流实现。
主要原因如下:
-
UDP推送机制的目标是在网络状况良好的情况下,提高客户端发现服务变更的速度。然而,UDP协议本身是无连接的,不保证消息的可靠传输,因此UDP推送仅作为一种辅助手段,1.x的客户端主要还是依赖于每10秒一次的轮询查询来获取最新配置。
-
相比UDP,gRPC提供了连接管理和流量控制,减少了因网络不稳定导致的消息丢失问题,同时也降低了服务器资源的消耗。
-
gRPC支持TLS加密,进一步提升了数据传输的安全性。
参考资料
Dubbo-kubernetes 基于Informer服务发现优化之路