面试官:说说什么是流式API&RPC?什么场景使用?

1,235 阅读10分钟

一、引言

在当今的软件开发领域,尤其是在处理大规模数据传输、实时通信场景时,流式 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

其中,chunklength表示chunk的字节长度,使用16进制表示,{chunk-length} 表示 chunk 的字节长度,使用 16 进制表示,{chunk-data} 为 chunk 的内容。

当 chunk 都传输完,需要额外传输 0\r\n\r\n 表示结束。

image

下面是一个例子:

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 结束。。

image

HTTP/2 的Stream并发传输

HTTP/1.1 的实现是基于请求-响应模型的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了HTTP经典的队头阻塞问题。而 HTTP/2 通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,实现HTTP/2的并发传输。

image

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握手过程,这些都是耗时操作。

小结

  1. 什么时候用Chunked,什么时候用SSE?
特性HTTP ChunkedSSE
传输方向服务端->客户端服务端->客户端
长连接支持支持支持
自动重连不支持支持
浏览器支持原生支持现代浏览器支持,一些老版本不支持
传输内容格式任意,如某些HTTP接口是application/x-protobuf,就无法使有SSEtext/event-stream(文本事件流)
  1. 什么时候用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机制实时下发。

image

当集群规模较大时,为了降低 kube-apiserver 的压力,Kubernetes将二者结合为list-watch机制,定期轮询+实时监听去保证客户端的资源信息实时性。

image

应用案例2:基于SSE实现chatgpt打字机效果

chatgpt相信大家都用过,当我们向机器人发送一条指令后,机器人会按照【打字机】将答案一个个打出来,看起来非常不错,这其实就是基于HTTP SSE实现。

以coze为例,当我发送【请说出Golang中channel的使用注意事项】命令后,可以看到请求的响应头有Content-Type: text/event-stream,代表这是SSE响应。

image

然后点击右侧的【EventStream】,可以看到消息确实是按照流式下发的,前端按照规定的格式解析,实现打字机效果。

image

三、流式RPC

实现机制

GRPC Stream

gRPC 使用 HTTP/2 网络协议进行服务间通信。 HTTP/2 的一个关键优势是它支持Stream, 每个Stream都可以在单个连接上复用多个双向消息。

image

在 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 读取数据并写入到其中;通过分散配置数据,为节点配置提供冗余和弹性。

image.png

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 部署和状态管理等业务场景诉求。

image

应用案例4:Nacos中基于GRPC双向流实现配置推送

Nacos可以简单理解为字节内部的Consul+TCC(服务注册中心+配置中心)。配置推送是指在控制台修改完某配置项,下发给各订阅端,类似TCC中发布变更的配置,各TCC Client可实时感知到配置变化。

该功能在Nacos 1.x中基于UDP实现,而在2.0及之后的版本中,转向了更为稳定和高效的gRPC双向流实现。

image

主要原因如下:

  1. UDP推送机制的目标是在网络状况良好的情况下,提高客户端发现服务变更的速度。然而,UDP协议本身是无连接的,不保证消息的可靠传输,因此UDP推送仅作为一种辅助手段,1.x的客户端主要还是依赖于每10秒一次的轮询查询来获取最新配置。

  2. 相比UDP,gRPC提供了连接管理和流量控制,减少了因网络不稳定导致的消息丢失问题,同时也降低了服务器资源的消耗。

  3. gRPC支持TLS加密,进一步提升了数据传输的安全性。

参考资料

Server-Sent Events 教程

Kubernetes 文档

Dubbo-kubernetes 基于Informer服务发现优化之路

gRPC Streaming, Client and Server

HTTP2学习笔记

HTTP2的变化及原理

HTTP/2 牛逼在哪?

Dubbo Streaming

Kitex Thrift Streaming

Nacos的配置推送如何工作?

万字长文解析 etcd 如何实现 watch 机制?

什么是ETCD