Unix Like, 操作系统

千兆公网带宽的TCP sendbuffer与receivebuffer调整

不知道有多少人留意过,使用千兆公网带宽传输数据时,只要距离稍微远点,就无法跑满千兆。对于这种情况,可能第一反应是“线路繁忙,公网带宽不足,跑不满很正常”。不过如果你有在不同地区的多台千兆设备间传数据的经历的话,你大概会发现,最大传输速度跟延迟成负相关。

首先丢出个公式:

TCP有ARQ机制,已发出的数据要收到ACK后才能丢弃。因为至少要等一个RTT才能收到ACK,所以sendbuffer要至少能存放一个RTT内能发出的数据量。另外,TCP的拥塞控制也会根据接收方的receivebuffer大小限制数据的发出速率。因此,如果想达到100%的带宽利用率,双方的buffer size都得符合上述公式。

根据Linux kernel的document,TCP sendbuffer max size默认为4MB,所以RTT低于多少时才能保证千兆呢?

RTT = 32ms,只要超过32ms,你的千兆带宽就不能跑满。万兆网络的话,这个数还得除以十:3.2ms,基本上只能在局域网内跑满(我猜即使buffer够了,万兆在公网还真的跑不满……)。

这里再丢出两条公式:

为什么会有另外两条公式?因为开头给出的只是理论值,实际上的buffer,并非仅用来储存payload,因此需要扣除非payload部分。

sendbuffer:

第一条计算sendbuffer的公式的系数0.7,是我测出来的,并不是准确值,为什么会是接近这个值,我还在寻找答案。

receivebuffer:

计算receivebuffer的可能有点复杂,对于tcp_adv_win_scale,kernel文档解释如下:

tcp_adv_win_scale – INTEGER
Count buffering overhead as bytes/2^tcp_adv_win_scale
(if tcp_adv_win_scale > 0) or bytes-bytes/2^(-tcp_adv_win_scale),
if it is <= 0.

Possible values are [-31, 31], inclusive.

Default: 1

值的应用以及公式的出处,可以在这里找到:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/net/tcp.h?h=v5.10.12#n1395

简单来讲,就是receivebuffer有一部分是作为buffering overhead的,其大小通过tcp_adv_win_scale进行控制。所以扣除这一部分才是实际用来存放payload的size。

buffer在内核中的设定:

TCP的sendbuffer与receivebuffer分别在net.ipv4.tcp_wmem和net.ipv4.tcp_rmem中设定:

三个值分别是min,default和max,kernel会在min和max自动调整,程序也可以通过setsockopt() API设定。

下面是对buffer的调整尝试:

A到B之间的RTT是146ms,A的sendbuffer max size是默认4MB,测试下B从A下载文件的速度:

调整A的sendbuffer:

再到B上测试下速度:

快的确是快了,但只有21.2 MB/s,好像并不是预期的结果,如果你抓包看看A和B之间的数据包,你会发现,B的窗口大小上限是3 MB,见下图的Win:

所以这限制了未被ACK的数据量,A不能发送超出B接收能力的量,通过窗口大小和RTT可以反推出速度:3MB / 0.146s ≈ 20.5MB/s,接近curl的速度。

TCP窗口大小是receivebuffer的具体表现,查看一下B的receivebuffer:

如果把数值代入receivebuffer的公式,你可以算得receivebuffer的值是接近6MB,跟当前tcp_rmem的max size一致,所以B目前的行为是符合这两个数值的预期的。

下面调整下B的receivebuffer:

这时抓包,可以看到B的窗口大小上限是18M:

0.146s * 112MB/s = 16.352,可以确定目前的瓶颈是物理带宽而不是buffer。

再找了个与A的RTT是68ms的C:

A的sendbuffer已经调整过了,足以应付68ms的RTT,但C的receivebuffer还没调整:

新的速度接近原来的两倍,把数值代入公式:0.068s * rate = 12MB – (12MB / 2^1),得rate = 88.2353MB/s,接近实际速率。

另外,这里再提一个参数“tcp_slow_start_after_idle”,kernel document的解释如下:

tcp_slow_start_after_idle – BOOLEAN
If set, provide RFC2861 behavior and time out the congestion
window after an idle period. An idle period is defined at
the current RTO. If unset, the congestion window will not
be timed out after an idle period.

Default: 1

这个功能默认是启用的,连接空闲一个RTO后,后面会从新进入慢启动状态,长连接要保证稳定性能的话,置0。