【tcp】关于tcp四次挥手的TIME_WAIT

了解了 TCP 四次挥手,在四次挥手的过程中,发起连接断开的一方会有一段时间处于 TIME_WAIT 的状态,你知道 TIME_WAIT 是用来做什么的么?

在面试和实战中,TIME_WAIT 相关的问题始终是绕不过去的一道难题。

一、TIME_WAIT 发生的场景

在一次升级线上应用服务之后,我们发现该服务的可用性变得时好时坏,一段时间可以对外提供服务,一段时间突然又不可以,大家都百思不得其解。

运维同学登录到服务所在的主机上,使用 netstat 命令查看后才发现,主机上有成千上万处于 TIME_WAIT 状态的连接。

经过层层剖析后,我们发现罪魁祸首就是 TIME_WAIT。

为什么呢?

我们这个应用服务需要通过发起 TCP 连接对外提供服务。

每个连接会占用一个本地端口,当在高并发的情况下,TIME_WAIT 状态的连接过多,多到把本机可用的端口耗尽,应用服务对外表现的症状,就是不能正常工作了。

当过了一段时间之后,处于 TIME_WAIT 的连接被系统回收并关闭后,释放出本地端口可供使用,应用服务对外表现为,可以正常工作。

这样周而复始,便会出现了一会儿不可以,过一两分钟又可以正常工作的现象。

那么为什么会产生这么多的 TIME_WAIT 连接呢?

这要从 TCP 的四次挥手说起。

tcp四次挥手

TCP 连接终止时,主机 1 先发送 FIN 报文,主机 2 进入 CLOSE_WAIT 状态,并发送一个 ACK 应答,同时,主机 2 通过 read 调用获得 EOF,并将此结果通知应用程序进行主动关闭操作,发送 FIN 报文。

主机 1 在接收到 FIN 报文后发送 ACK 应答,此时主机 1 进入 TIME_WAIT 状态。

主机 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 就进入 CLOSED 状态。

为什么是这个时间呢?你可以先想一想,稍后我会给出解答。

你一定要记住一点,只有【发起连接终止的一方】会进入 TIME_WAIT 状态。

这一点面试的时候经常会被问到。

二、TIME_WAIT 的作用

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

这要从两个方面来说:

首先,这样做是为了确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

TCP 在设计的时候,做了充分的容错性设计,比如,TCP 假设报文会出错,需要重传。在这里,如果图中主机 1 的 ACK 报文没有传输成功,那么主机 2 就会重新发送 FIN 报文。如果主机 1 没有维护 TIME_WAIT 状态,而直接进入 CLOSED 状态,它就失去了当前状态的上下文,只能回复一个 RST 操作,从而导致被动关闭方出现错误。现在主机 1 知道自己处于 TIME_WAIT 的状态,就可以在接收到 FIN 报文之后,重新发出一个 ACK 报文,使得主机 2 可以进入正常的 CLOSED 状态。

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

我们知道,在网络中,经常会发生报文经过一段时间才能到达目的地的情况,产生的原因是多种多样的,如路由器重启,链路突然出现故障等。如果迷走报文到达时,发现 TCP 连接四元组(源 IP,源端口,目的 IP,目的端口)所代表的连接不复存在,那么很简单,这个报文自然丢弃。

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

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

注意,2MSL 的时间是从主机 1 接收到 FIN 后发送 ACK 开始计时的;

如果在 TIME_WAIT 时间内,因为主机 1 的 ACK 没有传输到主机 2,主机 1 又接收到了主机 2 重发的 FIN 报文,那么 2MSL 时间将重新计时。

道理很简单,因为 2MSL 的时间,目的是为了让旧连接的所有报文都能自然消亡,现在主机 1 重新发送了 ACK 报文,自然需要重新计时,以便防止这个 ACK 报文对新可能的连接化身造成干扰。

TIME_WAIT是一个抽象的定义,而TCP_TIMEWAIT_LEN是Linux默认的值,是一个常量。

三、TIME_WAIT 的危害

过多的 TIME_WAIT 的主要危害有两种:

第一是内存资源占用,这个目前看来不是太严重,基本可以忽略。

第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。

要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000 ,也可以通过net.ipv4.ip_local_port_range指定,如果 TIME_WAIT 状态过多,会导致无法创建新连接。

四、如何优化 TIME_WAIT?

在高并发的情况下,如果我们想对 TIME_WAIT 做一些优化,来解决我们一开始提到的例子,该如何办呢?

