作者:henrystark [email protected]: http://henrystark.blog.chinaunix.net/日期:20140315本文可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接。如有错讹,烦请指出。这篇文章是我在实验过程中用到的方法总结。之所以公开,是因为我的实验方法也借鉴了他人的源码。有必要把这份开源精神传递下去。但是出于保密和个人权利保护需要,并不能说明完整的方法和代码。有需要交流的同行请自行找我的联系方式。TCP接收端流量控制TCP发送端有拥塞控制,根据网络状况调节cwnd,算法核心在于“拥塞状态”的控制,避免网络过度负载,选择合适的发送窗口。TCP接收端有流量控制,目的不在约束发送端的包投递速率,而在给予发送端足够大的通告窗口,同时顾及到本端的接收速率。这样做的必要原因有:(1)糊涂窗口综合症,避免有一个字节空余就通告一个字节的场景,因为这样会严重降低有效负载率。【注 1】(2)高速链路,例如带宽10Gbps【注 2】,普通PC的总线速率只有6Gbps,过快的包投递可能会淹没缓存。这时候流控的作用就得以体现。在普通网络场景中,通告窗口awnd普遍大于cwnd,这是为了把控制权尽量交给发送方,接收方不成为限制因子。分配合适的接收窗口需要估算对端的cwnd,一般而言,接收窗口大于cwnd的两倍。1.接收端估算cwnd和RTT的原理cwnd(拥塞窗口)、awnd(通告窗口)都和RTT(往返延时)关联。要估算cwnd,调节适当的awnd,首先需要估算RTT。RTT的测量大致有两种方法【注 3】:1.1 使用时间戳。TCP header有timestamp和echo timestamp,发送端投递数据包、接收端回复ACK都需要携带这些数据。接收方在回复ACK时,会打入时间戳,由发送方回显。接收方在收到有时间戳回显的数据包以后,可以用当前系统时间减去时间戳,即可得出RTT。流程如下: timestamp = time_TCP receiver -----------------------> TCP sender ACK packet | | echo timestamp = time_ | 计算RTT这种测量RTT的策略相当简单,下面来分析优缺点。优点:在平稳流量下,策略简单,RTT准确。平稳流量指的是没有丢包的情况,一个数据包1500byte,忽略网卡发送消耗的时间,RTT相当可靠。实验:我设置RTT为12ms,内核打印时间为13ms。缺点:不支持时间戳选项时不可用。这种方法在丢包情况下够不够可靠呢?丢包情况下,可能经过了较长的时间,数据包才发送。具体情形可以分为:(1)ACK丢失。(2)数据包丢失。(3)发送方等待较长时间才发送新的数据包。(4)最严重的,超时。有丢包时,RTT计算过程如下: timestamp = time1TCP receiver --------------------------------------------------> TCP sender | ACK packet, ack_seq = 9000 | | Data packet, seq = 9000 | | drop by Switch | | | | echo timestamp = time2 |计算RTT上面列举的四种情况,能造成RTT测量偏大的只有后两种。超时情况下,发送端没有收到ACK,这时重发数据包的时间戳会造成RTT突发增大。除了超时,还有没有发送方等待较长时间才投递数据包的情况呢?也确实有,为了提升效率,发送端通常在数据负荷不够的情况下,会等待一段时间。实际上,关于丢包和有delay情况下的RTT测量,rfc1072: TCP Extensions for Long-Delay Paths 4.2节【引用 1】 早有讨论。rfc1072主要论述了发送端测量RTT的方式,接收端使用时间戳测量时,原理和发送端是一样的。当然,任何事情都少不了例外,我在实验过程中,确实看到接收端用时间戳测量RTT有偏大的情况,而且不止一次【注 5】。源码,位于linux-3.2.18/net/ipv4/tcp_input.c:static inline void tcp_rcv_rtt_measure(struct tcp_sock *tp){ if (tp->rcv_rtt_est.time == 0) //第一次接收到数据,三次握手之后,才开始传送数据的阶段 goto new_measure; if (before(tp->rcv_nxt, tp->rcv_rtt_est.seq)) //这里是判断数据量是否够多,如果有awnd这么多,就可以更新RTT,其中rcv_nxt是接收窗口的边界,表示期望接收的下一个数据包 return; tcp_rcv_rtt_update(tp, jiffies - tp->rcv_rtt_est.time, 1); //更新RTT,还是用jiffesnew_measure: tp->rcv_rtt_est.seq = tp->rcv_nxt + tp->rcv_wnd; tp->rcv_rtt_est.time = tcp_time_stamp;}1.2 不使用时间戳这种方法原理也很简单,判断接收端是否已经收到了一个接收窗口的数据。把这个时间间隔作为一个RTT。然而,这种方法有明显的缺陷,就是RTT偏大,发送端cwnd通常小于接收端awnd。并且这种方法在丢包时,RTT测量也会偏大,因为丢包时,收取一个满窗的数据可能要等很久。static inline void tcp_rcv_rtt_measure_ts(struct sock *sk, const struct sk_buff *skb){ struct tcp_sock *tp = tcp_sk(sk); if (tp->rx_opt.rcv_tsecr && //一系列选项判断,是否支持时间戳,数据包是否大于MSS (TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq >= inet_csk(sk)->icsk_ack.rcv_mss)) tcp_rcv_rtt_update(tp, tcp_time_stamp - tp->rx_opt.rcv_tsecr, 0);}统计RTT之后,就可以估算发送端拥塞窗口。第二种方法估算cwnd显然偏大。第一种方法比较靠谱。2.接收缓存的动态调节2.1 更新RTT这些源码很容易懂,无关紧要,注意看函数上面的注释,这里写出了方法的起源:DRS【引 2】。/* Receiver "autotuning" code. * * The algorithm for RTT estimation w/o timestamps is based on * Dynamic Right-Sizing (DRS) by Wu Feng and Mike Fisk of LANL. * * * More detail on this code can be found at * , * though this reference is out of date. A new paper * is pending. */static void tcp_rcv_rtt_update(struct tcp_sock *tp, u32 sample, int win_dep){ u32 new_sample = tp->rcv_rtt_est.rtt; long m = sample; //注意,Linux内核为了避免浮点运算,RTT采样都是按8倍存储的。 //至于为什么是8倍,涉及到排队论的延时采样分析。至于为什么不用浮点数,自行google。 //所以看到">>3" "> 3); new_sample += m; } else { m rcv_rtt_est.rtt != new_sample) tp->rcv_rtt_est.rtt = new_sample;}2.2 缓存调节这才是通告窗口的取值来源,缓存要适当调节,适应发送速率的需要。把缓存通告出去,就是通告窗口了。/* * This function should be called every time data is copied to user space. * It calculates the appropriate TCP receive buffer space. */void tcp_rcv_space_adjust(struct sock *sk){ struct tcp_sock *tp = tcp_sk(sk); int time; int space; if (tp->rcvq_space.time == 0) goto new_measure; //只有time大于一个RTT,才调节缓存。调节周期就是一个RTT time = tcp_time_stamp - tp->rcvq_space.time; if (time rcv_rtt_est.rtt >> 3) || tp->rcv_rtt_est.rtt == 0) return; //接收缓存应该大于一个RTT内接收数据的两倍 space = 2 * (tp->copied_seq - tp->rcvq_space.seq); space = max(tp->rcvq_space.space, space); if (tp->rcvq_space.space != space) { int rcvmem; tp->rcvq_space.space = space; if (sysctl_tcp_moderate_rcvbuf && !(sk->sk_userlocks & SOCK_RCVBUF_LOCK)) { int new_clamp = space; /* Receive space grows, normalize in order to * take into account packet headers and sk_buff * structure overhead. */ space /= tp->advmss; //这里是对space做处理,变成MSS的整数倍 if (!space) space = 1; rcvmem = SKB_TRUESIZE(tp->advmss + MAX_TCP_HEADER); //一个SKB的size while (tcp_win_from_space(rcvmem) advmss) rcvmem += 128; space *= rcvmem; space = min(space, sysctl_tcp_rmem[2]); if (space > sk->sk_rcvbuf) { sk->sk_rcvbuf = space; /* Make the window clamp follow along. */ tp->window_clamp = new_clamp; //更新窗口增长上限 } } }new_measure: tp->rcvq_space.seq = tp->copied_seq; tp->rcvq_space.time = tcp_time_stamp;}注解:【1】糊涂窗口综合症,为了避免一字节通告窗口的奇葩现象(有效负载过低),需要对TCP做出改进。http://www.tcpipguide.com/free/tTCPSillyWindowSyndromeandChangesTotheSlidingWindow.htmhttp://en.wikipedia.org/wiki/Sillywindow_syndrome【2】数据中心网络带宽极高,10Gbps已经捉襟见肘。http://www.d1net.com/datacenter/news/237774.html【3】RTT测量方法和问题。 Zhang [Zhang86], Jain [Jain86] and Karn [Karn87]。【4】ACK由数据包驱动,数据包由ACK驱动,TCP是一个控制闭环系统。由此可推出的结论是:接收端如果没有收到三个以上的数据包,是无法触发重复ACK,知会发送端重传的。这种情况只有等到RTO才能重传。这也就是TCP窗口尾丢包问题,简称TLP。Google和Taobao hritian都对此做了改进。【5】准确来说,流量过小的情况,用时间戳测量都是不准的,这时候应该用第二种的方法测量。我在实验中看到RTT有突发2-3倍以上的情况,说明时间戳方法确实有缺陷。引用:【1】http://tools.ietf.org/html/rfc1072【2】Fisk M, Feng W. Dynamic right-sizing in TCP[J]. http://lib-www. lanl. gov/la-pubs/00796247. pdf, 2001: 2. 11-06 06:27