Linux · 2014-01-20

tcp_tw_recycle和tcp_timestamps导致connect失败问题

近来线上陆续出现了一些connect失败的问题,经过分析试验,最终确认和proc参数tcp_tw_recycle/tcp_timestamps相关;

1. 现象

第一个现象:模块A通过NAT网关访问服务S成功,而模块B通过NAT网关访问服务S经常性出现connect失败,抓包发现:服务S端已经收到了syn包,但没有回复synack;另外,模块A关闭了tcp timestamp,而模块B开启了tcp timestamp;

第二个现象:不同主机上的模块C(开启timestamp),通过NAT网关(1个出口ip)访问同一服务S,主机C1 connect成功,而主机C2 connect失败;

 

2. 分析

根据现象上述问题明显和tcp timestmap有关;查看linux 2.6.32内核源码,发现tcp_tw_recycle/tcp_timestamps都开启的条件下,60s内同一源ip主机的socket connect请求中的timestamp必须是递增的。

源码函数:tcp_v4_conn_request(),该函数是tcp层三次握手syn包的处理函数(服务端);

源码片段:

if (tmp_opt.saw_tstamp &&

tcp_death_row.sysctl_tw_recycle &&

(dst = inet_csk_route_req(sk, req)) != NULL &&

(peer = rt_get_peer((struct rtable *)dst)) != NULL &&

peer->v4daddr == saddr) {

if (get_seconds() < peer->tcp_ts_stamp + TCP_PAWS_MSL &&

(s32)(peer->tcp_ts – req->ts_recent) >

TCP_PAWS_WINDOW) {

NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);

goto drop_and_release;

}

}

tmp_opt.saw_tstamp:该socket支持tcp_timestamp

sysctl_tw_recycle:本机系统开启tcp_tw_recycle选项

TCP_PAWS_MSL:60s,该条件判断表示该源ip的上次tcp通讯发生在60s内

TCP_PAWS_WINDOW:1,该条件判断表示该源ip的上次tcp通讯的timestamp 大于 本次tcp

 

分析:主机client1和client2通过NAT网关(1个ip地址)访问serverN,由于timestamp时间为系统启动到当前的时间,因此,client1和client2的timestamp不相同;根据上述syn包处理源码,在tcp_tw_recycle和tcp_timestamps同时开启的条件下,timestamp大的主机访问serverN成功,而timestmap小的主机访问失败;

 

参数:/proc/sys/net/ipv4/tcp_timestamps – 控制timestamp选项开启/关闭

/proc/sys/net/ipv4/tcp_tw_recycle – 减少timewait socket释放的超时时间

 

3. 解决方法

echo 0 > /proc/sys/net/ipv4/tcp_tw_recycle;

tcp_tw_recycle默认是关闭的,有不少服务器,为了提高性能,开启了该选项;

为了解决上述问题,个人建议关闭tcp_tw_recycle选项,而不是timestamp;因为 在tcp timestamp关闭的条件下,开启tcp_tw_recycle是不起作用的;而tcp timestamp可以独立开启并起作用。

源码函数:  tcp_time_wait()

源码片段:

if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)

recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);

……

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_twsk_schedule(tw, &tcp_death_row, timeo,

TCP_TIMEWAIT_LEN);

 

timestamp和tw_recycle同时开启的条件下,timewait状态socket释放的超时时间和rto相关;否则,超时时间为TCP_TIMEWAIT_LEN,即60s;

 

内核说明文档 对该参数的介绍如下:

tcp_tw_recycle – BOOLEAN

Enable fast recycling TIME-WAIT sockets. Default value is 0.

It should not be changed without advice/request of technical

experts.

 

 

 

类似的故障场景:
几台通过NAT访问一台服务器,服务器上开启了tcp_tw_recycle用于TIME_WAIT的快速回收故障现象和分析步骤:
1) 通过NAT出口的客户端经常请求Web服务器无响应,telnet服务器端口不通,但是可以ping通。同一机房,有独立IP地址的服务器不存在这样的问题;
2) 在服务器抓包,发现服务端可以收到客户端的SYN请求,但是没有回应SYN,ACK,也就是说内核直接将包丢弃了。

解决方法:
1) 关闭服务其端的tcp_timestamps,故障解决,但是这么做存在安全和性能隐患;
2) 关闭tcp_tw_recycle,故障也可以解决。推荐NAT环境下的机器不要开启该选项;
3) 也就是说这两个参数不可能同时启用。

后记:
1) 当tcp_tw_recycle和tcp_timestamps同时打开时会激活TCP的一种隐藏属性:缓存连接的时间戳。60秒内,同一源IP的后续请求的时间戳小于缓存中的时间戳,内核就会丢弃该请求。NAT只改IP地址信息,但不会改变timestamp(TCP的时间戳不是系统时间,而是系统启动的时间uptime,所以两台机器的的TCP时间戳一致的可能性很小),所以很容易造成连接失败的情况。

