TCP rwnd算法挖坟

Posted dog250

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP rwnd算法挖坟相关的知识,希望对你有一定的参考价值。

新算法的模拟代码明早再写。

一个大厂内部分享,讲师说TCP长肥管道无法填满,这是错误的。我曾经单流填满过一条200ms的5Gbps专线。

为什么大家碰到填充长肥管道难题后首先想到的都是rcvbuff不够而不是rwnd不够呢?显然是Linux TCP实现的误导。Linux TCP的rcvbuff决定了rwnd。

Linux TCP的rwnd不允许超过rcvbuff,参见 __tcp_select_window 和 tcp_rcv_space_adjust 这两个函数。rwnd的硬限制便阻止了程序以更快的速率读取数据,再猛的CPU也是浪费,rwnd决定了没有那么多数据可供读取。

打个比方,在一条0.5s(500ms)的10GBps(为了和主机单位统一,换算做大B)链路上,TCP接收端配置了10B的rcvbuff,其rwnd最大就是10B,这意味着1s(1 RTT)内,发送端只能发送10B,该连接的吞吐即被限制为10Bps,即便接收端有10GBps的接收能力也只能维持在可怜的10Bps,因为1s只能送过来这么多数据。
但实际上,10B的超小rcvbuff果真是吞吐能力的限制因素吗?

理想情况下,一条从socket send到socket recv的管道不需要任何buffer,但为了平滑到达率和socket read速率之间的统计波动,需要设置一些buffer,但肯定不需要BDP那么大:

论长肥管道的传输能力,关键在肥而不在长,接收端的接收能力需要和“肥”相匹配,与长无关。

因此,端到端传输能力取决于接收能力,而不是取决于rcvbuff的大小,rwnd应该基于接收能力计算而不是rcvbuff。

程序应该从协议栈用力吸数据。

换一句话就说明白了,网络传输能力取决于瓶颈带宽BltBW而不是路由器buffer。现在大家都认同BBR比CUBIC更合理,把这认同搬到端到端流控就是一个意思。既然BBR不以填充buffer为目标,rwnd也不应受rcvbuff的限制。

rcvbuff的作用是“暂存尚未被应用程序取走的积压数据”,而不是限制发送量。只要保证积压数据量维持在rcvbuff内即可。因此,合理的流控措施是,监测rcvbuff积压量或者积压率越过一定阈值,缩小rwnd进行源抑制。

如果应用程序发生了抖动降低了读取速率,rcvbuff开始堆积,至于rcvbuff需要多大,取决于接收端能以多快的速度抑制发送端,这个时间不会短于半个RTT。抖动处理应该整体考虑,rcvbuff溢出只是其中一环,类似于拥塞控制,需要启发式算法。

流控和拥塞控制是统一的,cwnd控制网络拥塞,rwnd控制程序降速,后者也是一种拥塞,网络拥塞交换机buffer排队,程序降速rcvbuff排队,本质是一回事。

我觉得正常理解流控就该是以上这个样子,反而Linux TCP的实现显得怪怪的。但大家都在Linux这个台面上工作,目之所及全是它,也就难免认为Linux都是正确的了。

跟同事及几个朋友深入聊过这个话题后,我决定挖一下坟,看看原始的TCP怎么说。从1974年原汁原味的RFC675。果然找到了证据:

下面这段更明显:

还给出了一个具体算法:

详情参见:Specification of Internet Transmission Control Program
最后这个算法很有趣,它可作为一个通配的自适应rwnd计算公式,省的去probe了:

其中F/B是rcvbuff的当前填充率,痛则不通,通则不痛,显然填充率越高越不好,填充率设为x,Wmax为允许的最大rwnd,a暂取2,这个公式就是:

W’ = W*(1-F/B)^a

显然,实际的rwnd可根据应用程序实际读的速率在0到Wmax之间自适应:

  • 程序读得慢了,rcvbuff堆积,x变大,rwnd变小。
  • 程序读得快了,rcvbuff清空,x变小,rwnd趋向于Wmax。

简单有效!

可是为什么Linux却采用了一种截然不同的错误方式计算rwnd呢?我想是因为RFC675太陈年了,一般都以RFC793为准,但RFC793并没详细描述rwnd的计算。根本没几个人知道RFC675。

拥塞算法一直都希望用buffer堆积来识别拥塞并计算cwnd,但buffer堆积是端到端不可见的,不得不让RTT参与间接度量buffer堆积,但依然测不准。对于流控而言,rcvbuff堆积则是真实可观测的,流控范畴的拥塞控制便可用更加直接的方式:

  • rcvbuff在堆积,降速,rcvbuff在清空,加速。

说了这么多,最终还是要落地。我不怎么会编程,自己在填充一条长肥管道时只能采用手工hack的方式,如果要正式修改Linux内核TCP实现,我没能力也没兴趣,看到这篇文字的工人如果有兴趣可以实现上述那个公式,替换掉 rwnd = min(sysctl_tcp_rmem[2], copied_to_user) 这个算法:

rwnd = Wmax * (1 - queue_len/rcvbuff_size)^2。

若queue_len等于rcvbuff_size,则丢掉新收的skb。

好办法!具体的测试程序明天再写(我不会编程,只能写个简单的模拟,置入内核太麻烦,无能为力),今天先MARK。

若要这个新算法高效工作,需要TCP pacing。pacing是一种现代的传输方式,而burst则属于传统。参考下面的论文深入了解:
Understanding the Performance of TCP Pacing
最重要的一张图:

按照排队理论定性理解上图,但不要按照排队理论去计算,现实网络并不符合泊松分布。

长肥管道高利用率的关键是pacing,pacing匹配接收端的处理速率,有效减少对rcvbuff的依赖,和BBR模型一致。减少对buffer的依赖,是pacing相对于burst更现代的原因。

多思考一点,能否将RFC675中计算rwnd的公式反过来用于网络拥塞控制呢?完全可以,但需要改一改。由于端到端无法直接测量buffer用量,只能用RTT间接体现,这是什么?这不就是Vegas吗?BBR吸取了时延算法的精髓,将时延用作分母来测量带宽,这一切都来自历史源头,RFC675。

周末了,总结一下rwnd新算法。主要是挖了一下RFC675这座老坟,证明最初的TCP就是按照程序的读取能力自适应rwnd的,而不是按rcvbuff的硬限制确定rwnd,这是一个自然而然的想法。RFC675根本就没有几个人知道,RFC793倒是妇孺皆知,但793却没有描述rwnd算法,加上Linux太流行了,Linux当然说什么就是什么咯。但无论Linux再“正确”,它计算rwnd的算法也是错误的。自然而然的rwnd算法和河流类似,在上海不管是长江还是川杨河,都会满满流量自然汇入东海,入海口没有任何buffer,在山区山洪流入小溪的场景却相反,山洪在汇入小溪的地方会形成堰塞湖,这是一个巨大的buffer,让小溪慢慢消化,若堰塞湖溢出,将会带来灾难。今天先写个笔录,明天写个测试程序。

浙江温州皮鞋湿,下雨进水不会胖。

以上是关于TCP rwnd算法挖坟的主要内容,如果未能解决你的问题,请参考以下文章

新 TCP 流控

计算机网络之TCP协议流量拥塞控制算法原理:滑动窗口cwnd与rwnd

TCP拥塞控制

计算机网络 | 谈谈TCP的流量控制与拥塞控制

socket与Linux TCP

山洪灾害监测预警系统