net.ipv4.tcp_max_tw_buckets

一个暴力的方法是通过 sysctl 命令,将系统值调小。

这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置,并且只打印出警告信息。

这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。

调低 TCP_TIMEWAIT_LEN,重新编译系统

这个方法是一个不错的方法,缺点是需要“一点”内核方面的知识,能够重新编译内核。

我想这个不是大多数人能接受的方式。

SO_LINGER 的设置

英文单词“linger”的意思为停留,我们可以通过设置套接字选项,来设置调用 close 或者 shutdown 关闭连接时的行为。

设置 linger 参数有几种可能:

1. 如果l_onoff为 0,那么关闭本选项。l_linger的值被忽略,这对应了默认行为,close 或 shutdown 立即返回。如果在套接字发送缓冲区中有数据残留,系统会将试着把这些数据发送出去。

2. 如果l_onoff为非 0, 且l_linger值也为 0,那么调用 close 后,会立该发送一个 RST 标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了 TIME_WAIT 状态,直接关闭。这种关闭的方式称为“强行关闭”。 在这种情况下,排队数据不会被发送,被动关闭方也不知道对端已经彻底断开。只有当被动关闭方正阻塞在recv()调用上时,接受到 RST 时,会立刻得到一个“connet reset by peer”的异常。

3. 如果l_onoff为非 0, 且l_linger的值也非 0,那么调用 close 后,调用 close 的线程就将阻塞,直到数据被发送出去,或者设置的l_linger计时时间到。

第二种可能为跨越 TIME_WAIT 状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。

net.ipv4.tcp_tw_reuse:更安全的设置

那么 Linux 有没有提供更安全的选择呢?当然有。

这就是net.ipv4.tcp_tw_reuse选项。

Linux 系统对于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 的套接字为新的连接所用。

那么什么是协议角度理解的安全可控呢?

1. 只适用于连接发起方(C/S 模型中的客户端);

2. 对应的 TIME_WAIT 状态的连接创建时间超过 1 秒才可以被复用。

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

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

五、总结

本讲主要讲了 TCP 的四次挥手,重点对 TIME_WAIT 的产生、作用以及优化进行了讲解,你需要记住以下三点:

1. TIME_WAIT 的引入是为了让 TCP 报文得以自然消失,同时为了让被动关闭方能够正常关闭

2. 不要试图使用SO_LINGER设置套接字选项,跳过 TIME_WAIT;

3. 现代 Linux 系统引入了更安全可控的方案,可以帮助我们尽可能地复用 TIME_WAIT 状态的连接。

六、思考题

1. 最大分组 MSL 是 TCP 分组在网络中存活的最长时间,你知道这个最长时间是如何达成的?换句话说,是怎么样的机制,可以保证在 MSL 达到之后,报文就自然消亡了呢?

2. RFC 1323 引入了 TCP 时间戳,那么这需要在发送方和接收方之间定义一个统一的时钟吗?

net.ipv4.tcp_tw_recycle 是客户端和服务器端都可以复用,但是容易造成端口接收数据混乱,4.12内核直接将其废弃了。

#########################

#默认0,tw快速回收

net.ipv4.tcp_tw_recycle=0

##########################

那么通过setsockopt设置SO_REUSEADDR这个方法呢?

网上资料基本上都是通过设置这个来解决TIME_WAIT。这个方法有什么优劣吗?

这个是解决端口复用的问题,并不是解决TIME_WAIT。

这个是告诉内核,即使TIME_WAIT状态的套接字,我也可以继续使用它做为新的套集字使用。

为什么是2MSL,不是3 MSL,4 MSL?

因为是一来一返。

数据报文可能在发送途中延迟但最终会到达,因此要等老的“迷路”的重复报文段在网络中过期失效,这样可以避免用相同源端口和目标端口创建新连接时收到旧连接姗姗来迟的数据包,造成数据错乱

TIME_WAIT 等待时间是 2 个 MSL,已经足够让一个方向上的包最多存活 MSL 秒就被丢弃,保证了在创建新的 TCP 连接以后,老连接姗姗来迟的包已经在网络中被丢弃、消失,不会干扰新的连接

1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端。

1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达。

并且这个时间足够让两个方向上的包最多存活MSL秒就被丢弃,保证了在创建新的TCP连接以后,老连接姗姗来迟的包已经在网络中被丢弃消失,不会干扰新的连接。

