关于NAT后端服务端使能TW(Timewait)连接快速回收造成用户连接成功率降低的问题,来捋一捋。
对于主动关闭连接的一方,总是在FIN_WAIT2状态收到FIN并回复ACK(或在FIN_WAIT1收到FIN/ACK并回复ACK),因为回复的ACK无序列号无法得到对端的确认,所以主动关闭一方无法知道最后的ACK是否到达对端。所以主动关闭一端在回复ACK后进入TW状态,保留2MSL时间等待对方重传FIN,保证ACK可靠地到达对端。
另一方面,TW状态下的五元组连接无法被重新建立。如果TW状态下相同五元组连接被新建,那么网络中“迷路”的旧连接报文很可能重新到达,并将新连接FIN关闭或劫持。所以TW状态持续2MSL,保证旧连接报文在网络中消失。
TW状态的连接如僵尸般存在,即无法被重用,又占据着内存,有很多方式去控制TW状态连接数量,如设置tcp_max_tw_buckets内核选项控制系统TW状态连接最大值,或使能linger选项让连接在关闭时直接RST,但这样的方式不优雅也不安全。
Linux通过tcp_tw_reuse和tcp_tw_recycle内核选项控制TW连接的复用。期望TW连接的复用,就需要解决新旧连接报文区分的问题,防止旧连接报文对新连接的劫持。Linux的tcp_tw_reuse或tcp_tw_recycle功能使能需同时开启tcp_timestamps时间戳,在旧连接关闭时,记录旧连接最后到来报文的时间戳,以此为标准区别新旧连接报文,时间戳小于该记录值的报文被判定为旧连接报文,旧连接报文将被丢弃,而新连接报文允许TW连接的服用。
其中,tcp_tw_reuse为连接重用,即连接仍处于TW状态但在一定条件下允许复用,tcp_tw_recycle为连接回收,即TW状态连接将被快速回收清理。
首先,TW重用只在客户端生效,即只有在主动发起连接的一侧才会判断是否使能了tcp_tw_reuse。
static int __inet_check_established(struct inet_timewait_death_row *death_row,
struct sock *sk, __u16 lport,
struct inet_timewait_sock **twp)
{
...
sk_nulls_for_each(sk2, node, &head->twchain) {
if (sk2->sk_hash != hash)
continue;
if (likely(INET_TW_MATCH(sk2, net, acookie,
saddr, daddr, ports, dif))) {
tw = inet_twsk(sk2);
if (twsk_unique(sk, sk2, twp))
goto unique;
else
goto not_unique;
}
}
}
在connect调用过程中,如果选用四元组信息为TW状态连接,则调用twsk_unique()判断连接是否可以重用。
if (tcptw->tw_ts_recent_stamp &&
(twp == NULL || (sysctl_tcp_tw_reuse &&
get_seconds() - tcptw->tw_ts_recent_stamp > 1)))
如果TW状态连接有最近的对端时间戳(对端最后一个到达报文时间戳)记录,同时目前时间与对端时间戳时间相距大于1秒,则连接可重用。
快速回收前,即系统仍存在TW状态连接时的处理情况。首先看一下使能tcp_tw_recycle时主动进入TW状态的情况(tcp_time_wait)。
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
recycle_ok = tcp_remember_stamp(sk);
if (recycle_ok) {
tw->tw_timeout = rto;
} else {
tw->tw_timeout = TCP_TIMEWAIT_LEN;
if (state == TCP_TIME_WAIT)
timeo = TCP_TIMEWAIT_LEN;
}
在使能tcp_tw_recycle情况下,首先对接收报文中的时间戳和接收报文时间(当前时间)进行记录,同时修改TW状态定时器超时时间为一个rto,使TW状态尽快超时回收。
再来看另一种TW状态的处理,即连接仍处于TW状态,但在连接上收到新的报文(tcp_timewait_state_process)。
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);
可以看到处理基本和主动进入TW状态的处理相同,即记录相关时间戳和较少TW状态超时时间。
快速回收后,即TW状态连接已超时清理,同样五元组的连接新建时的处理。
if (tmp_opt.saw_tstamp &&
tcp_death_row.sysctl_tw_recycle &&
(dst = inet_csk_route_req(sk, &fl4, req)) != NULL &&
fl4.daddr == saddr) {
if (!tcp_peer_is_proven(req, dst, true)) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
goto drop_and_release;
}
}
if (tm &&
(u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL &&
(s32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW)
ret = false;
因为TW状态连接已被清理,所以已经无法查找到原有连接的五元组信息,但在TW状态连接快速回收前,系统将接收报文中的时间戳和接收报文时间记录在IP层,此时仍可以根据对端IP查询到上一次报文到达时携带的时间戳信息。所以,如果该对端IP前一次报文到达时间距现在小于TCP_PAWS_MSL(60秒),并且前一次报文时间戳大于新报文的时间戳,系统判断新到来的报文仍可能属于旧连接,则新的连接建立请求将被拒绝。
另外,TW状态快速回收在新的内核版本已经被移除。
在NAT环境下使能TW快速回收存在巨大隐患。多个客户端请求在SNAT后,源地址被转为统一地址,但因为客户端时间设置的差异,无法保证所有请求报文时间戳保持增序。一旦服务端某一连接被TW快速回收,将只在IP层保留转换后地址和最后到达报文的时间戳信息,那么时间戳不符合增序的客户端请求将被拒绝。所以,处于NAT后的服务器不建议使能TW快速回收。