填满TCP长肥管道

Posted dog250

tags:

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

填满TCP长肥管道很难,不信你可以试试。

难点在于:

  • 基于丢包的cc算法很难hold住窗口,随机丢包会降窗,长RTT导致窗口涨回来很慢。
  • BBR算法稍好一点,但需要很大的接收buffer,buffer等于BDP,对内存有高要求。

BBR说恶心了,本文与BBR无关,全部实验的cc算法均为CUBIC。

本文的要点是:

  • 长肥管道填不满,吞吐低,并不是rcvbuff不够大,而是rwnd计算方法错了。

直连两个25Gbps网卡,设置40ms延时,sendbuff足够,iperf打流只能到3Gbps~4Gbps,工人们接受这个结论,因为工人们都知道,这是因为recvbuff设置的不够大,将rmem[2]设置成超大,iperf即可到达23Gbps,这说明工人们的建议是对的,说到底还是sysctl_tcp_rmem[2]设置的不够大。

但这是不符合直觉的,见下图:

理论上iperf打流带宽和延时无关,但实际上显然是有关的。

抓包发现根因是rwnd太小,来看下rwnd小的原因。

理论上,rwnd的计算要考虑数据守恒,守恒的意思是:

  • 被读走多少数据就能进入多少数据。

守恒没有问题,但至少Linux TCP计算rwnd的方式是有问题的:

copied = read_by_socket
rwnd = free space = min(sysctl_tcp_rmem[2], copied)

数据守恒加了一个限制,这不仅限制了rwnd,还限制了socket读的速率,因为rcvbuff里最多也就sysctl_tcp_rmem[2]字节数据。

按照上图所示的直觉,rwnd是和rcvbuff无关的,rwnd应该取决于数据被读走的速率,它的值应该是:
rwnd = min(BltbwRTT, rate_of_readRTT)

rwnd的值可以远大于rcvbuff。以上基于rate_of_read计算rwnd的目标就是rcvbuff为0,而不是填满它。这和传统算法是相反的。传统算法以为填满rcvbuff就是最好的资源利用率,rcvbuff有个阈值,让rwnd恰好不超过它。

实现时不必去计算min(BltbwRTT, rate_of_readRTT),probe即可。不断加性增rwnd并监测rcvbuff堆积,当堆积达到阈值不再增加rwnd,保持rcvbuff堆积稳定在阈值内,过一段时间再次probe。

如果应用程序采用busy poll的方式,以上算法可很快探测到应用程序读数据的极限,维持在那个极限将获得最大吞吐率。

这实际就是BBR的思想搬到了端到端流控。传统基于丢包的cc将填满buffer为目标,而BBR则将不填buffer为目标,显然流控也可如此倒换。

下面是我发现这个问题的过程。

发现违反上图截面直觉的现象后,我观察了很低的CPU利用率,发送端dump出rwnd,cwnd,inflight:

inflight < rwnd < cwnd

显然是rwnd limited。于是在接收端将rwnd通告为原始rwnd值的2倍rwnd’ = rwnd*2,CPU利用率即上涨1倍,吞吐也增加1倍,此时rcvbuff里的free space并没改变,说明“到的多,读的也多”。问题是:

  • 为什么TCP接收端没能自己算出这rwnd’呢?

核对rcvbuff,free space之后,发现 rwnd = free space = sysctl_tcp_rmem[2] - copied 这个计算方法。而这算法限制死了程序读数据上限。

取消这个限制,我的做法是,为了达到“到的多,读的也多”,我尝试探测程序最快能读多快,为此,我不得不一点点增加rwnd去探测这个极限,显然这正是cc的思路,而拥塞点,就是程序。

我不晓得别的TCP实现如何,但大致也都和Linux TCP差不多。

看下效果,25Gbps网卡直连,40ms端到端延时,接收端rwnd算法没修改之前:

修改之后:

很炸的感觉,是不是?有了这个,以后再也不要调rcvbuff了。

这个思路适合端到端全链路,从一个磁盘经过某个应用程序,经由网络,再经过另一个应用程序,再到落盘,每一个环节都要遵循一个原则:

  • 主动保持buffer为0,而不是填满它。

下面是一些附属:
接收端查看和修改rwnd的脚本:

stap -ge 'probe kernel.function("__tcp_select_window").return  printf("%d\\n", $return)'
stap -ge 'probe kernel.function("__tcp_select_window").return  $return=123456'

打印rcvbuff数据:

STAP_PRINTF("recvbuf:%d  rwnd:%d  copy:%d\\n", sk->sk_rcvbuf, tp->rcv_wnd, tp->copied_seq - tp->rcvq_space.seq);

将cwnd设置为定值以bypass cc或者让reno增窗更快些:

#!/usr/local/bin/stap -g

%
#include <linux/tcp.h>
%

function _set_cwnd(skk:long, type:long)
%
	struct sock *sk = (struct sock *)STAP_ARG_skk;
	struct tcp_sock *tp = tcp_sk(sk);
	if (STAP_ARG_type == 0) 
		printk("########## retrans:%d\\n", tp->snd_cwnd);
	 else 
		printk("normal trans:%d\\n", tp->snd_cwnd);
	
        // 设置为定值
	tp->snd_cwnd = 292000;
%

probe kernel.function("tcp_write_xmit")

	_set_cwnd($sk, 1);


probe kernel.function("tcp_xmit_recovery")

	_set_cwnd($sk, 0);


function inc_cwnd(skk:long, skbb:long)
%
	struct sock *sk = (struct sock *)STAP_ARG_skk;
	struct sk_buff *skb = (struct sk_buff *)STAP_ARG_skbb;
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcphdr *th;

	if (skb->protocol != htons(ETH_P_IP))
		return;

	th = (struct tcphdr *)skb->data;
	if (ntohs(th->source) == 5001) 
		tp->snd_cwnd_cnt += 900;
	
%

probe kernel.function("tcp_ack").return

	inc_cwnd($sk, $skb);

跟友商一哥们儿一起探讨TCP传输优化问题,正好我自己也有这方面的一个大数据量长程同步的需求,在一起怼了一波TCP之后,彻底抛开了cc,把cwnd写死了再说,看看端到端的send/rcvbuff有没有啥好调的,果然抓到一条大鱼,不敢独享,分享一篇短文。

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

以上是关于填满TCP长肥管道的主要内容,如果未能解决你的问题,请参考以下文章

TCP rwnd算法挖坟

新 TCP 流控

TCP rwnd自适应

拥塞算法

《图解HTTP》-读数笔记

TCP系列34—窗口管理&流控—8缓存自动调整