因此2MS = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)

由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

为什么 2MSL 问题就不存在了?

这是因为时间戳会告诉我们报文发送的时间,这样在迷走报文和正确报文同时到达的情况下,我们就可以很方便的分辨出应该丢弃掉那个报文,并不会对最后收到的报文产生任何不利的影响。

net.ipv4.tcp_tw_reuse 只适用于连接发起方(C/S 模型中的客户端),但目前要解决的是服务端连接不足问题,这个方法要如何发挥作用呢?

这里针对的TIME_WAIT是指主动关闭的一方,不一定是客户端或者服务器端,如果服务器端主动关闭连接,也是属于这样的范畴的。

SO_REUSEADDR和SO_REUSEPORT可以详细说下作用吗?

这是为了解决如何快速复用处于TIME_WAIT的连接,如果不设置这个选项,处于TIME_WAIT的连接是不能被快速复用的,必须等待系统回收连接才可以,如果这个时候开启服务器端口,会报地址已被占用的错误。

IP包中TTL每经过一次路由就少1,那么2MSL怎么确保可以一定大于TTL的?

只要每次经过一跳的时间肯定大于1秒以上就可以了,实际处理的时间肯定大于这个值的。

2MSL设置的为60秒,TTL设置为60,只有每次一跳都大于1秒,那么肯定时间总和大于60秒了。

报文的自然消亡,就是TTL时间为0了,不会在网络中继续传播,到了某个网络设备,报文会被丢弃掉。

出现大量time_wait应该都是在客户端的一方,因为客户端发起请求会占用一个新端口,主动关闭到time_wait阶段就相当于这个新的端口一直被占用。

这种大量time_wait在连接数多的情况下是肯定会出现的,是不是可以从减少连接的方向去解决问题呢,比如用连接池这种技术可以解决吗?

在服务端你看到的连接都是服务器端被动建立的连接,本地端口都是服务器监听的端口,类似

<127.0.0.1, 80, ip1, 51231>

<127.0.0.1, 80, ip2, 51331>

...

所以,不会存在我讲到的那个问题,这些个连接过了一段时间自然会被回收。

连接池是为了多个线程复用连接,减少TCP连接的数量,是为了更高效的使用TCP,确实也客观减少了连接的数量。

这个可控优化的方法,是复用端口的意思吗?不过复用端口数据不混乱了?

不是端口复用,是复用处于 TIME_WAIT 的套接字为新的连接所用。

前提在文中也提到了,是通过TCP时间戳来解决2MSL的问题。

复用后的套接字,如何回复旧连接的FIN呢?

处于 TIME_WAIT 的套接字为新的连接所用,通过时间戳可以知道旧连接的FIN是一个无效的FIN,从而直接回复RST,让旧连接直接出错退出。

假如开启了net.ipv4.tcp_tw_reuse,对方主机的时间跟发送方时间本来就有差,这个要怎么避免?

所以有NTP服务嘛

为什么会出现端口号不够用的情况?

难道指的的为客户服务的代理服务器可能会端口号不够用吗?

因为代理服务器要处理来自成千上万的客户请求,需要选择不同的端口号为客户服务,将请求发给服务器吗?

这个例子是客户端发起连接过多,每次发起连接都会占用一个端口。

客户端和服务端是相对,比如一个应用程序对于客户的请求是服务端,同时为了服务这个客户请求,又要向另一个服务发起调用请求(典型的例子是向数据库发起连接请求)。

服务器出现大量time wait并且一直不消失 ,怎么解决?

不会不消失的,系统过了一段时间会自动回收TIME_WAIT状态的连接。

如果是突然出现大批的TIME_WAIT连接,需要看一下处理断连的逻辑。

 netstat看一下,看看是什么进程,什么端口,为什么会有这个现象。

咱们写程序的socket相关函数,是属于系统调用?还是属于封装了系统调用的库函数呢?

 一般我们在Linux下调用的都是glibc库函数,而glibc是封装了系统调用的标准C语言库。

迷走报文的问题不好处理,tcp链路重连后,报文编号应该和之前的链路不一致了,能不能解决迷走报文?

我觉得是可以的,比如报文编号和时间戳有关系,就可以保证报文编号是线性不同的。但是设计TCP是那么久远之前的事情,所以当时是没有考虑这么周详的。

2MSL一定能保证服务器正常关闭吗?

