ATC19:R2P2: Making RPCs first-class datacenter citizens
一句话总结:设计新的传输协议R2P2,使网络本身能够感知并调度RPC请求,从而显著降低尾延迟、提高吞吐量。
R2P2 目的
- 通过请求级、网络感知的调度,在微秒级 RPC 场景下,同时实现低尾延迟、高吞吐、可扩展性,并消除传统负载均衡方案的瓶颈。
背景补充
传统 RPC 通常基于 TCP 连接,目标服务器在连接建立时就固定了,后续所有请求和响应都复用同一条连接,目标选择和数据传输是耦合的。
L4 Load Balancing & L7 LB
一个流=一个tcp连接,回顾五元组。
- L4:“一个流的所有包”都固定去同一个后端,改不了,但本文可以做到每个rpc独立调度,去到不同的后端。①客户端连接到负载均衡器的 虚拟 IP(VIP),②负载均衡器根据某种规则(如 哈希、轮询)选择一台后端服务器,③将 整个连接(TCP 或 UDP 流)映射到该服务器,④之后该连接上的 所有包 都发往同一台服务器
- L7:所有流量(请求全文 + 响应全文)都必须经过这个反向代理(Reverse Proxy)。①客户端连接到反向代理的 IP/端口,②反向代理 终止连接(即完全接收请求)③解析请求内容(如 HTTP 方法、URL、Header)④根据策略(如 URL 路径、Cookie、负载情况)选择后端服务器 ⑤与后端服务器 建立新连接(或复用连接池),转发请求 ⑥收到响应后,再转发回客户端
感知方法
- 对任务
- 通过协议头部的显式标记和上层应用的配置来获取任务对调度的特定要求。核心机制是:客户端在发送请求时,通过 R2P2 协议头中的
Policy字段,直接告诉路由器这个 RPC 需要什么样的调度行为。 - 无法做到
- 感知任务大小、负载长度 / 基于 URL 路径的路由(如
/api/user去一组服务器,/api/order去另一组)/ 基于请求内容的区分(如任务优先级)/ 基于用户身份的会话保持(session stickiness)—— 除非上层应用通过 Policy 字段扩展实现 - 这些是 L7 反向代理(如 NGINX)的典型能力,但 R2P2 选择不做,因为这会破坏 P4 硬件的可行性。
- 通过协议头部的显式标记和上层应用的配置来获取任务对调度的特定要求。核心机制是:客户端在发送请求时,通过 R2P2 协议头中的
当前论文中实现了两种策略:
| Policy 值 | 含义 | 使用场景 |
|---|---|---|
| Unrestricted | 路由器可以自由选择任何后端服务器 | 普通的、无特殊要求的 RPC(如大多数读请求) |
| Sticky | 强制将请求路由到特定的“主”服务器 | Redis 写操作(必须发往 master) |
- 对网络(感知网络拥塞)有限度的
-
- R2P2 可叠加使用拥塞控制协议(DCTCP、DCQCN、Homa),从而间接感知网络拥塞。( §3.1)
- DCTCP(Data Center TCP):通过 ECN 标记感知交换机队列深度,调整发送窗口。
- DCQCN(Data Center Quantized Congestion Notification):用于 RDMA 网络的拥塞控制。
- Homa:一种接收端驱动的低延迟传输协议,R2P2 的消息语义可以映射到其上。
-
- 对服务器(实时感知每台服务器的真实负载)
- 路由器维护每台服务器的“当前并发请求数”
- 请求到达时,路由器“占用”一个槽位 转发 REQ0 时
- 服务器完成请求后,主动“释放”槽位 收到 R2P2-FEEDBACK 时
R2P2的设计
协议层
| 消息类型 | 方向 | 作用 |
|---|---|---|
| REQ0 | 客户端 → 路由器 → 服务器 | RPC 的第一个包(可能带部分负载)。唯一经过路由器 的消息 |
| REQn | 客户端 → 服务器 | 多包请求的后续数据包。直连,不经过路由器 |
| REQready | 服务器 → 客户端 | 服务器收到 REQ0 后,若请求是多包的,发送此消息告知客户端“我已就绪,请继续发送 REQn” |
| REPLY | 服务器 → 客户端 | RPC 响应,可以多包。直连,不经过路由器 |
| R2P2-FEEDBACK | 服务器 → 路由器 | 服务器完成一个请求后发送,用于更新路由器的负载计数器 |
| SACK | 任意方向 | 请求重传缺失的包(多包场景) |
| DROP | 路由器/服务器 → 客户端 | 显式丢弃请求(用于流控或拒绝) |
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Magic | Header Size | Message Type |Policy|F|L|Resv|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ReqId (16 bits) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| PacketId / Packet Count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 长度 | 说明 |
|---|---|---|
| Magic | 8 bits | 协议标识,用于快速识别 R2P2 包 |
| Header Size | 8 bits | 头部长度(允许扩展) |
| Message Type | 8 bits | REQ0、REQn、REPLY、FEEDBACK 等 |
| Policy | 3 bits | 调度策略(Unrestricted / Sticky / 预留扩展) |
| F (First) | 1 bit | 标记是否为请求的第一个包 |
| L (Last) | 1 bit | 标记是否为请求/响应的最后一个包 |
| ReqId | 16 bits | 请求 ID,与 (src_ip, src_port) 一起唯一标识一个 RPC |
| PacketId / Packet Count | 16 bits | 多包场景下的包序号(REQn/REPLY)或总包数(REQ0) |
调度层(调度策略)
先查询再正式发包,用户的查询首包和后续正式包是分开发的。不同于源路由,而是在路径上动态找到目的地址的。路径上只有一个路由器(一个逻辑上的决策点),作为中央调度节点。
创新点
- 跟“代理”的区别是,传统 L7 代理(如 NGINX)作为中继时,而 R2P2 的“路由器”只处理每个 RPC 的第一个小包(通常几十字节),后续的请求数据包(REQn)和全部响应(REPLY)都直连客户端与服务器。在协议层面实现了“目标选择”与“数据传输”的彻底解耦。
- 调度逻辑可以下沉到交换机硬件。让网络设备本身理解 RPC 语义并参与调度,是“making RPCs first-class datacenter citizens”的真正含义。(通用的饼)
- 可以在此处补充一下“下沉”的通用好处
- 省了CPU,从“CPU 核数线性扩展”到“线速无瓶颈”
- 硬件的延迟确定性,从“微秒级抖动”到“纳秒级确定性”
- 架构革新,从“加一层代理”到“网络即调度器”(这一条AI浓度高,只直接算网融合得了吧!)
- JBSQ 策略的硬件友好设计(见下)
论文提出 JBSQ(n)(Join-Bounded-Shortest-Queue)策略
- 路由器维护一个中央队列,每个服务器有有限长度的队列(深度n)。
- 路由器仅将请求发往队列未满的服务器,且优先选择队列最短的。
- 相比JSQ(Join-Shortest-Queue),JBSQ更易在硬件(如P4交换机)中实现
- 原因: JBSQ 通过增加一个“有界”约束
- JSQ要求:在所有服务器中,选择当前队列长度(未完成请求数)最小 的那个。
- JBSQ(n) 将策略简化为:在 当前队列长度 < n 的服务器中,选择任意一个(通常是第一个找到的)
实现
- R2P2路由器的两种实现(可以吹 R2P2 的设计是 硬件友好,但不绑定硬件)
- 软件中继:基于DPDK,运行在通用CPU上,延迟约5μs,2核可处理10Gbps线速。
- 硬件实现:运行在P4可编程交换机(Tofino ASIC)上,延迟<1μs,利用寄存器维护队列状态,通过“重循环”机制实现JBSQ逻辑。
- 数据面硬件(P4)实现JBSQ 中的优化(§4.4)
- 寄存器组织:每个服务器的计数器存在独立的 P4 寄存器中。
- 分组轮查:将服务器分成多个组,每组在同一流水线阶段并行检查。如果当前组没有空位,包被“重循环”回到流水线开头,检查下一组。
- 状态优化:数据平面会记住上次转发时使用的组,下次从该组开始,减少重循环次数。
- 容忍竞态:论文承认可能存在“检查时有空位,但转发前被其他包占满”的竞态,但认为这在实际负载下影响很小,且不会导致系统崩溃。