测试环境的一台Nginx服务器,最近一直被前端同事吐槽网络有问题,经常出现访问HTTP请求时超时,哪怕是静态文件也经常超时。
刚开始以为是公司网络抽风了,也就没放在心上,但持续了一个星期,而且复现率很高,这才反应过来应该不是网络的锅。于是在请求客户端与Nginx服务器上均作了抓包。
本地客户端抓包结果如下图1,请求Nginx服务器TCP握手时超时。结果似乎很明朗,客户端TCP握手的SYN
请求丢包导致多次重试,直到重试超时而TCP握手失败。看上去似乎就是网络问题,但这复现率也太高了,于是在服务器上也做了一次抓包。
图2为Nginx服务器抓包图,显示服务器接收到了TCP握手请求,但是未做应答,直到客户端重试超时结束。很明显,并不是网络丢包而导致的TCP握手失败,而是某种机制导致的服务器直接抛弃了客户端的请求报文。
准备两个Client机器(192.168.50.150、192.168.50.160)以及一个Server机器(118.24.117.115),并且两个Client机器均处于同一个NAT网关下(116.239.x.x)。
启用两个Client机器TIME-WAIT
连接快速回收,编辑文件/etc/sysctl.conf
,添加或修改以下参数:
net.ipv4.tcp_tw_recycle = 1
然后执行命令使sysctl.conf
的参数生效:
[root@VM_0_10_centos ~]# /sbin/sysctl -p
[root@VM_0_10_centos ~]# cat /proc/sys/net/ipv4/tcp_tw_recycle
1
通过netstat -s
命令查看网络统计时,可以发现每次复现出TCP握手失败后,如下的一行统计值都会对应增长。
[root@VM_0_10_centos ~]# netstat -s | grep timestamp
2143 packets rejects in established connections because of timestamp
Per-host PAWS机制
PAWS uses the same TCP Timestamps option as the RTTM mechanism described earlier, and assumes that every received TCP segment (including data and ACK segments) contains a timestamp SEG.TSval whose values are monotone non-decreasing in time. The basic idea is that a segment can be discarded as an old duplicate if it is received with a timestamp SEG.TSval less than some timestamp recently received on this connection.
PAWS机制是基于客户端IP而非客户端IP+端口号的,当服务端开启了net.ipv4.tcp_tw_recycle
后,服务端会对SYN
报文做时间戳检查。每当快速回收TIME_WAIT
连接后,会在60秒缓存该客户端IP的TSval
时间戳。在这60秒内如果同一个IP再发起SYN
请求时,会校验新请求的TSval
是否大于缓存的TSval
,保证同一个请求方IP的TSval
时间戳是递增的。而对于非递增的SYN
请求则直接丢弃处理。
TSval并非真正的时间戳,而是由时间戳依据一定算法算出来的一个值,与时间戳有同等的特性,即随时间单调递增;
如上复现操作时的图,由于Client1和Client2处于同一个NAT网关下,对于Server来说,Client1和Client2的IP相同均为NAT网关的IP。
TSval
为1853021;TSval
为1927141;TSval
为1864558;当步骤3请求时,对于Server来说,IP(116.239.x.x)的TSval
时间戳小于上次请求的时间戳,因此该SYN
请求直接被丢弃了,导致TCP握手失败。
Linux内核源码中,对于PAWS机制的实现很简单,如果客户端的TCP报文中启用了timestamp option
,且服务端启用了net.ipv4.tcp_tw_recycle
,则会触发PAWS机制检查。
tcp_ipv4.c
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
...
/* 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 (tmp_opt.saw_tstamp && // TCP报文中启用了timestamp option
tcp_death_row.sysctl_tw_recycle && // 开启了net.ipv4.tcp_tw_recycle
(dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&
fl4.daddr == saddr) { // 仅判断源IP相同,不区分端口号
if (!tcp_peer_is_proven(req, dst, true)) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
goto drop_and_release;
}
}
...
}
tcp_metrics.c
bool tcp_peer_is_proven(struct request_sock *req, struct dst_entry *dst, bool paws_check)
{
struct tcp_metrics_block *tm;
bool ret;
if (!dst)
return false;
rcu_read_lock();
tm = __tcp_get_metrics_req(req, dst);
if (paws_check) {
if (tm &&
// peer 信息保存的时间离现在在60秒(TCP_PAWS_MSL)之内
(u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL &&
// peer 信息中保存的timestamp 比当前收到的SYN报文中的timestamp大1(TCP_PAWS_WINDOW)
(s32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW)
ret = false;
else
ret = true;
} else {
if (tm && tcp_metric_get(tm, TCP_METRIC_RTT) && tm->tcpm_ts_stamp)
ret = true;
else
ret = false;
}
rcu_read_unlock();
return ret;
}
关闭TCP的TIME_WAIT
快速回收功能即可:编辑文件/etc/sysctl.conf
,添加或修改以下参数:
net.ipv4.tcp_tw_recycle = 0
然后执行命令使sysctl.conf
的参数生效:
[root@VM_0_10_centos ~]# /sbin/sysctl -p
[root@VM_0_10_centos ~]# cat /proc/sys/net/ipv4/tcp_tw_recycle
0
查看Linux内核源码后发现,触发PAWS机制检查的前提条件之一是客户端的TCP请求携带了timestamp option
,如下图1。但是当使用Windows操作系统时,客户端默认关闭了timestamp option
,通过抓包也显示TCP请求中未携带timestamp option
,却依然触发了PAWS机制,如下图2。