如果服务器一直收不到最后的ack呢?假如说当服务器发送fin以后,客户端也收到了,但是因为网络状态不好,ack传不过去,导致客户端2MSL计时器到时了,接着关闭了,那服务器是不是一直处在last_lack状态,服务器的rto计时器不停超时重传,一直到客户端收到fin以后,发现自己已经关闭了,发送rst报文给服务器,服务器收到rst以后出错呢?

服务器在LAST_ACK状态,如果收到RST,自然认为自己是一个"终止"的连接,将自己的状态置为closed即可。

我怎么感觉TIME_WAIT的那两个好处是一样的啊。

A收到了FIN发送一个ACK,此时网络中由旧有的连接产生的报文就只剩一个ACK了吧,保证最后一个ACK能够到达对端和保证旧有连接产生的报文全部消失是一回事吧?

不是的,旧连接的报文消失和2MSL紧密有关,这是需要让连接推迟一段时间关闭;

而ACK是为了维护连接的状态,相当于保持了一个连接的上下文信息。

第二种可能为跨越 TIME_WAIT 状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。 

这句话没理解,危险的行为怎么解释?

强行关闭TIME_WAIT的连接,直接跳过TCP四次挥手的过程,这会导致连接关闭的正常清理工作不能有效的执行,同时也不能使用TIME_WAIT带来的2MSL数据包自动消亡的特性。

TIME_WAIT 是TCP主动关闭方才会存在的状态,什么机制确认谁是主动关闭方?

谁先发起断连的操作,谁就是主动关闭方。

TCP(7)手册中,参数net.ipv4.tcp_tw_recycle 非常蛋疼,尤其是在普通用户家中,有多台设备,或者网吧、公司等多台设备,共用同一个NAT设备环境下,tw回收选项是很有问题的面向公共服务器作为它不会把手连接两台不同的计算机上,这问题很难发现,无从下手。

启用TIME-WAIT状态sockets的快速回收,这个选项不推荐启用。

在NAT(Network Address Translation)网络下,会导致大量的TCP连接建立错误。

TIME-WAIT状态的作用

一、人尽皆知的是,防止上一个TCP连接的延迟的数据包(发起关闭,但关闭没完成),被接收后,影响到新的TCP连接。(唯一连接确认方式为四元组:源IP地址、目的IP地址、源端口、目的端口),包的序列号也有一定作用,会减少问题发生的几率,但无法完全避免。

二、另外一个作用是,当最后一个ACK丢失时,远程连接进入LAST-ACK状态,它可以确保远程已经关闭当前TCP连接。如果没有TIME-WAIT状态,当远程仍认为这个连接是有效的,则会继续与其通讯,导致这个连接会被重新打开。当远程收到一个SYN 时,会回复一个RST包,因为这SEQ不对,那么新的连接将无法建立成功,报错终止。

“如果不维持 TIME_WAIT 这个状态,那么再次收到对端的 FIN 包后,本端就会回一个 Reset 包,这可能会产生一些异常。”

1. 如果不维持TCP状态,最后一次ACK结束,Client端认为已经结束,就会关闭连接。这时候Server端在发送FIN包,客户端应该就收不到了吧?

2. 假设客户端收到了,回复RESET,会产生什么样的异常?

 1. 内核协议栈会收到该包。假设该fd已经被关闭,内核协议栈收到对端的fin包后,就查找不到对应的连接,它就会通知对端这是个异常的连接。

2. 假设客户端重新建立了一个连接,复用了之前的端口,由于网络延迟的原因,这个fin包可能在新连接建立后到达,那么这个新连接就会被误伤。所以一定要设立一个保护时间,来应对网络的不确定性。

七、参考

/proc/sys/net/ipv4/* Variables

https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt

关于TIME-WAIT状态的案例分析

https://blog.csdn.net/enweitech/article/details/79261439

TIME_WAIT:隐藏在细节下的魔鬼

https://time.geekbang.org/column/article/125806?noteid=7709294

《网络编程实战》笔记 | 10 TIME-WAIT:隐藏在细节下的魔鬼

https://www.cnblogs.com/1million/p/14329557.html

tcp_tw_reuse、tcp_tw_recycle 使用场景及注意事项

https://www.cnblogs.com/lulu/p/4149312.html

tcp_tw_recycle参数引发的系统问题

http://blog.csdn.net/zhuyiquan/article/details/68925707

おすすめ

転載: blog.csdn.net/michaelwoshi/article/details/121180561
おすすめ