网络编程——TIME_WAIT

  1. 我们这个应用服务需要通过发起 TCP 连接对外提供服务。每个连接会占用一个本地端口,当在高并发的情况下,TIME_WAIT 状态的连接过多,多到把本机可用的端口耗尽,应用服务对外表现的症状,就是不能正常工作了。当过了一段时间之后,处于 TIME_WAIT 的连接被系统回收并关闭后,释放出本地端口可供使用,应用服务对外表现为,可以正常工作。这样周而复始,便会出现了一会儿不可以,过一两分钟又可以正常工作的现象。

产生TIME_WAIT 连接,从TCP 的四次挥手说起
在这里插入图片描述

  1. TCP 连接终止时,主机 1 先发送 FIN 报文,主机 2 进入 CLOSE_WAIT 状态,并发送一个 ACK 应答,同时,主机 2 通过 read 调用获得 EOF,并将此结果通知应用程序进行主动关闭操作,发送 FIN 报文。主机 1 在接收到 FIN 报文后发送 ACK 应答,此时主机 1 进入 TIME_WAIT 状态。
  2. 主机 1 在 TIME_WAIT 停留持续时间是固定的,是最长分节生命期 MSL(maximum segment lifetime)的两倍,一般称之为 2MSL。和大多数 BSD 派生的系统一样,Linux 系统里有一个硬编码的字段,名称为TCP_TIMEWAIT_LEN,其值为 60 秒。也就是说,Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-        WAIT state, about 60 seconds  */
  1. 过了这个时间之后,主机1就进入closed状态,只有发起连接终止的一方才会进入TIME_WAIT状态
1.TIME_WAIT 的作用

为什么不直接进入 CLOSED 状态,而要停留在 TIME_WAIT 这个状态?

  1. 理由1:这样做为了确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭

  2. TCP设计时做了充分容错性设计,比如TCP假设报文出错,需要重传,如果主机1的ACK报文没有传输成功,主机2就会重新发送FIN报文。

  3. 如果主机1没有维护TIME_WAIT状态,而直接进入CLOSED状态,就会失去当前状态的上下文,只能回复一个RST操作,从而导致被动关闭方出现出错。

  4. 现在主机1知道自己出入TIME_WAIT状态,就可以接受到FIN报文之后,重新发出一个ACK报文,使得主机2可以进入正常的CLOSED状态。

  5. 第二个理由个连接“化身”和报文迷走有关系,为了让旧连接的重复分节在网络中自然消失

  6. 网络经常会发生报文经过一段时间才会到达目的地的情况,产生的原因有:路由器重启、链路突然出现故障,如果迷走的报文到达时,发现TCP连接四元组(源IP,源端口,目的IP,目的端口)所代表的链接不复存在,那么这个报文自然丢弃。

  7. 这样考虑一个场景,在原链接中断后,又重新创建了一个原链接的“化身”,其实就是因为这个连接和原来的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文被误认为是连接“化身”的一个TCp分节,这样会对TCP通信产生影响。
    在这里插入图片描述

  8. 所以,TCP设计出这个机制,经过2MSL时间,足以让两个方向上的分组都被丢弃,使得原来连接的分组在网络中都自然消失,而出现的分组一定都是新化身所产生的

  9. 2MSL的时间是主机1接收到FIN后发送ACK开始计时的;如果在TIME_WAIT时间内,因为主机1的ACK没有传输到主机2,主机1又接受到了主机2重发的FIN报文,那么2MSL时间将重新计时。因为2MSL的时间,目的是为了让旧连接的所有报文都能自然消亡,现在主机1重新发送ACK报文,自然需要重新计时,防止这个ACK报文对新可能的连接化身造成干扰

2.TIME_WAIT的危害

第一种是内存资源占用,不是很严重,可以忽略。
第二种是端口资源的占用,一个TCP连接至少消耗一个本地端口,端口是有限的23768-61000,也可以通过net.ipv4.ip_local_port_range指定,如果TIME_WAIT状态太多,导致无法创建新连接。

3.如何优化 TIME_WAIT?

高并发的情况下

  1. net.ipv4.tcp_max_tw_buckets
    暴力方法是通过sysctl命令,将系统值调小。这个值默认为18000,当系统中处于TIME_WAIT的连接一旦超过这个值,系统就会将TIME_WAIT连接状态重置,打印警告信息。这个方法治标不治本不推荐使用。
  2. 调低TCP_TIMEWAIT_LEN,重新编译系统
  3. SO_LINGER的设置
    “linger”的意思为停留,可以通过设置套接字选项,设置调用close或者shutdown关闭连接时的行为

int setsockopt(int sockfd, int level, int optname, const void *optval,socklen_t optlen);
struct linger {
 int  l_onoff;    /* 0=off, nonzero=on */
 int  l_linger;    /* linger time, POSIX specifies units as seconds */
}

设置linger参数的几种可能:

  • 如果l_onoff为0,那么关闭本选项,l_linger的值忽略,对应了默认行为,close或shutdown立即返回,如果套接字发送缓冲区有数据残留,系统会将试着把这些数据发送出去
  • 如果l_onoff非0,且l_linger值也为0,那么调用close后,会立刻发送一个RST标志给对端,该TCP连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接强行关闭。这种情况下,排队数据不会被发送,被动关闭方也不知道对端已经彻底断开,只有当被动关闭正阻塞在recv()调用上时,接受到RST时,会立刻得到一个“connect reset by peer”的异常。
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s,SOL_SOCKET,SO_LINGER, &so_linger,sizeof(so_linger));
  • 如果l_onoff非0,且l_linger的值也非0,那么调用close后,调用close的线程就将阻塞,知道数据被发送出去,或者设置的l_linger计时时间到

第二种可能为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为。
4. net.ipv4.tcp_tw_reuse:更安全的设置

Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. Default value is 0.It should not be changed without advice/request of technical experts.

这段话是从协议角度理解如何是安全可控的,可以复用处于TIME_WAIT的套接字为新的连接所用
协议角度理解的安全可控呢?主要有两点:

  • 只适用于连接发起方(C/S 模型中的客户端);
  • 对应的 TIME_WAIT 状态的连接创建时间超过 1 秒才可以被复用。

使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即net.ipv4.tcp_timestamps=1(默认即为 1)。

要知道,TCP 协议也在与时俱进,RFC 1323 中实现了 TCP 拓展规范,以便保证 TCP 的高可用,并引入了新的 TCP 选项,两个 4 字节的时间戳字段,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

总结:
  • TIME_WAIT的引入为了让TCP报文得以自然消失,同时为了让被动关闭放能正常关闭
  • 不要试图使用SO_LINGER设置套接字选项,跳过TIME_WAIT
  • 现代Linux系统引入了更安全可控的方案,可以复用TIME_WAIT状态的连接
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值