2) TIME_WAIT状态是用于保障连接正常关闭的,并不会消耗过多资源。在高并发环境中tcp_tw_recycle和tcp_tw_reuse经常被打开用户快速回收和重用TIME_WAIT状态的socket,在资源有限的情况下这么多也无可厚非,不过也应该知道这么做会带来的后果。

记一次TIME_WAIT网络故障

最近发现一个PHP脚本时常出现连不上服务器的现象,调试了一下,发现是TIME_WAIT状态过多造成的,本文简要介绍一下解决问题的过程。

 

遇到这类问题,我习惯于先用strace命令跟踪了一下看看:

shell> strace php /path/to/file
EADDRNOTAVAIL (Cannot assign requested address)

从字面结果看似乎是网络资源相关问题。这里顺便介绍一点小技巧:在调试的时候一般是从后往前看strace命令的结果,这样更容易找到有价值的信息。

查看一下当前的网络连接情况,结果发现TIME_WAIT数非常大:

shell> netstat -ant | awk '
    {++s[$NF]} END {for(k in s) print k,s[k]}
'

补充一下,同netstat相比,ss要快很多:

shell> ss -ant | awk '
    {++s[$1]} END {for(k in s) print k,s[k]}
'

重复了几次测试,结果每次出问题的时候,TIME_WAIT都等于28233,这真是一个魔法数字!实际原因很简单,它取决于一个内核参数net.ipv4.ip_local_port_range:

shell> sysctl -a | grep port
net.ipv4.ip_local_port_range = 32768 61000

因为端口范围是一个闭区间,所以实际可用的端口数量是:

shell> echo $((61000-32768+1))
28233

问题分析到这里基本就清晰了,解决方向也明确了,内容所限,这里就不说如何优化程序代码了,只是从系统方面来阐述如何解决问题,无非就是以下两个方面:

首先是增加本地可用端口数量。这点可以用以下命令来实现:

shell> sysctl net.ipv4.ip_local_port_range="10240 61000"

其次是减少TIME_WAIT连接状态。网络上已经有不少相关的介绍,大多是建议:

shell> sysctl net.ipv4.tcp_tw_reuse=1
shell> sysctl net.ipv4.tcp_tw_recycle=1

注:通过sysctl命令修改内核参数,重启后会还原,要想持久化可以参考前面的方法。

这两个选项在降低TIME_WAIT数量方面可以说是立竿见影,不过如果你觉得问题已经完美搞定那就错了,实际上这样可能会引入一个更复杂的网络故障。

关于内核参数的详细介绍,可以参考官方文档。我们这里简要说明一下tcp_tw_recycle参数。它用来快速回收TIME_WAIT连接,不过如果在NAT环境下会引发问题。

RFC1323中有如下一段描述:

An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the PAWS mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender’s timestamp clock. Such an extension is not part of the proposal of this RFC.

大概意思是说TCP有一种行为,可以缓存每个连接最新的时间戳,后续请求中如果时间戳小于缓存的时间戳,即视为无效,相应的数据包会被丢弃。

Linux是否启用这种行为取决于tcp_timestamps和tcp_tw_recycle,因为tcp_timestamps缺省就是开启的,所以当tcp_tw_recycle被开启后,实际上这种行为就被激活了,当客户端或服务端以NAT方式构建的时候就可能出现问题,下面以客户端NAT为例来说明:

当多个客户端通过NAT方式联网并与服务端交互时,服务端看到的是同一个IP,也就是说对服务端而言这些客户端实际上等同于一个,可惜由于这些客户端的时间戳可能存在差异,于是乎从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。如果发生了此类问题,具体的表现通常是是客户端明明发送的SYN,但服务端就是不响应ACK,我们可以通过下面命令来确认数据包不断被丢弃的现象:

shell> netstat -s | grep timestamp
... packets rejects in established connections because of timestamp

安全起见,通常要禁止tcp_tw_recycle。说到这里,大家可能会想到另一种解决方案:把tcp_timestamps设置为0,tcp_tw_recycle设置为1,这样不就可以鱼与熊掌兼得了么?可惜一旦关闭了tcp_timestamps,那么即便打开了tcp_tw_recycle,也没有效果。

好在我们还有另一个内核参数tcp_max_tw_buckets(一般缺省是180000)可用:

shell> sysctl net.ipv4.tcp_max_tw_buckets=10000

通过设置它,系统会将多余的TIME_WAIT删除掉,此时系统日志里可能会显示:「TCP: time wait bucket table overflow」,多数情况下不用太在意这些信息。

总体来说,这次网络故障本身并没什么高深之处,本不想罗罗嗦嗦写这么多,不过拔出萝卜带出泥,在过程中牵扯的方方面面还是值得大家品味的,于是便有了这篇文字。