02 TCP 四次挥手的性能提升

接下来,我们一起看看针对 TCP 四次挥手关不连接时,如何优化性能。

在开始之前,我们得先了解四次挥手状态变迁的过程。

客户端和服务端双方都可以主动断开连接,通常先关闭连接的一方称为主动方,后关闭连接的一方称为被动方。

可以看到,四次挥手过程只涉及了两种报文,分别是 FIN 和 ACK:

  • FIN 就是结束连接的意思,谁发出 FIN 报文,就表示它将不会再发送任何数据,关闭这一方向上的传输通道;

  • ACK 就是确认的意思,用来通知对方:你方的发送通道已经关闭;

四次挥手的过程:

  • 当主动方关闭连接时,会发送 FIN 报文,此时发送方的 TCP 连接将从 ESTABLISHED 变成 FIN_WAIT1。

  • 当被动方收到 FIN 报文后,内核会自动回复 ACK 报文,连接状态将从 ESTABLISHED 变成 CLOSE_WAIT,表示被动方在等待进程调用 close 函数关闭连接。

  • 当主动方收到这个 ACK 后,连接状态由 FIN_WAIT1 变为 FIN_WAIT2,也就是表示主动方的发送通道就关闭了。

  • 当被动方进入 CLOSE_WAIT 时,被动方还会继续处理数据,等到进程的 read 函数返回 0 后,应用程序就会调用 close 函数,进而触发内核发送 FIN 报文,此时被动方的连接状态变为 LAST_ACK。

  • 当主动方收到这个 FIN 报文后,内核会回复 ACK 报文给被动方,同时主动方的连接状态由 FIN_WAIT2 变为 TIME_WAIT,在 Linux 系统下大约等待 1 分钟后,TIME_WAIT 状态的连接才会彻底关闭。

  • 当被动方收到最后的 ACK 报文后,被动方的连接就会关闭。

你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。

这里一点需要注意是:

主动关闭连接的,才有 TIME_WAIT 状态。

主动关闭方和被动关闭方优化的思路也不同,接下来分别说说如何优化他们。

主动方的优化

关闭的连接的方式通常有两种,分别是 RST 报文关闭和 FIN 报文关闭。

如果进程异常退出了,内核就会发送 RST 报文来关闭,它可以不走四次挥手流程,是一个暴力关闭连接的方式。

安全关闭连接的方式必须通过四次挥手,它由进程调用 close 和 shutdown 函数发起 FIN 报文(shutdown 参数须传入 SHUT_WR 或者 SHUT_RDWR 才会发送 FIN)。

调用 close 函数 和 shutdown 函数有什么区别?

调用了 close 函数意味着完全断开连接,完全断开不仅指无法传输数据,而且也不能发送数据。此时,调用了 close 函数的一方的连接叫做「孤儿连接」,如果你用 netstat -p 命令,会发现连接对应的进程名为空。

使用 close 函数关闭连接是不优雅的。于是,就出现了一种优雅关闭连接的 shutdown 函数,它可以控制只关闭一个方向的连接:

第二个参数决定断开连接的方式,主要有以下三种方式:

  • SHUT_RD(0):关闭连接的「读」这个方向,如果接收缓冲区有已接收的数据,则将会被丢弃,并且后续再收到新的数据,会对数据进行 ACK,然后悄悄地丢弃。也就是说,对端还是会接收到 ACK,在这种情况下根本不知道数据已经被丢弃了。

  • SHUT_WR(1):关闭连接的「写」这个方向,这就是常被称为「半关闭」的连接。如果发送缓冲区还有未发送的数据,将被立即发送出去,并发送一个 FIN 报文给对端。

  • SHUT_RDWR (2):相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。

close 和 shutdown 函数都可以关闭连接,但这两种方式关闭的连接,不只功能上有差异,控制它们的 Linux 参数也不相同。

FIN_WAIT1 状态的优化

主动方发送 FIN 报文后,连接就处于 FIN_WAIT1 状态,正常情况下,如果能及时收到被动方的 ACK,则会很快变为 FIN_WAIT2 状态。

