tcp_tw_reuse、tcp_tw_recycle
http://www.cnblogs.com/lulu/p/4149312.html
http://www.cnxct.com/coping-with-the-tcp-time_wait-state-on-busy-linux-servers-in-chinese-and-dont-enable-tcp_tw_recycle/?utm_source=tuicool&utm_medium=referral
net.ipv4.tcp_tw_reuse = 0 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭 net.ipv4.tcp_tw_recycle = 0 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭 net.ipv4.tcp_fin_timeout = 60 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间(可改为30,一般来说FIN-WAIT-2的连接也极少
查看机器的log,发现某个后端服务异常,代码使用了短连接请求后端服务,并在失败时自动重试,创建socket时发生错误。查看机器的网络状态,发现有大量的TIME_WAIT状态。
统计机器的TIME_WAIT状态数量有几个命令,最简单的是 cat /proc/net/sockstat
sockets: used 2861
TCP: inuse 603 orphan 0 tw 19 alloc 795 mem 339
UDP: inuse 985 mem 557
如果要查看更详细的状态统计,可以使用netstat 或者ss 加 awk 来处理
netstat -ant |awk '{if(NR>1)++s[$NF]} END {for(k in s) print k,s[k]}'
ss -ant |awk '{if(NR>1)++s[$1]} END {for(k in s) print k,s[k]}'
推荐使用ss命令,当socket数量很大的时候,ss会快很多。
因此,原因很清楚了,是短连接在后端服务异常时大量产生的TIME_WAIT状态导致创建文件描述符失败,不能处理请求。
这种情况通常的处理建议是打开tcp_tw_recycle 或者tcp_tw_reuse 选项,那么是否有效,还会不会有什么坑?
TIME_WAIT是在连接断开时产生,先看下连接断开的过程:
上面就是常说的连接断开四次挥手的过程,TIME_WAIT出现在主动断开连接方,那它存在的意义是什么呢?
stevens在unix网络编程里边讲到有两点:
1、保证TCP连接关闭的可靠性。如果最终发送的ACK丢失,被动关闭的一端会重传最终的FIN包,如果执行主动关闭的一端没有维护这个连接的状态信息,会发送RST包响应,导致连接不正常关闭。
2、允许老的重复分组在网络中消逝。假设在一个连接关闭后,发起建立连接的一端(客户端)立即重用原来的端口、IP地址和服务端建立新的连接。老的连接上的分组可能在新的连接建立后到达服务端,TCP必须防止来自某个连接的老的重复分组在连接终止后再现,从而被误解为同一个连接的化身。要实现这种功能,TCP不能给处于TIME_WAIT状态的连接启动新的连接。
TIME_WAIT的时长通常定义成2*MSL,MSL表示报文在网络上存在的最长时间,如果超过这个时间,报文将被丢弃。linux下TIME_WAIT被定义在tcp.h中,时间是60s,除非重新编译内核,否则不能修改。
/* how long to wait to destroy TIME-WAIT state, about 60 seconds*/
#define TCP_TIMEWAIT_LEN (60*HZ)
如果每秒有1000个请求,在60秒内产生的TIME_WAIT就有60000个,要控制或者减少TIME_WAIT的数量,协议栈提供了tcp_tw_recycle、tcp_tw_reuse、tcp_max_tw_buckets这几个选项,下面逐一分析。
tcp_tw_recycle
linux协议栈实现的时候提供了一种快速回收TIME_WAIT状态的机制,不用等待2MSL的时间,只要等待一个重传的时间即可回收,在idc内部,这个时间极短,可能不到1ms。但是新建立的连接可能存在风险:
1、如果之前的FIN延迟到达,新连接会被reset
2、如果之前发出的包延时后到达对端,会造成干扰
tcp协议栈设计的时候是如何处理这些风险呢,代码如下:
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
if (!want_cookie && !isn) {
/* VJ's idea. We save last timestamp seen
* from the destination in peer table, when entering
* state TIME-WAIT, and check against it before
* accepting new connection request.
*
* If "isn" is not zero, this request hit alive
* timewait bucket, so that all the necessary checks
* are made in the function processing timewait state.
*/
if (tcp_death_row.sysctl_tw_recycle) {
bool strict;
dst = af_ops->route_req(sk, &fl, req, &strict);
if (dst && strict &&
!tcp_peer_is_proven(req, dst, true,tmp_opt.saw_tstamp)) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
goto drop_and_release;
}
}
}
...
}
在tcp_tw_recycle模式下,判断是无效连接的条件是:
1、来自对端的tcp syn请求携带时间戳
2、本机在MSL时间内接收过来自同一台ip机器的tcp数据
3、新连接的时间戳小于上次tcp数据的时间戳
以上条件满足时,连接请求会被拒绝,使用netstat -s |grep timestamp 有如下记录
……packets rejects in established connections because of timestamp
因此,在启用了tcp_tw_recycle的情况下,TIME_WAIT时间内(60s),同一源ip主机syn请求中的timestamp必须是递增的,连接才能被接受。
这个看起来很完美,同一个主机的timestamp的一定是递增的,但是NAT环境就悲剧了,NAT下,多个主机映射到同一个或几个对外IP,NAT设备只修改源地址和端口,timestamp不做修改,不能保证来自NAT机器多个主机间连接请求的timestamp是递增的,时间戳小的请求都会被拒绝。
tcp_tw_reuse
TIME_WAIT的重用只满足一定的条件下,处于TIME_WAIT状态的socket连接可以被新请求的syn使用。条件如下:
1、新请求的sequence要大于TIME_WAIT连接的最后的sequence
2、如果启用了tcp的timestamp选项,syn请求的时间戳要大于TIME_WAIT连接最后接收数据的时间戳
这个选项没有太大的意义,满足这个条件的情形并不多,并不能减少TIME_WAIT的数量。
tcp_max_tw_buckets
这个选项其实没有什么可说的,就是设置系统允许的最大TIME_WAIT数量,如果超过这个量,就不再出现TIME_WAIT,直接close
struct inet_timewait_sock *inet_twsk_alloc(const struct sock *sk,
struct inet_timewait_death_row *dr,
const int state)
{
struct inet_timewait_sock *tw;
if (atomic_read(&dr->tw_count) >= dr->sysctl_max_tw_buckets)
return NULL;
...
}
这种方式对TIME_WAIT数量控制简单粗暴,但是效果也比较明显。但是问题和tcp_tw_recycle类似,新连接也可能被对端重传的FIN reset。
总结:控制TIME_WAIT的选项都存在一些问题,最好慎用。最好的方式是维持正常的TIME_WAIT状态,通过连接池的方式复用连接,减少TIME_WAIT出现的数量。如果要使用tcp_tw_recycle,一定要确保没有NAT设备接入,如果是只有client场景的机器,可以使用tcp_tw_reuse或增大net.ipv4.ip_local_port_range范围来发起更多的连接。