电商、直播等业务要求以非常快的速度完成请求应答,计算和存储的飞速提高也在推动 HPC、分布式训练集群、超融合等新应用的普及,网络变成制约性能的主要因素之一。为此,我们设计了低开销高性能的 RoCE 网络,构建了低时延、无损的大型以太网数据中心,作为 RDMA 等技术的底层基石,也为 UCloud 未来的物理网络建设打下了良好基础。
一 低开销高性能的无损网络选型
普通的内网进行数据包交互时,通常会使用系统级的 TCP/IP 协议栈或者是 DPDK 技术,这两种方案都是依靠软件进行协议栈解封装的,对系统的 CPU 有不少消耗。而有一种方案:RDMA,可以直接使用网卡进行协议栈解封装,无需消耗系统 CPU,能有效降低数据处理的延时。
RDMA 并没有规定全部的协议栈,比如物理链路层、网络层、传输层每个字段长什么样,如何使用,但对无损网络有相当高的要求:
不轻易丢包,重传带来的延时非常大。
吞吐量巨大,跑满最好。
延时越低越好,100us 都嫌长。
依据上述要求,主流的网络方案有三种:
图:主流的 RDMA 网络方案
① InfiniBand: 该方案重新设计了物理链路层、网络层、传输层,是 RDMA 最初的部署方案,所以要使用专用的 InfiniBand 交换机做物理隔离的专网,成本较大,但性能表现最优;
② iWARP: 该方案的目的是让主流的以太网支持 RDMA,将 InfiniBand 移植到 TCP/IP 协议栈,使用 TCP 协议保证无丢包,但缺点在于 TCP 开销较大,且算法复杂,所以性能表现较差;
③ RoCEv2: 该方案的目的也是让主流的以太网支持 RDMA(RoCEv1 版本已很少提及了)。网络侧使用 PFC 保证拥塞时不丢包,网卡侧又使用 DCQCN 的拥塞控制算法进一步减缓拥塞(该拥塞算法需要网络侧支持 ECN 标记),传统的以太网经过 PFC 和 ECN 的加持进化成为无损以太网,在无损以太网上运行 RDMA 性能大大增强。
RoCEv2(后文简称 RoCE)方案的成熟案例较多,我们也选用了该方案进行研究。但 RoCE 方案仍存在一些问题,如 PFC 压制的不公平性、PFC 传递带来的死锁风险、过多的调参、ECN 标记的滞后性(ECN 概率标记是软件轮询机制)等,是需要我们解决完善的。
二 网络设计的目标
要把 RoCE 搬到经典的数据中心网络上,这可不是一件容易的事儿。
当前数据中心是常见的 CLOS 架构,LCS 是汇聚交换机,LAS 是 TOR 交换机。如果 RoCE 直接运行在这上面,问题是显而易见的:例如出现Incast 事件时,转发不了的报文会被存放在交换机缓存中,但缓存也不是无限大的,如果存满了,这个数据包就丢掉了,很明显这种丢包频率肯定不能被 RDMA 所接受。
上面只是举了一个简单的例子,实际上出现的问题要更复杂一些。在设计之前,需要先明确好我们的目标是什么,做到有的放矢。
简单来讲我们的目标就是:
在各种流量模型下,网络带宽要能跑满;
缓存使用要尽可能低;
极限情况下缓存使用满了也不能丢包。
总结来说,为了让 RoCE 跑在已有的网络上,我们需要从三个方面下手:
① QOS 设计:指队列、调度、整形等一系列的转发动作,相对独立。
② 无损设计:是 RDMA 的要求之一,使用 PFC 技术实现。无损是一种基本保障,含义是在最拥塞的情况下也能保证其可用性,让上层应用可以放心发送数据,不必担心丢包的风险(所以说 PFC 并不是降速的手段)。
③ 拥塞控制设计:使用 DCQCN 技术实现。拥塞控制是满足基本保障前提下的进一步优化,含义是在开始拥塞的时候,就告知服务器两端,使其从源端开始降速,从根本上解决问题。
补充一点拥塞带来的坏处:当出现拥塞后,必然要使用缓存,使用缓存后虽然不丢包了,但是带来的后果是延迟上升,而且吞吐也不能再增加一丝一毫。网络中拥塞点有很多,每一跳都可能成为拥塞点,在上图的网络中,最多会有 3 个拥塞点。
Tip 缓存的使用能带来多少延时?
我们按 25Gbps 来算,缓存 25Mb 的数据,大约需要 1ms 的时间才能发送完毕,25Mb 也仅仅是 3.1MB,而常见的 Broadcom Trident 3 芯片有 32MB 的缓存。
有了这三个方面的认识,我们就可以化繁为简,逐一破解。
三 QOS 设计
QOS 的设计,无非是入队、调度、监管和整形。
入队方式可以依据 DSCP、TOS、COS 等标记,然后信任某种标记入队,也可以选择使用策略抓取其它报文特征入队。我们最终选择的策略是:在 IDC 边界处,使用报文特征抓取入队,并重写 DSCP,IDC 内部仅根据 DSCP 入队(IDC 内部减少策略使用,满足高速转发即可)。这样,既能保证 DSCP 标记的可信任,又能减轻 IDC 内部的策略复杂度。根据这个思路,我们分别设置对应策略:
对 ToR 下行端口与 Border 上行端口: 抓取特定报文,进入特定队列。
对其余设备和端口设置:信任 DSCP,按映射入队。
用图表表达即:
■ IDC 边界入队
次序 | Match | Action |
---|---|---|
1 | udp_dport==4791 && dscp==48 | 入队列 6 |
2 | udp_dport==4791 && dscp==46 | 入队列 5 |
其他 | 其他 | 修改 dscp 为预定义 |
* 这是已有的标记策略,我们 IDC 内部为业务进行分类,并标记特定的 DSCP。
* 其中次序 1、2 只在 RoCE 网络的 ToR 部署。
■ IDC 内部 DSCP 映射
DSCP | 队列 |
---|---|
48 | 6 |
46 | 5 |
其他 | 2… |
下面该聊聊调度设计了,调度的对象是缓存中的数据,也就是说,调度是仅在拥塞时才生效的,而且调度生效后,影响的将是各队列的流量大小。
带着以上的认识,我们开始调度设计。在一般的 RoCE 网络中,使用的有如下队列(或流量):
① 协议信令类,目前来看只有 CNP 流量;(其它协议均不跨跳,所以不考虑)
② RoCE 流量;
③ 业务 / 管理流量。
这三大类流量,还可以继续分小类。按照 ETC 所推荐的调度模型,我们选择了SP+WDRR 的调度方式,即:1 类流量绝对优先,在缓存积压的时候优先调度,直到队列为空。2 类和 3 类流量次优,两者之间按照 WDRR 调度,权重值可以灵活定义。这样就能保证 CNP 报文在 3us 内转发给流量源站(没有拥塞的网络单跳的延时在 1us 以内)。
以上调度设计中有个漏洞:如果队列 6 的流量过大,可能会将低优先级的队列饿死(即长时间得不到调度),虽然理论上队列 6 的流量一般都在几十~ 几百 Mbps,但仍要提防服务器恶意攻击行为。于是,我们将 SP 的队列限制其队列使用带宽。这个便是所谓的监管和整形了。
四 无损设计与分析
RoCE 的流量需要保证运行在无损队列中,无损队列使用了 PFC 技术,能针对某一队列发送 Pause 帧,迫使上游停流。
在博通的 XGS 系列芯片中,有一块缓存管理单元 MMU(简称缓存),存放已收到但没转发走的报文,并给入口和出口都计数:“0/1 的入口和 0/2 的出口,都用了 1 个 cell”(cell 是缓存资源的最小单位)。
缓存会给每个入口和出口设置一个上限,超过这个上限就不能再使用 cell 缓存报文了。上限以下还画了很多其它的水线,同时对每一个出口和入口进行进一步细分,可以按照队列进行统计限额其中入方向。入方向上,细分了 PG-Guaranteed 大小、PG-Share 大小、Headroom 大小;出方向上,细分了 Queue-Guaranteed 大小,Queue-Share 大小(如下图所示,这里我们不考虑端口,只考虑队列)。
缓存使用的时候,总是从下往上依次申请使用,所以更喜欢把这些区块大小称之为 “水线”,当“某区块”都使用完毕,就称之为 “缓存水位”到达了“某水线”。例如:当 PG-Share 区块使用完毕,就称之为,入口缓存水位已经到达 PG-Share 水线。如果所有区块用完就产生丢包了,称为 no buffer 丢包。
每一块大小都有其特殊用处,先简单看下其作用,后面再探讨下无损队列中的这 5 个水线应该如何设置。
► PG-Guaranteed和Queue-Guaranteed是保证缓存,这部分是独享的,即使不用,别的队列也不能抢占使用。
► PG-Share和Queue-Share使用的是共享缓存,因为动态水线的缘故,它们的大小不固定,如果很多队列都在用,那平分一下,每个队列的水线就都很小。另外,PG-Share 还有另一个重要的作用:PFC 发送的临界点,也称为 xoff 水线,只要到达该水线,PFC 就会从这个口发出去,回落一些后,才恢复正常。
► Headroom是一个特殊的水线,只有在无损队列中才能发挥其作用。设想一下,PFC 发出去以后,流量真的能瞬间停下来么?答案是不能的!因为线缆中还有一部分数据,而且七七八八的转发处理时间也要算进去。所以 Headroom 空间就是用来做这个的。
1、PG-Guaranteed 和 Queue-Guaranteed
讲完了基本原理,回过头来看网络设计。先看 PG-Guaranteed 和 Queue-Guaranteed 水线,这俩水线与“无损队列”关系不大,保证缓存的作用只是满足交换机基本的存储转发功能,所以配置为一个数据包大小即可。那我们按照最差的情况来算,即 MTU=9216 的巨型帧。
但实际上我们不必为此发愁,因为动态水线的缘故,共享缓存中总会有剩余的缓存以供使用,所以保持原厂的默认配置即可。
2、Queue-Share
接下来是 Queue-Share 水线。在无损队列中,我们希望在缓存丢包前,能触发 PFC 进行反压,所以在任何情况下,都应该入口 PG-Share 先到达水线,出口 Queue-Share 永远不能到达水线(PG-Share 到达会发 PFC,Queue-Share 到达会丢包)。
之前讲过,MMU 记账是出口入口各记一笔,这样来看,最差情况应该是多打一(出口的帐全记在一个队列上,入口的帐会均摊到不同队列中)。为了让出口水线永远不会到达,索性将出口水线配置为无限大好了,事实证明这样做也没有问题,因为入口的 PG-Share 是动态水线,总能在 Buffer 破产前触发该水线。
这样一来,Queue-Share 好像已经搞定了,其实不然,如果 TCP 流量参与进来混跑呢?这问题可就严重了,TCP 的 Lossy 队列会吃掉大量缓存,所以 Lossy 队列中,对应的 Queue-Share 水线也应当限制一下。
3、PG-Share
PG-Share 水线只要配置为动态水线即可,大小可以随意调节,都不会出太大问题的,但需要满足一个不等式:(PG-Share + PG-Guarantee + Headroom) * [入口个数]≤ Queue-Share + Queue-Guarantee
该公式描述的是一个端口多打一的场景。入口个数根据实际情况选取一个较大值(拿 ToR 来看,最差情况是 39 打 1,32 个 25G 下行,8 个 100G 上行)。
这里的 PG-Share 是动态水线,动态水线用一个简洁的公式即可表达:PG-Share = [剩余 Buffer] * α
这里的α是缩放因子,用户可自由调节。可以看出,缩放因子决定了 PG-Share 水线的大小。依据上面等式,我们只要将 Queue-Share 水线设置为静态最大、PG-Share 设置为动态即可,入口的缩放因子α可随意。
当然入口α也不能设置太小,在端口少打多的情况下,由于入口的水位很低,导致均摊到每个出口时,出口的水位更低!出口的水位过低时,会发现已有的 ECN 配置不再生效(例如:可能出口的水位还到不了 Kmax 的一半)。在我们的经验看来,无损队列中 PG-Share 的α,配置 1/8,1/4,1/2,1 都可以,具体大小还要联合拥塞设计中 ECN 参数来决定。
4、Headroom
Headroom 水线很重要,但可以通过实验 + 推导的方式得出合理的配置,先来看一个等式:[Headroom 大小] = [PFC 构造到停流的时间] * [端口速率] / [64 字节小包占用的比特数]
使用 64 字节小包计算,是因为小包对缓存的使用率最低,单个 Cell 有 200 多字节,但只能被一个报文独享。其中,只有 [PFC 构造到停流的时间] 是需要进一步分解的:T = Tm1 +Tr1 + Tm2 +Tr2
Tm1:下游 PG 检测到 xoff 用完,到构造 PFC 帧发出的时间。
Tr1:PFC 帧从下游发往上游的时间。
Tm2:对端收到 PFC 帧,到队列停止的时间。
Tr2:队列停止后,线缆中报文传输的时间。
可以看出,这四个时间中,只有线缆长度是变量,继续化简后可以得出:[Headroom 大小] = (Tm1 + Tm2 +2 * [线缆长度] / [信号传播速度]) * [端口速率] / [64 字节小包占用的比特数])
这里面 Tm1 + Tm2 是常数,可以实验测得,剩余的都是已知量了。最后根据公式就可以算得 100G 口,100M 光纤下,H = 408 cell;25G 口,15M AOC 下,H = 98 cell。当然,真正使用的时候,还要再冗余一点,毕竟这是临界值。
5、死锁分析和解决
谈到 PFC 就不得不提一下死锁,死锁危害极大,而且其传递性会迅速扩散到整个网络,以至于整个网络的无损队列全部停流。死锁的研究很多,其中较详细的是微软的一篇论文 《Deadlocks in Datacenter Networks: Why Do They Form, and How to Avoid Them》。
死锁产生的一个必要条件是 CBD(环状缓存依赖),在我们的组网环境中,是典型的 CLOS 组网,所以在稳定状态下不会存在 CBD,也没有死锁风险。而且整个 POD 内部路由不做过滤,明细互知,汇聚采用 4 台~8 台冗余,即使出现两点故障,收敛后的拓扑也不会存在 CBD,即不会存在死锁风险。
至此,我们已经解决稳定状态下的死锁了,但还要考虑一点:收敛过程中,是否存在 CBD?其实仔细分析一下还是会存在的,我们考虑了很多收敛场景,确实会有部分场景下,存在微环路。有微环路就一定有 CBD。事实证明,我们也真实地模拟出了微环路导致的死锁。
死锁问题总是要解决的。我们使用三种方法:
针对各种微环路场景,通过设计网络协议,控制收敛的现先后关系,避免出现微环路出现。
对于其它未知的死锁风险,使用交换机的死锁检测功能,释放缓存(释放缓存会产生丢包,但收敛过程本身就有乱序 / 丢包情况)。
将 PG-Share 的水线适当拉高,尽量使用 DCQCN 拥塞控制来压制流量。
五 拥塞控制设计与分析
网络拥塞控制是一个很复杂的课题,这里只讲一些基本的设计思路。
RoCE 使用的拥塞控制算法是 DCQCN,《Congestion Control for Large-Scale RDMA Deployments》 这篇论文很详细地描述了该算法。这里先简单的描述下这个算法:维护这个算法的节点是服务器,也就是流量的两端,中间的交换机作为传输节点,通告是否拥塞。发送方叫 Reaction Point,简称 RP;接收方叫 Notification Point,简称 NP;中间交换机叫 Congestion Point,简称 CP。发送方(RP)以最高速开始发送,沿途过程中如果有拥塞,会被标记 ECN 显示拥塞,当这个被标记的报文转发到接收方(NP)的时候,接收方(NP)会回应一个 CNP 报文,通知发送方(RP)。收到 CNP 报文的发送方(RP),就会开始降速。当发送方没有收到 CNP 报文时,就开始又提速了。
上述过程就是 DCQCN 的基本思路。虽然整个算法十分复杂,但都是围绕这个基本思路,继续完善算法细节(下图分别是 NP 的状态机和 RP 的算法)。可调参数也十分众多,比如降速要降低多少?提速效率是否积极?网络拥塞度如何维护?拥塞度更新周期多久?CNP 报文的敏感度多大?这都是问题,需要对流量建模后找出合理参数。
DCQCN 算法中,对 RP、NP 和 CP 都有很多参数可以调节。RP 和 NP 节点在服务器上,准确来说应该是在网卡上,网卡初始化的参数已经为最优值,无需再进行调整,这样就剩 CP 上的参数需要调整了。CP 上有三个参数其实就是 WRED-ECN 的那三个参数,分别是 Kmin,Kmax,Pmax,这三者的关系,可以用下图来表示。横轴是出向队列长度,纵轴是报文被标记的概率。从图中可以看到,在队列长度超过 Kmax 时,标记概率出现一个跳变,从 Pmax 直接到达 100%。
根据上面的理论分析,我们可以通过实验证实和试错的方法一步步找到最优解。
现在设想一下:在一个拥塞场景中,当出口队列长度小于 Kmin 时,不会被标记,出口队列长度可能会稳步增长,当队列长度超过 Kmin 时,DCQCN 才开始降速。
所以 Kmin 的大小决定了 RoCE 网络的基础延时,这些缓存中的报文是发送者发出,但未被接收者确认的报文,我们称之为 inflight bytes,约等于延时带宽积。所以,Kmin 的配置规范为小于期望的延时带宽积。有了这个理论基础后,实践测得理论符合实际,还可以根据测得的延时进一步调整该数值。
我们用同样的思路来思考 Kmax,承接刚刚的思路,那就是:Kmax 的配置规范为小于或等于能容忍的延时带宽积。但这次不再这么简单了,因为 Kmax 还决定了图中的斜率。同样决定斜率的还有 Pmax,在讨论 Kmax 和 Pmax 前,我们不得不先介绍下整个 ECN 的理想与现实。
理想状态下,标记概率在定义域 Kmin~Kmax 内的变化是连续的,而且,队列的长度是准确的。但事与愿违,博通芯片 SDK 使用软件轮询的方式测得队列长度,而且将此刻的队列长度与历史值做指数平均,并依此计算标记概率。软件轮询带来的结果是,标记概率在定义域 Kmin~Kmax 内的变化是不连续的,其次,指数平均值会让测得的队列长度是滞后的(当然指数平均也带来了好处,这里不展开)。
这件事带给我们的影响就是,理论推导的 Pmax,甚至 Kmin、Kmax 都被推翻,请继续往下看:理想状态下,一个 25G 端口、单 QP 会话下,最大的有效 Pmax 是多少?
根据 DCQCN 中 NP 的算法,50us 内收到多个 CE 标记包,会被认为只有一个有效包,所以最高的 CE 标记速率应该为 20000 个包每秒(即 1 个包每 50 微秒),依此,我们算得最高有效 Pmax,即是设置的 Pmax 值,如下表所示:我们假设一个 25G 端口、只有一个 QP 会话,此时最高有效 Pmax 是多少?可以根据表格中第 4、5 列计算出最后一列最高有效 Pmax 的值。
再回到现实,我们按照推导的数据对表格最后一行进行验证。
对端口限速模拟拥塞,测得稳定时 RoCE 流量 pps=2,227,007,然后选取一组 ECN 配置:Kmin=1cell,Kmax=1400cell,Pmax=1%,理论上来说 Pmax 已经超出最高有效的值了,理论上即使在拥塞时,出口水位也不可能达到 1400cell,所以再设置一个监控项,监控出口水位有没有超过 1400cell(触发式告警,并非轮询,所以不会存在采集不到的情况)这是第一个实验。
作为对比,第二个实验使用另一组 ECN 配置,Kmin=800cell,Kmax=1400cell,Pmax=1%,按照之前分析,这一组配置下,出口水位也不会超过 1400cell,因为在 1400cell 水位时,Pmax=1 已经超过最高有效标记概率了。
可是实验结果并不符合预期,第一个实验没有触发告警,通过;第二个却触发告警了。这就意味着在某些时刻,缓存水位超过 1400cell 了!水位是波动的,并没有稳定在某个值!我们大胆猜测其中原因:从缓存队列积压,到得到缓解,这其中有太多地方消耗了时间:队列长度的轮询、指数平均算法、CNP 的生成与转发,甚至于降速后线缆中的数据传输等等。
为解决这一难题,我们另辟蹊径,选择了另外一条路:首先制定了几个小目标,然后通过大量的实验来摸索出验证一套安全可靠的配置。这个方法虽然更野蛮,但很有效。
► 小目标 1:服务器端口吞吐量要在 95% 以上;
► 小目标 2:所有流量场景下交换机 99% 的时间里 PFC 发送速率不得高于 5pps;
► 小目标 3:任意场景下服务器端到端延时不得高于 80us(90% 场景下低于 40us)。
对于流量模型,我们设计筛选后,选用了 50 余种流量,最终我们得到了同时满足这三个小目标的合理参数。
不得不说,DCQCN 很难玩转,参数众多且互有联系,这里也只是提供一些实践规律,欢迎一同深入探讨。