但是当迟迟收不到对方返回的 ACK 时,连接就会一直处于 FIN_WAIT1 状态。此时,内核会定时重发 FIN 报文,其中重发次数由 tcp_orphan_retries 参数控制(注意,orphan 虽然是孤儿的意思,该参数却不只对孤儿连接有效,事实上,它对所有 FIN_WAIT1 状态下的连接都有效),默认值是 0。

你可能会好奇,这 0 表示几次?实际上当为 0 时,特指 8 次,从下面的内核源码可知

如果 FIN_WAIT1 状态连接很多,我们就需要考虑降低 tcp_orphan_retries 的值,当重传次数超过 tcp_orphan_retries 时,连接就会直接关闭掉。

对于普遍正常情况时,调低 tcp_orphan_retries 就已经可以了。如果遇到恶意攻击,FIN 报文根本无法发送出去,这由 TCP 两个特性导致的:

  • 首先,TCP 必须保证报文是有序发送的,FIN 报文也不例外,当发送缓冲区还有数据没有发送时,FIN 报文也不能提前发送。

  • 其次,TCP 有流量控制功能,当接收方接收窗口为 0 时,发送方就不能再发送数据。所以,当攻击者下载大文件时,就可以通过接收窗口设为 0 ,这就会使得 FIN 报文都无法发送出去,那么连接会一直处于 FIN_WAIT1 状态。

解决这种问题的方法,是调整 tcp_max_orphans 参数,它定义了「孤儿连接」的最大数量:

当进程调用了 close 函数关闭连接,此时连接就会是「孤儿连接」,因为它无法在发送和接收数据。Linux 系统为了防止孤儿连接过多,导致系统资源长时间被占用,就提供了 tcp_max_orphans 参数。如果孤儿连接数量大于它,新增的孤儿连接将不再走四次挥手,而是直接发送 RST 复位报文强制关闭。

FIN_WAIT2 状态的优化
当主动方收到 ACK 报文后,会处于 FIN_WAIT2 状态,就表示主动方的发送通道已经关闭,接下来将等待对方发送 FIN 报文,关闭对方的发送通道。

这时,如果连接是用 shutdown 函数关闭的,连接可以一直处于 FIN_WAIT2 状态,因为它可能还可以发送或接收数据。但对于 close 函数关闭的孤儿连接,由于无法在发送和接收数据,所以这个状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒:

它意味着对于孤儿连接(调用 close 关闭的连接),如果在 60 秒后还没有收到 FIN 报文,连接就会直接关闭。

这个 60 秒不是随便决定的,它与 TIME_WAIT 状态持续的时间是相同的,后面我们在来说说为什么是 60 秒。

TIME_WAIT 状态的优化

TIME_WAIT 是主动方四次挥手的最后一个状态,也是最常遇见的状态。

当收到被动方发来的 FIN 报文后,主动方会立刻回复 ACK,表示确认对方的发送通道已经关闭,接着就处于 TIME_WAIT 状态。在 Linux 系统,TIME_WAIT 状态会持续 60 秒后才会进入关闭状态。

TIME_WAIT 状态的连接,在主动方看来确实快已经关闭了。然后,被动方没有收到 ACK 报文前,还是处于 LAST_ACK 状态。如果这个 ACK 报文没有到达被动方,被动方就会重发 FIN 报文。重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

TIME-WAIT 的状态尤其重要,主要是两个原因:

  • 防止具有相同「四元组」的「旧」数据包被收到;

  • 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;

原因一:防止旧连接的数据包

TIME-WAIT 的一个作用是防止收到历史数据,从而导致数据错乱的问题。

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?

  • 如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。

  • 这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。

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

原因二:保证连接正确关闭

TIME-WAIT 的另外一个作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?

  • 如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSE 状态了,那么服务端则会一直处在 LASE-ACK 状态。

  • 当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。

我们再回过头来看看,为什么 TIME_WAIT 状态要保持 60 秒呢?这与孤儿连接 FIN_WAIT2 状态默认保留 60 秒的原理是一样的,因为这两个状态都需要保持 2MSL 时长。MSL 全称是 Maximum Segment Lifetime,它定义了一个报文在网络中的最长生存时间(报文每经过一次路由器的转发,IP 头部的 TTL 字段就会减 1,减到 0 时报文就被丢弃,这就限制了报文的最长存活时间)。

