谈谈网络流量控制
如今 Infiniband 和融合以太网(CE)之争早在 20 多年前就发生过一次,IB 像极了当时的 ATM,这次的以太网依然可能再次胜出,虽然可能会不再那么容易。本文谈谈一些关于流控的思考。
从能耗视角看,相比于简单丢包后重传,缓存数据包被认为更高效。假设重传和缓存引入相同的时延,丢包重传意味着一次无效的传输,而缓存数据包的能量则消耗在存储单元,但缓存数据包无需依赖准确的丢包检测和重传算法。因此若做高效的缓存而不是丢弃数据包方案,引入逐跳流控就显得必要。
从流量模型视角看,数据流波动越大,网络越异构,链路带宽差异越大,为了不丢包,独立因素乘法效应 ,buffer 需求越大,时延就越抖动,暴力增大 buffer 会降低实时流量的体验。同时,数据长度相对 BDP 越来越小,单纯依赖端到端反馈来进行拥塞控制的难度越来越大,因此需要逐跳流控。
无论 RoCE 还是 RoIB,目前均提供了逐跳流控,区别在于 IB 更复杂精细,而 CE-PFC 相对简单但效果也更有限,取以太网的老性格,差不多就行。
经常看到一些网文在介绍 IB 时都是泛泛而谈,将其流控错以为类似 TCP 的端到端流控,但其实差得很远。IB 流控是逐跳的(InfiniBand Credit-Based Link-Layer Flow-Control),非常类似当年的 ATM。当年 H.T. Kung 写过一个小册子 Traffic Management for High-Speed Networks(非常棒!建议阅读),讲的就是 ATM credit-based 逐跳流控的细节,但由于可扩展性问题被实现了 “最大最小公平性” 的 rate-based 流控所取代,所谓可扩展性问题即当时 credit-based 流控基于 VC 做 credit 控制,如果交换机上有成千上万的 VC,流控将 log 级复杂化。后来无论 IB 还是 CE 均不再以数据流做流控的粒度,这就解决了扩展性问题,IB 选择了 VL(Virtual Lane),而 CE 选择了 PQ(Priority Queue),故名 PFC,无论是 VL 还是 PQ,都数量固定有限,剩下的只是将数据流(or 数据块)映射到 VL 或 PQ,IB 和 CE 在底层是一回事。
IB 和 CE 逐跳流控区别在于,IB 是全时的,而 CE-PFC 则是对拥塞事件的反应,在效果上看,IB 比较平滑,PFC 对 buffer 需求大,引入的时延抖动也更大。但综合评判效果,需要具体数据的统计分布而不是基于对最好最差情况的分析,这一点上看,PFC 并没有那么糟糕,而 IB 的优势也不具压倒性。
前面提到过,数据长度相对 BDP 越来越小,小突发越来越高是趋势,这意味着 IB 的全时平滑流控可能更具实际优势,正如当年 ATM 所宣称的那样,但以太网还有杀手锏,KISS,见样学样,它可以快速从对手那里习得本质优势,并提供一个差不多的版本,让这些对手仅有的优势不再,就像当年以太网对待对手那样,这份名单上包括 ATM,FDDI,FC,各种环。换句话说,以太网直接支持全时 PQ 流控非常容易,但 IB 做减法就很难。
说起流控本身,就不得不再说说 TCP,它在 1974 年 RFC675 中被第一次定义,我们都很熟知 RFC793,但几乎没人知道 675,这 TCP 创世文档绝对值得一看,其中提到的滑动窗口影响了后续几乎所有协议的流控算法,其中就包括 ATM,IB 的 credit-based 流控,广义看,PFC 也算。
TCP 流控非常干净,端到端协议自然避开了可扩展问题,但它的 Linux 实现却不是非常直白,我曾经深入评价过这个事:TCP 流控挖坟。参考 675 的实现建议,TCP 的流控和丢包重传是解耦的,比如 675 说,如果 sender 发送速度超过了 receiver 的接收能力,receiver 应该有机制能降低 rwnd,而不是保证不丢包,这个机制就是
r
w
n
d
=
W
∗
(
1
−
F
B
)
a
rwnd = W*(1 - \dfrac{F}{B})^a
rwnd=W∗(1−BF)a,如果丢包,重传就是了。流控本身要 “Thus we see that line utilization and retransmissions can be traded off against buffering.”
那么,虽然但是,事后来看,把拥塞控制也交给 675 的流控建议也可以,这就可以统一处理端到端任意一处的拥塞。将应用程序不读,读得慢等效为拥塞,自然而然,丢包空洞和应用程序不读是等价的,一切都可以统一处理,因此,rwnd 有了新变化:
r w n d = W ∗ ( 1 − F B ) a rwnd = W*(1 - \dfrac{F}{B})^a rwnd=W∗(1−BF)a=> W c u r r = W m a x ∗ ( 1 − F B ) a ⋅ ( W l a s t W l a s t − b e f o r e ) β W_{curr} = W_{max}*(1 - \dfrac{F}{B})^a\cdot(\dfrac{W_{last}}{W_{last-before}})^\beta Wcurr=Wmax∗(1−BF)a⋅(Wlast−beforeWlast)β
如果网络发生拥塞,最后的因子会 < 1,窗口自然收缩。并且它是可以收敛的。我想,如果拥塞崩溃没有发生在 1986 年,而发生在 1974 年,拥塞控制算法一定会是上面简单的形式,至少最初是。但 TCPIP(1981 年成为 TCP/IP v4) 创世伊始,哪来的拥塞,拥塞在 10 年后的 1984 年 RFC896 中被意识到,再 2 年后以相似但不完全相同的形式实际发生,这就注定了后面以 VJ 为主角的复杂的故事。
有人会说,如果 receiver 应用程序拼命快速收数据,会不会把带宽榨干,答案显而易见:
- 如果抽取速度大于带宽承受极限,丢包会造成公式最后两个因子快速 < 1,窗口收缩;
- 如果网络带宽非常大,拼命收数据需要付出 CPU 代价,相对 sender 的 cc 而言,这不是免费的。
这意味着如果你想获得更大的带宽,你必须让你的应用程序收得更快一点,多劳多得。如果大家都试图抢夺更大带宽,丢包会让大家收缩,行为和结果,收益和代价因此而相配而不会错配,相对于 sender 取 min(rwnd, cwnd) 的复杂计算,这才是更自然的。
如今我们可以理解,Linux TCP 以一种更加谨慎的方式实现了 TCP,我前面那个文章的几个评论很好,我引用一下:
评论1:在我的理解中,rwnd实际上提供了一种保障,即在rwnd的范围内的inflight数据都不会被receiver拒绝。若rwnd大于rcvbuff,一个丢包就会导致后续数据无法向应用层提交,从而导致(rwnd-rcvbuff)数量的包被丢掉。这里感觉会导致对网络资源的浪费,这种情况似乎很难处理?
评论2:若不出现收端读不赢的情况,填充率一定程度上还能反应掉率(reorder对填充率影响也有,但是通常不大),也必须配合write pacing。理论上,在网络质量高的环境下,上面的公式能用比较小的but,获得一个比较快的流速。但是如果网络波动大,流速就会波动大(a越大,波动越大)所以,我猜测,linux那个实现,其实是个折中。
总结本文涉及的三者,IB,CE,TCP,其流控算法精华在 RFC675 中均能找到,读史使人明智。现在我们都知道,反压(背压,backpressure)几乎已经是流控领域的标准,也是早有预见。
回到当代,正如 ATM 如日中天的 1990 年代,随着高速网络的发展,为了保证高带宽高吞吐,我们对不丢包的无损网络的追求越来越需要关注代价,因为这个代价关系到网络性能除了带宽之外的另一面,时延。越大的带宽需要越大的 buffer,越大的 buffer 引入越大的时延,保带宽还是降时延?这是时代对流控的呼唤,不引入时延的前提下保证全速。
我一直没有描述流控的细节,如 Traffic Management for High-Speed Networks 中区别对待的 credit-based fc and rate-based fc,简单解释一下 rate-based 流控,它由 sender 的速率设置以及 sender 的速率控制组成。这两个阶段对应于基于 credit-based 流控中的 buffer 的分配管理和 credit 控制阶段。其实 rate-based fc 一直被用在拥塞控制领域,aimd,bbr 都基于此,而流控领域几乎都是 credit-based backpressure,这是一件正常到无法察觉的事,就像我们说母语一样。但本质上,它们是同一件事的两面,RFC675 如是说。
再用这个动图形象化 credit-based 流控,看,物理气压传递 credit,不需要太多 buffer。
浙江温州皮鞋湿,下雨进水不会胖。