引言
在当今复杂多变的网络环境中,TCP 连接的管理已成为影响系统性能和可靠性的关键因素。其中,TCP TIME_WAIT 状态下的端口重用问题尤为棘手,它不仅涉及 TCP 协议的底层机制,还与实际网络环境的复杂性密切相关。本文将通过理论分析和实践验证来探讨这一问题。
偶发的交易失败
在一个大型企业的互联网业务系统中,部分客户端在访问系统时偶尔会遇到交易失败。这种失败看似随机发生,却足以影响用户体验和业务运营。为了找出问题的根源,我们开展了一系列深入的网络分析工作。
我们首先收集了大量的网络报文,并在两个关键点进行了数据包捕获:
捕获点 A:靠近客户端
捕获点 B:在网络路径上,该点右侧连接广域网
通过对这些数据包的仔细分析,我们逐渐揭示了问题的现象:
在捕获点 A,我们观察到客户端发出 SYN 报文尝试建立 TCP 连接,但服务端并未回应预期的SYN-ACK。相反,服务端直接返回了 RST 报文,导致 TCP 握手失败。
更有趣的是,在捕获点 B 的观察中,我们发现客户端在短时间内,具体为 9.4 秒,试图重用相同的端口发起新的连接。然而,服务端以 RST 报文拒绝了这些请求。
初步诊断
这些观察结果指向了一个现象:TCP 端口重用被拒绝。
1、网络中存在 NAT(网络地址转换)设备:A、B 两点之间的网络设备做了 SNAT,而 B 到服务端之间的网络设备,如防火墙和负载均衡器,也可能会进行 NAT 转换。 2、在我们可观察到的 A、B 两点之间的网络设备上,由于 NAT 池资源不足,导致在短时间内重用了仍处于 TIME_WAIT 状态的端口。 3、“服务端”因某种原因拒绝了这些重用端口的连接请求。但这里的“服务端”到底是最右端服务器,还是它和捕获点 B 之间的某个网络设备,我们缺乏直接观测点,还不能下结论。
TCP TIME_WAIT 状态
TIME_WAIT 状态是 TCP 连接生命周期中一个常被忽视却至关重要的阶段。它由主动关闭连接的一方进入,通常持续 2MSL(Maximum Segment Lifetime)的时间。在大多数 Linux 系统中,MSL 时间默认设置为 30 秒,因此 TIME_WAIT 状态将持续 60 秒。
TIME_WAIT 解决的问题
TIME_WAIT 状态在 TCP 协议中扮演着重要角色:
防止旧连接的数据包干扰新连接:在网络环境中,数据包的传输并非总是即时的。有时,属于已关闭连接的数据包可能会延迟到达。如果没有 TIME_WAIT 状态,这些延迟的数据包可能会被误认为是新建立的连接(使用相同的四元组)的一部分,从而导致数据混乱。
确保被动关闭方能够正常关闭连接:在四次挥手的最后阶段,如果主动关闭方(进入 TIME_WAIT 状态的一方)发送的最后一个 ACK 丢失,被动关闭方会重新发送 FIN。TIME_WAIT 状态确保主动关闭方在这段时间内仍能响应,重新发送 ACK,从而保证连接的完整关闭。
TIME_WAIT 的形成过程
第一次挥手:服务端发送 FIN 报文,表示不再发送数据,但仍可接收数据。此时服务端进入FIN_WAIT_1 状态。
第二次挥手:客户端收到 FIN 后,发送 ACK 作为响应,并进入 CLOSE_WAIT 状态。服务端收到 ACK 后,进入 FIN_WAIT_2 状态。
第三次挥手:客户端处理完剩余数据后,发送 FIN 报文,表示准备关闭连接。客户端此时进入 LAST_ACK 状态。
第四次挥手:服务端收到客户端的 FIN 后,发送 ACK 作为响应,并进入 TIME_WAIT 状态。客户端收到 ACK 后,立即关闭连接(CLOSED 状态)。
主机 TIME_WAIT 状态下的端口重用行为
TIME_WAIT 状态下,服务器如何处理来自相同客户端、相同端口的新连接请求。
时间戳选项(TCP Timestamps)对端口重用行为的影响。
序列号(Sequence Number)对端口重用行为的影响。
客户端:IP 地址为 10.2.2.136 服务端:IP 地址为 10.2.2.237,端口 80,运行 httpd 服务 操作系统:CentOS Linux release 7.9.2009(Core) 内核版本:Linux 3.10.0-1160.el7.x86_64
端口重用成功的场景
重用端口的 SYN 报文,时间戳大于服务端最后收到的报文,但序列号小于服务端最后收到的报文。
初始连接的建立与关闭:
数据传输完成后,双方通过 FIN 和 ACK 报文关闭连接。
服务端回应 SYN-ACK,客户端确认,连接成功建立。
源 IP 10.2.2.236 向目的 IP 10.2.2.237 发送 SYN 报文,源端口为 47982,目的端口为 80。
TIME_WAIT 状态的产生与持续:
连接关闭后,服务端(主动关闭方)进入 TIME_WAIT 状态,预计持续 60 秒。
端口重用的尝试:
最后客户端 RST 重置,是因为模拟程序的缘故,并不会真正完成握手。如果是真实场景,客户端此时应当返回 ACK,完成 TCP 三次握手的全过程,进入数据传输阶段。
服务端发送 SYN-ACK 响应,确认了重用端口的 SYN 报文,更新序列号和确认号,变迁为 SYN_RECV 状态。
尽管新 SYN 的序列号 100 小于服务端最后收到的序列号 3262892139,服务端仍然接受了这个连接请求。
新 SYN 报文的时间戳为 85878072,大于服务端最后收到的时间戳 83454786。
在 TIME_WAIT 期间,源 IP 10.2.2.236 再次使用源端口 47982 向目的端口 80 发起新的 SYN 请求。
端口重用失败的场景
在这个场景中,我们观察到:
初始连接的建立与关闭:
与场景一类似,建立并关闭了初始连接。
TIME_WAIT 状态的产生与持续:
服务端进入 TIME_WAIT 状态,等待 2MSL 时间。
端口重用的尝试:
源 IP 10.2.2.236 再次使用源端口 47990 向目的端口 80 发起新的 SYN 请求。
新 SYN 报文的时间戳为 84489294,小于服务端最后收到报文的时间戳 85489294。
新 SYN 报文的序列号为 587125688,大于服务端最后收到报文的序列号 586125781。
服务端拒绝建立连接,返回 ACK 报文,该报文没有 SYN 标志位,同时重复了对客户端 FIN 报文的 ACK 确认号,表示拒绝建立连接。
最后客户端 RST 重置,是因为模拟程序的缘故,并不会真正完成握手。而此时,如果是真实场景,处于 SYN_SENT 状态下的客户端,也的确会返回 RST。
参数值为 0,收到 RST 报文后,会结束 TIME_WAIT 状态,变迁为 CLOSED 状态,释放连接。
参数值为 1,收到 RST 报文后直接丢弃,继续 TIME_WAIT 状态,直到 2MSL 时间结束。
实验结论
只要 SYN 报文的时间戳数值大于服务端最后收到的报文,哪怕序列号小于服务端最后收到的报文,即可成功复用端口,使处于 TIME_WAIT 状态的 Socket 从新进入 SYN_RECV 状态。 只要 SYN 报文的时间戳数值小于服务端最后收到的报文,处于 TIME_WAIT 状态的 Socket 将拒绝连接,遵循 TIME_WAIT 机制,返回一个重复的 ACK 报文,并回到 TIME_WAIT 状态。
SYN 报文的序列号数值大于服务端最后收到的报文,可成功复用端口,连接建立成功。
SYN 报文的序列号数值小于服务端最后收到的报文,连接被 Socket 拒绝,建立失败。
这些实验结果为我们理解 TIME_WAIT 状态下的端口重用机制提供了事实依据。然而,当我们将这些结论与开篇案例中观察到的现象进行对比时,发现存在不一致之处。这种差异引发了我们进一步的思考和分析。
案例现象与实验结果的差异
在标准的 TCP 协议实现中,“TIME_WAIT 状态下的 Socket 收到 SYN 报文”这种情况并不会直接触发 RST 机制。而根据《TCP/IP详解 卷1:协议》中的描述,触发 RST 的主要场景包括:
客户端连接请求,即 SYN 报文,发送到服务端不存在的端口。 客户端或服务端,主动终止一个连接。 在一个已关闭的 TCP 连接上收到了报文段。
网络环境差异
在捕获点 A、B 之间的网络设备上,做了SNAT 的转换。
在网络设备 A 右侧的位置,的确在 10 秒内重用了 TCP 端口,随之而来的是 RST 报文。
结论与建议
综合以上分析,我们可以推断,案例中观察到的问题现象,很可能是由于网络路径中的中间设备,如防火墙或负载均衡器的特定实现所导致的。这种行为虽然与标准TCP实现有所不同,但在实际网络环境中并不罕见,它反映了网络设备厂商在处理复杂网络场景时所做的权衡和优化。
因此,在多样的网络环境和系统版本中,如果使用抓包方式观察分析此类问题,我们可能会看到相同的特征和不同的现象。
总结端口重用导致的连接失败的相同特征在于:
NAT网络结构
部分连接失败,而不是全部失败
在短时间内发生了相同 TCP 四元组的重用,这个时间可能是 60 秒、120 秒
参考资料与阅读推荐
[1] Coping with the TCP TIME-WAIT state on busy Linux servers, by Vincent Bernat
[2] 解Bug之路-NAT引发的性能瓶颈 by 无毁的湖光-Al
[3] 在 TIME_WAIT 状态的 TCP 连接,收到 SYN 后会发生什么? by 小林coding
[4] 机械工业出版社《TCP/IP详解 卷1:协议》 by W. Richard Stevens