为什么是 2 MSL 的时长呢?这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。

为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。

因此,TIME_WAIT 和 FIN_WAIT2 状态的最大时长都是 2 MSL,由于在 Linux 系统中,MSL 的值固定为 30 秒,所以它们都是 60 秒。

虽然 TIME_WAIT 状态有存在的必要,但它毕竟会消耗系统资源。如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。

  • 客户端受端口资源限制:如果客户端 TIME_WAIT 过多,就会导致端口资源被占用,因为端口就 65536 个,被占满就会导致无法创建新的连接;

  • 服务端受系统资源限制:由于一个四元组表示 TCP 连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口 但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接;

另外,Linux 提供了 tcp_max_tw_buckets 参数,当 TIME_WAIT 的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT 而直接关闭:

当服务器的并发连接增多时,相应地,同时处于 TIME_WAIT 状态的连接数量也会变多,此时就应当调大 tcp_max_tw_buckets 参数,减少不同连接间数据错乱的概率。

tcp_max_tw_buckets 也不是越大越好,毕竟内存和端口都是有限的。

有一种方式可以在建立新连接时,复用处于 TIME_WAIT 状态的连接,那就是打开 tcp_tw_reuse 参数。但是需要注意,该参数是只用于客户端(建立连接的发起方),因为是在调用 connect () 时起作用的,而对于服务端(被动连接方)是没有用的。

cp_tw_reuse 从协议角度理解是安全可控的,可以复用处于 TIME_WAIT 的端口为新的连接所用。

什么是协议角度理解的安全可控呢?主要有两点:

  • 只适用于连接发起方,也就是 C/S 模型中的客户端;

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

使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持(对方也要打开 ):

由于引入了时间戳,它能带来了些好处:

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

  • 同时,它还可以防止序列号绕回,也是因为重复的数据包会由于时间戳过期被自然丢弃;

老版本的 Linux 还提供了 tcp_tw_recycle 参数,但是当开启了它,就有两个坑:

  • Linux 会加快客户端和服务端 TIME_WAIT 状态的时间,也就是它会使得 TIME_WAIT 状态会小于 60 秒,很容易导致数据错乱;

  • 另外,Linux 会丢弃所有来自远端时间戳小于上次记录的时间戳(由同一个远端发送的)的任何数据包。就是说要使用该选项,则必须保证数据包的时间戳是单调递增的。那么,问题在于,此处的时间戳并不是我们通常意义上面的绝对时间,而是一个相对时间。很多情况下,我们是没法保证时间戳单调递增的,比如使用了 NAT,LVS 等情况;

所以,不建议设置为 1 ,建议关闭它:

在 Linux 4.12 版本后,Linux 内核直接取消了这一参数。

另外,我们可以在程序中设置 socket 选项,来设置调用 close 关闭连接行为。

如果 l_onoff 为非 0, 且 l_linger 值为 0,那么调用 close 后,会立该发送一个 RST 标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了 TIME_WAIT 状态,直接关闭。

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

被动方的优化

当被动方收到 FIN 报文时,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

内核没有权利替代进程去关闭连接,因为如果主动方是通过 shutdown 关闭连接,那么它就是想在半关闭连接上接收数据或发送数据。因此,Linux 并没有限制 CLOSE_WAIT 状态的持续时间。

当然,大多数应用程序并不使用 shutdown 函数关闭连接。所以,当你用 netstat 命令发现大量 CLOSE_WAIT 状态。就需要排查你的应用程序,因为可能因为应用程序出现了 Bug,read 函数返回 0 时,没有调用 close 函数。

处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文关闭发送通道,同时连接进入 LAST_ACK 状态,等待主动方返回 ACK 来确认连接关闭。

如果迟迟收不到这个 ACK,内核就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与主动方重发 FIN 报文的优化策略一致。

还有一点我们需要注意的,如果被动方迅速调用 close 函数,那么被动方的 ACK 和 FIN 有可能在一个报文中发送,这样看起来,四次挥手会变成三次挥手,这只是一种特殊情况,不用在意。

