您的位置:首页 > 理论基础 > 计算机网络

TCP零窗口更新与超时重传联合优化的packetdrill确认

2016-07-24 10:08 369 查看
早先写了一篇《关于TCP Zero Window Update感知的非常棒的优化》,大意是当数据接收端由于缓冲区配额问题导致丢包时,发送端是无法区分这种情况和拥塞丢包的,最终的结果依然是超时退避重传,在接收端发送窗口更新并被发送端收到的时候,这个窗口更新不会打扰正在退避重传中的逻辑,如果有退避等待正在进行,且这个等待重传的数据包占据了通告窗口,那么在重传定时器到期之前,将不会有任何数据被发送。
我为什么一直纠结于这个事,因为我不信任除了ACK时钟之外的任何外部时钟,在接收端通告了零窗口之后,相当于ACK丢失了,此后驱动TCP前进的是定时器这种外部时钟,比如零窗口探测定时器,重传定时器等,我希望,只要接收端有窗口可用了,就马上停止这些外部时钟,重启ACK自时钟。仅此而已。
之前那篇文章已经说的够详细了,本文只是用packetdrill对其进行确认。

1.明确一下这个问题是可能发生的

你在内核日志里看到过以下的信息吗?
TCP: Peer 192.0.2.1:50648/8080 unexpectedly shrunk window 1105539947:1105542947 (repaired)
这是怎么回事呢?
想知道怎么回事就grep内核代码吧。最终我们发现它是在定时器超时的时候被调用的,如果发现此时的窗口是零,那么就打印上面的一句话,意即对端关闭了窗口,此时重传一个重传队列头部的一个数据包作为”零窗口探测“(可以不再单独发零窗口探测了!)。
这说明了什么?这说明,对端窗口变为零的时候,发送端是存在”有数据包正在重传逻辑“中的可能性的。曾经,我在一台高压力服务器上看到过大量的上述”unexpectedly shrunk window“信息,当时,我无措,此时,我看到了希望,大大的希望。
现在的问题是,既然窗口为零的时候有数据包正在等待被重传,那么当对端窗口变成不为零的时候,这些等待重传的数据包是继续等待到超时呢?还是说马上结束等待,立马重传呢?
其实内核已经有了一些启发信息,关键就在这个”unexpectedly“上!为什么是unexpectedly呢?因为只有在对端窗口为非零的时候,发送端才能发送数据,也就是说接收端一定拥有足够的空间容纳被发送的数据,现在窗口变成零了,内核认为一定是接收端异常行为导致,接收端当初承诺了N个窗口,现在却收回了承诺,造成了丢包,因此这就是unexpectedly。
然而内核并没有意识到这个丢包不太可能是由于拥塞导致的!如果是由于拥塞导致的,接收端承诺N个窗口,实际上收到的数据由于拥塞丢包会小于N,且如果存在乱序接收,在接收端发现配额吃紧的时候,会回收乱序队列,从而腾出一些配额,这种情况下,事实上是”更加unexpectedly“的!不幸的是,内核没有发现这个想象。
窗口更新来了(且不管是什么触发了这个更新,也可能是对端主动发送了些数据顺带了窗口更新),窗口从零变非零了,此时重传队列头的数据包仍在退避等待中,窗口无法向后滑动...

2.重现这个现象

重现这个现象是比较困难的,这个我也已经说过。我需要构造一个时序来操作内核的内部缓冲区!这对于把协议栈作为黑盒子的我而言,是无能力为,必须白盒话,也就是直接操作协议栈来手工构造这个序列,比如在发现窗口为0的时候,手工发送一个skb制造超时重传。使用tcpprobe或者自己写Netfilter钩子,这也不难。
不过幸运的是,我们有更好的方案,那就是用packetdrill。脚本如下:
// Establish a connection.
0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.000 < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
0.000 > S. 0:0(0) ack 1 win 5840 <mss 1460,nop,nop,sackOK,nop,wscale 7>

// RTT 100ms
0.200 < . 1:1(0) ack 1 win 320
0.200 accept(3, ..., ...) = 4

// Send 10 data segments.
0.200 write(4, ..., 10000) = 10000

// 窗口更新为0,确认1001,制造丢包现场,引发超时退避重传
0.250 < . 1:1(0) ack 1001 win 0

// 在2秒后发送窗口更新,腾出窗口,此时并无新数据可发
2.250 < . 1:1(0) ack 1001 win 1000

// Send 10 data segments.
2.960 write(4, ..., 10000) = 10000


我先给出优化后的效果:



为了证明这个优化是有效的,我回退到优化前的标准TCP实现,执行同样的脚本,抓包:



至于说是怎么优化的?很简单,直接修改tcp_ack_update_window即可:
static int tcp_ack_update_window(struct sock *sk, struct sk_buff *skb, u32 ack,
u32 ack_seq)
{
struct tcp_sock *tp = tcp_sk(sk);
int flag = 0;
u32 nwin = ntohs(tcp_hdr(skb)->window);

if (likely(!tcp_hdr(skb)->syn))
nwin <<= tp->rx_opt.snd_wscale;

if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
flag |= FLAG_WIN_UPDATE;
tcp_update_wl(tp, ack_seq);
if (tp->snd_wnd != nwin) {
#if 1  // 优化代码
{
u32 old = tp->snd_wnd;
u32 new = nwin;
tp->snd_wnd = nwin;
if (old < new && old <= 2*tp->mss_cache) {
struct inet_connection_sock *icsk = inet_csk(sk);
printk("hit! owin: %u, nwin: %u\n", old, new);
icsk->icsk_retransmits++;
tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
}
}
#else
tp->snd_wnd = nwin;
#endif
/* Note, it is the only place, where
* fast path is recovered for sending TCP.
*/
tp->pred_flags = 0;
tcp_fast_path_check(sk);
if (nwin > tp->max_window) {
tp->max_window = nwin;
tcp_sync_mss(sk, inet_csk(sk)->icsk_pmtu_cookie);
}
}
}

tp->snd_una = ack;
return flag;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: