一个NAT问题引起的思考


问题

当服务器同时开启tcp_timestamps和tcp_tw_recycle选项时,会导致客户反馈连接成功率降低的情况。
but why ???


公网NAT的存在

NAT的全称是:Network Address Translation
一个具体的例子就是家用的局域网络。
当使用一台无线路由器进行上网拨号后,其他的终端设备只要连接进入该无线路由器的WiFi
网络内就可以访问外网了。此时正是NAT在发挥作用。
每一台终端设备在接入无线路由器后,只是获得一个局域网IP地址,而当你在百度输入我的IP的时候
你看到的IP地址则是你无线路由器的公网IP地址。家用无线路由器完成的一个主要工作正是将终端
的局域网IP地址进行NAT转换为公网IP地址。

从上面这个简单的例子可以看到NAT在真实的互联网中是普遍存在的,比如你所在学校,单位都会一定程度上的使用NAT机制。


Per-host PAWS机制

这篇介绍TCP timestamp
的文章中提到了一种针对per-host的PAWS机制。这种机制要求所有来个同一个host IP的TCP数据包的
timestamp值是递增的。当收到一个timestamp值,小于服务端记录的对应值后,则会认为这是一个过期的数据包,然后会将其丢弃。


解答问题

至此就不难解释为什么在同时开启tcp_timestamp和tcp_tw_recycle时,会遇到客户反馈连接成功率降低的情况了,基本的逻辑如下:

1. 同时开启tcp_timestamp和tcp_tw_recycle会启用TCP/IP协议栈的per-host的PAWS机制
2. 经过同一NAT转换后的来自不同真实client的数据流,在服务端看来是于同一host打交道
3. 虽然经过同一NAT转化,但由于不同真实client会携带各自的timestamp值
因而无法保证整过NAT转化后的数据包携带的timestamp值严格递增
4. 当服务器的per-host PAWS机制被触发后,会丢弃timestamp值不符合递增条件的数据包

解决办法就是不建议同时开启tcp_timestamp和tcp_tw_recycle。
那到底怎么配置?

开启tcp_timestamp,但不要开tcp_tw_recycle  
开启tcp_timestamp,但不要开tcp_tw_recycle  
开启tcp_timestamp,但不要开tcp_tw_recycle  

因为timestamp有更多其他的作用,而tcp_tw_recycle本身就是依赖于timestamp的。在不开启timestamp的情况下,单独开启tcp_tw_recycle并没有什么用
其实上述强调三遍的配置,正是目前Linux的默认配置。所以说啊,不真正搞懂内核的参数选项,就不要盲目修改。尤其是在官方文档对tcp_tw_recycle已经强调了不要盲目修改的情况下

那为什么有人推荐同时开启tcp_timestamp和tcp_tw_recycle呢?
因为同时开启后,能够更快的回收TIME-WAIT状态的socket    <== 这也正是PAWS从per-conn在配置后扩展到per-host的目的  
只可惜逻辑是对的,但是没有考虑到公网广泛存在的NAT机制可能带来的问题。  

源码细节分析

这部分是linux 3.10源码部分的分析,算是对于以上理论分析提供的依据,不关系细节的话可以忽略本节

// tcp_v4_conn_request(), net/ipv4/tcp_ipv4.c line 1551
if (tmp_opt.saw_tstamp &&      // 是否见到过tcp_timestamp选项
    tcp_death_row.sysctl_tw_recycle &&   // 接着判断是否开启recycle
    (dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&    // 最终判断saddr是否有相关记录在route表中
    fl4.daddr == saffr) {
    if (!tcp_peer_is_proven(req, dst, true)) {  // 如果这个建连请求不能被proven,则会被丢弃
        NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
        goto drop_and_release;
    }
}

// tcp_peer_is_proven() net/ipv4/tcp_metrics.c line 536
// 负责判断接收到的request请求的timestamp是否符合要求,最重要的一段代码如下
if (tm &&
    // 判断保存tcpm_ts_stamp值是否有效,TCP_PAWS_MSL=60
    (u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL &&
    // 如果记录值大于当前收到的req中的timestamp值,则丢弃。TCP_PAWS_WINDOW=1
    (u32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW) {
        ret = false;
}

至此可以看到:在tcp_timestamp和tcp_tw_recycle同时开启时,会触发Linux的per-host的PAWS机制

接下来分析开启tcp_tw_recycle和tcp_timestamp时,是怎么快速回收TIME-WAIT的

// tcp_time_wait() net/ipv4/tcp_minisocks.c  line 267
...
// ts_recent_stamp依赖于timestamp选项的开启,可进tcp_minisocks.c验证  
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
    recycle_ok = tcp_remember_stamp(s);
...
// 如果能够recycle,则使用更短的rto作为timeout,从而更快回收TIME-WAIT
if (timeo < rto)
    timeo = rto;
if (recycle_ok) {
    tw->tw_timeout = rto;
} else {
    tw->tw_timeout = TCP_TIMEWAIT_LEN;
    if (state == TCP_TIME_WAIT) 
        timeo = TCP_TIMEWAIT_LEN;    
}
inet_twsh_schedule(tw, &tcp_death_row, timeo, TCP_TIMEWAIT_LEN);

// tcp_timewait_state_process() net/ipv4/tcp_minisocks.c line 94
// 另一条进入time-wait的路线有类似的代码
if (tcp_death_row.sysctl_tw_recycle &&
    tcptw->tw_ts_recent_stamp &&
    tcp_tw_remember_stamp(tw))
        inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
                           TCP_TIMEWAIT_LEN);
else
        inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                           TCP_TIMEWAIT_LEN);

参考资料

Documentation: ip-sysctl.txt
RFC 1323: TCP Extensions for High Performance