如果连接双方同时关闭连接,会怎么样?

由于 TCP 是双全工的协议,所以是会出现两方同时关闭连接的现象,也就是同时发送了 FIN 报文。

此时,上面介绍的优化策略仍然适用。两方发送 FIN 报文时,都认为自己是主动方,所以都进入了 FIN_WAIT1 状态,FIN 报文的重发次数仍由 tcp_orphan_retries 参数控制。

接下来,双方在等待 ACK 报文的过程中,都等来了 FIN 报文。这是一种新情况,所以连接会进入一种叫做 CLOSING 的新状态,它替代了 FIN_WAIT2 状态。接着,双方内核回复 ACK 确认对方发送通道的关闭后,进入 TIME_WAIT 状态,等待 2MSL 的时间后,连接自动关闭。

小结

针对 TCP 四次挥手的优化,我们需要根据主动方和被动方四次挥手状态变化来调整系统 TCP 内核参数。

主动方的优化
主动发起 FIN 报文断开连接的一方,如果迟迟没收到对方的 ACK 回复,则会重传 FIN 报文,重传的次数由 tcp_orphan_retries 参数决定。

当主动方收到 ACK 报文后,连接就进入 FIN_WAIT2 状态,根据关闭的方式不同,优化的方式也不同:

  • 如果这是 close 函数关闭的连接,那么它就是孤儿连接。如果 tcp_fin_timeout 秒内没有收到对方的 FIN 报文,连接就直接关闭。同时,为了应对孤儿连接占用太多的资源,tcp_max_orphans 定义了最大孤儿连接的数量,超过时连接就会直接释放。

  • 反之是 shutdown 函数关闭的连接,则不受此参数限制;

当主动方接收到 FIN 报文,并返回 ACK 后,主动方的连接进入 TIME_WAIT 状态。这一状态会持续 1 分钟,为了防止 TIME_WAIT 状态占用太多的资源,tcp_max_tw_buckets 定义了最大数量,超过时连接也会直接释放。

当 TIME_WAIT 状态过多时,还可以通过设置 tcp_tw_reuse 和 tcp_timestamps 为 1 ,将 TIME_WAIT 状态的端口复用于作为客户端的新连接,注意该参数只适用于客户端。

被动方的优化
被动关闭的连接方应对非常简单,它在回复 ACK 后就进入了 CLOSE_WAIT 状态,等待进程调用 close 函数关闭连接。因此,出现大量 CLOSE_WAIT 状态的连接时,应当从应用程序中找问题。

当被动方发送 FIN 报文后,连接就进入 LAST_ACK 状态,在未等到 ACK 时,会在 tcp_orphan_retries 参数的控制下重发 FIN 报文。

