一,linux TIME_WAIT 相关参数
1,/proc/sys/net/ipv4/ip_local_port_range:端口范围,不要超过1024到65535,1024以下系统使用,65535以上会提示失败
2,/proc/sys/net/ipv4/tcp_max_tw_buckets:kernel中最多存在的TIME_WAIT数量
3,net.ipv4.tcp_tw_reuse = 0 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
4,net.ipv4.tcp_tw_recycle = 0 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
5,net.ipv4.tcp_fin_timeout = 60 表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
6,net.ipv4.tcp_timestamps=0 Timestamps 用在其它一些东西中﹐可以防范那些伪造的sequence 号码。可用于计算往返时延RTT。
注意:
- 不像Windows 可以修改注册表修改2MSL 的值,linux 需要修改内核宏定义重新编译,tcp_fin_timeout 不是2MSL 而是Fin-WAIT-2状态超时时间.
- tcp_tw_reuse 和 SO_REUSEADDR 是两个完全不同的东西
SO_REUSEADDR 允许同时绑定 127.0.0.1 和 0.0.0.0 同一个端口; SO_RESUSEPORT linux 3.7才支持,用于绑定相同ip:port,像nginx 那样 fork方式也能实现
(1). tw_reuse,tw_recycle 必须在客户端和服务端 timestamps 开启时才管用(默认打开)
(2). tw_reuse 只对客户端起作用,开启后客户端在1s内回收
(3). tw_recycle 对客户端和服务器同时起作用,开启后在 3.5*RTO 内回收,RTO 200ms~ 120s 具体时间视网络状况。
内网状况比tw_reuse 稍快,公网尤其移动网络大多要比tw_reuse 慢,优点就是能够回收服务端的TIME_WAIT数量
二,可能的影响
当系统中socket出现大量timewait状态时,这些TIME_WAIT是占据端口号的,而且基本要1分钟左右才能被Kernel回收,因此会占用大量系统端口号。
1,导致客户端选不到可用端口,端口号耗尽,无法跟服务端建立连接。错误:
Cannot assign requested address
2,导致客户端CPU高。因为内核在选择可以用端口时,是用遍历查找端口。
三,建议
对于客户端(为了解决TIME_WAIT问题的参数优化)
1,端口号范围调大
比较无脑的方案当然是把端口号范围调大,把端口号范围调大,这样就能抗更多的TIME_WAIT;同时将tcp_max_tw_bucket调小。只要port范围 - tcp_max_tw_bucket大于一定的值,那么就始终有port端口可用,这样就可以避免再次到调大临界值得时候继续击穿临界点。
2,作为客户端因为有端口65535问题,开启tcp_tw_reuse,不建议同时打开tw_recycle,帮助不大。
tw_reuse 帮助客户端1s完成连接回收,基本可实现单机6w/s请求,需要再高就增加IP数量吧。
对于服务端(NAT负载后的服务端和客户端访问网站出现丢包现象)
1. 打开tw_reuse无效
2,net.ipv4.tcp_timestamps和net.ipv4.tcp_tw_recycle一定不能同时打开。
3. 线上环境 tw_recycle 不要打开 因为一般的服务器、客户端都在NAT之后
公网服务打开就可能造成部分连接失败,内网的话到时可以视情况打开;
像我所在公司对外服务都放在负载后面,负载会把timestamp 都给清空,好吧,就算你打开也不起作用。
https://www.cnblogs.com/alchemystar/p/13444964.html
4. 服务器TIME_WAIT 高怎么办
不像客户端有端口限制,处理大量TIME_WAIT Linux已经优化很好了,每个处于TIME_WAIT 状态下连接内存消耗很少,
而且也能通过tcp_max_tw_buckets =100000 解决(这个值根据TIME_WAIT数量定),现代机器一般也不缺这点内存。
四,源码分析
1,客户端建链(相关参数:ip_local_port_range、tcp_tw_reuse、tcp_timestamps)
(1) 耗CPU性能的循环查找可用端口(ip_local_port_range)
tcp_v4_connect--->inet_hash_connect--->__inet_hash_connect
int __inet_hash_connect(...)
{
// 注意,这边是static变量
static u32 hint;
// hint有助于不从0开始搜索,而是从下一个待分配的端口号搜索
u32 offset = hint + port_offset;
.....
inet_get_local_port_range(&low, &high);
// 这边remaining就是61000 - 32768
remaining = (high - low) + 1
......
for (i = 1; i <= remaining; i++) {
port = low + (i + offset) % remaining;
/* port是否占用check */
....
goto ok;
}
.......
return -EADDRNOTAVAIL;
ok:
hint += i;
......
}
(2)开启tcp_tw_reuse,使得在大部分情况下1s之内的TIME_WAIT就可以重用(tcp_tw_reuse、tcp_timestamps)
tcp_v4_connect--->inet_hash_connect--->__inet_hash_connect--->__inet_check_established--->twsk_unique
static int __inet_check_established(......)
{
......
sk_nulls_for_each(sk2, node, &head->chain) {
if (sk2->sk_hash != hash)
continue;
if (likely(INET_MATCH(sk2, net, acookie,
saddr, daddr, ports, dif))) {
if (sk2->sk_state == TCP_TIME_WAIT) {
tw = inet_twsk(sk2);
if (twsk_unique(sk, sk2, twp))
break;
}
goto not_unique;
}
}
......
not_unique:
spin_unlock(lock);
return -EADDRNOTAVAIL;
}
而其中的核心函数就是twsk_unique,它的判断逻辑如下:
int tcp_twsk_unique(......)
{
......
if (tcptw->tw_ts_recent_stamp &&
(twp == NULL || (sysctl_tcp_tw_reuse &&
get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
// 对write_seq设置为snd_nxt+65536+2
// 这样能够确保在数据传输速率<=80Mbit/s的情况下不会被回绕
tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2
......
return 1;
}
return 0;
}
在开启了tcp_timestamp以及tcp_tw_reuse的情况下,在Connect搜索port时只要比之前用这个port的TIME_WAIT状态的Socket记录的最近时间戳>1s,就可以重用此port,即将之前的1分钟缩短到1s。同时为了防止潜在的序列号冲突,直接将write_seq加上在65537,这样,在单Socket传输速率小于80Mbit/s的情况下,不会造成序列号重叠(冲突)。
同时这个tw_ts_recent_stamp设置的时机如下图所示:
同时这个tw_ts_recent_stamp设置的时机如下图所示:
所以如果Socket进入TIME_WAIT状态后,如果一直有对应的包发过来,那么会影响此TIME_WAIT对应的port是否可用的时间。开启了这个参数之后,由于从1min缩短到1s。
2,NAT负载后的服务端和客户端访问网站出现丢包现象(相关参数:tcp_tw_recycle、tcp_timestamps)
三次握手收到syn包:tcp_conn_request--->tcp_peer_is_proven
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 (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;
}
}
......
}
/* 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.
* 注释大意为:
* 我们在进入TIME_WAIT状态的时候将最后的时间戳记录到peer tables中,
* 然后在新的连接请求进来的时候检查这个时间戳
*/
// 在tcp_timestamps和tcp_tw_recycle开启的情况下
bool tcp_peer_is_proven(struct request_sock *req, struct dst_entry *dst,
bool paws_check, bool timestamps)
{
......
if (tm &&
// /** TCP_PAWS_MSL== 60 */
/** TCP_PAWS_WINDOW ==1 */
// 以下都是针对同一个对端ip
// tcp_ts_stamp 对端ip的连接进入time_wait状态后记录的本机时间戳
// 当前时间在上一次进入time_wait记录的实际戳后的一分钟之内
(u32)get_seconds() - tm->tcpm_ts_stamp < TCP_PAWS_MSL && // 60s时间内
// tcp_ts 最近接收的那个数据包的时间戳(对端带过来的)
// 对端当前请求带过来的时间戳小于上次记录的进入time_wait状态后记录的对端时间戳
((s32)(tm->tcpm_ts - req->ts_recent) > TCP_PAWS_WINDOW ||
!timestamps))
ret = false;
else
ret = true;
......
return ret;
}
上述代码的核心意思即是在tcp_timestamps和tcp_tw_recycle开启的情况下,同样ip的连接,在上个连接进入time_wait状态的一分钟内,如果有新的连接进来,而且新的连接的时间戳小于上个进入time_wait状态的最后一个包的时间戳,则将这个syn丢弃,进入drop_and_release。
参考:
解Bug之路-Nginx 502 Bad Gateway
https://www.cnblogs.com/alchemystar/archive/2020/07/31/13409534.html
解Bug之路-记一次调用外网服务概率性失败问题的排查
https://www.cnblogs.com/alchemystar/p/13444964.html
tcp_tw_reuse、tcp_tw_recycle 使用场景及注意事项
https://www.cnblogs.com/lulu/p/4149312.html
一个NAT问题引起的思考
http://perthcharles.github.io/2015/08/27/timestamp-NAT/