tcp下载窗口太小的问题_面试官:换人!他连 TCP 这几个参数都不懂(二)相关推荐

  1. tcp下载窗口太小的问题_图解 TCP 重传、滑动窗口、流量控制、拥塞控制

    相信大家都知道 TCP 是一个可靠传输的协议,那如何它是如何保证可靠的呢? 为了实现可靠性传输,需要考虑很多事情,例如数据的破坏.丢包.重复以及分片顺序混乱等问题.如不能解决这些问题,也就无从谈起可靠 ...

  2. 值对于int32太大或太小怎么解决_深孔加工10种常见问题及解决措施,条条都是宝贝!...

    在深孔加工过程中,经常出现被加工件尺寸精度.表面质量以及刀具的寿命等问题,如何减少甚至避免这些问题的产生,是我们目前亟待解决的问题. 1.孔径增大,误差大 1)产生原因 铰刀外径尺寸设计值偏大或铰切削 ...

  3. md5后得到的32位字符串存储到mysql中太占空间了_面试官:你对MySQL高性能优化有什么规范建议?...

    推荐阅读:吊打面试官!MySQL灵魂100问,你能答出多少? 文章篇幅较长,建议先收藏再找个合适的时间阅读 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用 ...

  4. mysql 面试 死锁如何处理_面试官:你怎么连MySQL死锁产生原因都不知道?

    一.Mysql 锁类型和加锁分析 1.锁类型介绍: MySQL有三种锁的级别:页级.表级.行级.表级锁:开销小,加锁快:不会出现死锁:锁定粒度大,发生锁冲突的概率最高,并发度最低. 行级锁:开销大,加 ...

  5. swf缓存文件在哪里_面试官:mybatis一级缓存二级缓存的区别都不知道,知道门在哪吧...

    面试官:虫虫你简历上写了了解mybatis缓存,那你能说说一级缓存和二级缓存的区别吗? 虫虫:我只知道这是用来缓存sql查询的数据 面试官:没了? 虫虫:没了 面试官:公司门知道在哪里吧 自己走还是我 ...

  6. tcp 协议中发送窗口的大小应该是_面试必备--TCP协议中的窗口机制滑动窗口详解...

    窗口机制分类 在TCP协议当中窗口机制分为两种: 1.固定的窗口大小 2.滑动窗口 固定窗口存在的问题 我们假设这个固定窗口的大小为1,也就是每次只能发送一个数据,只有接收方对这个数据进行了确认后才能 ...

  7. 如何把class里的vector结构体memcpy出来_面试官:请说出线程安全的 ArrayList 有哪些,除了Vector...

    以下环境是 JDK 1.8 ArrayList 的初始容量 面试官:你看过 ArrayList 的源码? Python 小星:看过 面试官:那你说下ArrayList 的初始容量是多少? Python ...

  8. arraylist 后往前遍历_面试官:请说出线程安全的 ArrayList 有哪些,除了Vector

    以下环境是 JDK 1.8 ArrayList 的初始容量 面试官:你看过 ArrayList 的源码? Python 小星:看过 面试官:那你说下ArrayList 的初始容量是多少? Python ...

  9. 防止浏览器窗口太小,设置最小宽度

    页面设置宽高百分比的时候,防止浏览器窗口改变太小,页面内容发生改变 给页面设置最小宽度和最小高度 .contain{width : 100%;min-width: 810px;height: 100% ...

  10. 电脑声音太小如何增强_感觉手机音量太小了?教你这样设置,声音立马大上许多...

    不管是打电话,还是看电视,如果觉得手机的声音太小了,总会感到听起来很吃力,那么我们遇到这种情况,可以怎么办呢?建议大家看看下面这个方法,或许会让你的手机音量瞬间变大. 1.打开单声道音频 如今电脑已经 ...

最新文章

  1. java通过kerberos认证连接hdfs并写数
  2. CH451 点阵LED显示模块
  3. 都大二下学期了,说说读大学的感受吧
  4. visual studio c/c++报错:向“printf”传递了额外参数: _Param_(2) 未由格式字符串使用
  5. JS如何禁止别人查看网站源码
  6. css3--弹性布局
  7. android java打开wap链接,Android 链接 java 服务无法打开链接的有关问题
  8. 浙江省计算机数据库三级报名,浙江省计算机等级考试三级数据库技术
  9. Anaconda python安装使用
  10. mysql字符集插入中文数据乱码问题
  11. 11.GitLab webhooks
  12. @Transactional注解属性(3)
  13. GWR模型报错汇总(arcgis与GWR4)
  14. html5 blockquote,HTML5 blockquote 标签
  15. android手机和包支付,和包支付app
  16. Flutter 页面更新流程剖析
  17. 牛客小白月赛21 I I love you(dp的优化)
  18. 大数据起步之wormhole初识
  19. 【Hack The Box】linux练习-- Doctor
  20. Coursera | Introduction to Data Science in Python(University of Michigan)| Assignment4

热门文章

  1. 概念理解之装箱与拆箱
  2. poj 1226 Substrings kmp 好题,我调试了一晚上啊!!汗
  3. Pr 入门教程,如何确保剪辑保持同步?
  4. Ps 初学者教程,如何在图片中创造双色效果?
  5. Integrity Pro for mac(网站死链接清理)
  6. 用 Tenorshare ReiBoot修复iPhone无法开机
  7. 如何从Mac删除Bing重定向
  8. 浅谈Get和Post方法的区别与TCP与UDP区别
  9. Logstash配置方法
  10. Maven自定义配置生成包名