最近遇到一个线上报警:服务器出现大量TIME_WAIT导致其无法与下游模块建立新HTTP连接,在解决过程中,通过查阅经典教材和技术文章,加深了对TCP网络问题的理解。作为笔记,记录于此。
备注:本文主要介绍TCP编程中涉及到的众多基础知识,关于实际工程中对由TIME_WAIT引发的不能建立新连接问题的解决方法将在下篇笔记中给出。
1. 实际问题
初步查看发现,无法对外新建TCP连接时,线上服务器存在大量处于TIME_WAIT状态的TCP连接(最多的一次为单机10w+,其中引起报警的那个模块产生的TIME_WAIT约2w),导致其无法跟下游模块建立新TCP连接。
TIME_WAIT涉及到TCP释放连接过程中的状态迁移,也涉及到具体的socket api对TCP状态的影响,下面开始逐步介绍这些概念。
2. TCP状态迁移
面向连接的TCP协议要求每次peer间通信前建立一条TCP连接,该连接可抽象为一个4元组(four-tuple,有时也称socket pair):(local_ip, local_port, remote_ip,remote_port),这4个元素唯一地代表一条TCP连接。
1)TCP Connection Establishment
TCP建立连接的过程,通常又叫“三次握手”(three-way handshake),可用下图来示意:
可对上图做如下解释:
a. client向server发送SYN并约定初始包序号(sequence number)为J;
b. server发送自己的SYN并表明初始包序号为K,同时,针对client的SYNJ返回ACKJ+1(注:J+1表示server期望的来自该client的下一个包序为J+1);
c. client收到来自server的SYN+ACK后,发送ACKK+1,至此,TCP建立成功。
其实,在TCP建立时的3次握手过程中,还要通过SYN包商定各自的MSS,timestamp等参数,这涉及到协议的细节,本文旨在抛砖引玉,不再展开。
2)TCPConnection Termination
与建立连接的3次握手相对应,释放一条TCP连接时,需要经过四步交互(又称“四次挥手”),如下图所示:
可对上图做如下解释:
a. 连接的某一方先调用close()发起主动关闭(active close),该api会促使TCP传输层向remotepeer发送FIN包,该包表明发起active close的application不再发送数据(特别注意:这里“不再发送数据”的承诺是从应用层角度来看的,在TCP传输层,还是要将该application对应的内核tcp send buffer中当前尚未发出的数据发到链路上)。
remote peer收到FIN后,需要完成被动关闭(passive close),具体分为两步:
b. 首先,在TCP传输层,先针对对方的FIN包发出ACK包(主要ACK的包序是在对方FIN包序基础上加1);
c. 接着,应用层的application收到对方的EOF(end-of-file,对方的FIN包作为EOF传给应用层的application)后,得知这条连接不会再有来自对方的数据,于是也调用close()关闭连接,该close会促使TCP传输层发送FIN。
d. 发起主动关闭的peer收到remote peer的FIN后,发送ACK包,至此,TCP连接关闭。
注意1:TCP连接的任一方均可以首先调用close()以发起主动关闭,上图以client主动发起关闭做说明,而不是说只能client发起主动关闭。
注意2:上面给出的TCP建立/释放连接的过程描述中,未考虑由于各种原因引起的重传、拥塞控制等协议细节,感兴趣的同学可以查看各种TCP RFC Documents ,比如TCP RFC793。
3)TCP StateTransition Diagram
上面介绍了TCP建立、释放连接的过程,此处对TCP状态机的迁移过程做总体说明。将TCP RFC793中描述的TCP状态机迁移图摘出如下(下图引用自这里):
TCP状态机共含11个状态,状态间在各种socket apis的驱动下进行迁移,虽然此图看起来错综复杂,但对于有一定TCP网络编程经验的同学来说,理解起来还是比较容易的。限于篇幅,本文不准备展开详述,想了解具体迁移过程的新手同学,建议阅读《Linux Network Programming Volume1》第2.6节。
3. TIME_WAIT状态
经过前面的铺垫,终于要讲到与本文主题相关的内容了。 ^_^
从TCP状态迁移图可知,只有首先调用close()发起主动关闭的一方才会进入TIME_WAIT状态,而且是必须进入(图中左下角所示的3条状态迁移线最终均要进入该状态才能回到初始的CLOSED状态)。
从图中还可看到,进入TIME_WAIT状态的TCP连接需要经过2MSL才能回到初始状态,其中,MSL是指Max
Segment Lifetime,即数据包在网络中的最大生存时间。每种TCP协议的实现方法均要指定一个合适的MSL值,如RFC1122给出的建议值为2分钟,又如Berkeley体系的TCP实现通常选择30秒作为MSL值。这意味着TIME_WAIT的典型持续时间为1-4分钟。
TIME_WAIT状态存在的原因主要有两点:
1)为实现TCP这种全双工(full-duplex)连接的可靠释放
参考本文前面给出的TCP释放连接4次挥手示意图,假设发起active close的一方(图中为client)发送的ACK(4次交互的最后一个包)在网络中丢失,那么由于TCP的重传机制,执行passiveclose的一方(图中为server)需要重发其FIN,在该FIN到达client(client是active close发起方)之前,client必须维护这条连接的状态(尽管它已调用过close),具体而言,就是这条TCP连接对应的(local_ip, local_port)资源不能被立即释放或重新分配。直到romete peer重发的FIN达到,client也重发ACK后,该TCP连接才能恢复初始的CLOSED状态。如果activeclose方不进入TIME_WAIT以维护其连接状态,则当passive close方重发的FIN达到时,active close方的TCP传输层会以RST包响应对方,这会被对方认为有错误发生(而事实上,这是正常的关闭连接过程,并非异常)。
2)为使旧的数据包在网络因过期而消失
为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。
具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。
4. socket api: close() 和 shutdown()
由前面内容可知,对一条TCP连接而言,首先调用close()的一方会进入TIME_WAIT状态,除此之外,关于close()还有一些细节需要说明。
对一个tcp socket调用close()的默认动作是将该socket标记为已关闭并立即返回到调用该api进程中。此时,从应用层来看,该socket fd不能再被进程使用,即不能再作为read或write的参数。而从传输层来看,TCP会尝试将目前send buffer中积压的数据发到链路上,然后才会发起TCP的4次挥手以彻底关闭TCP连接。
调用close()是关闭TCP连接的正常方式,但这种方式存在两个限制,而这正是引入shutdown()的原因:
1)close()其实只是将socket fd的引用计数减1,只有当该socket fd的引用计数减至0时,TCP传输层才会发起4次握手从而真正关闭连接。而shutdown则可以直接发起关闭连接所需的4次握手,而不用受到引用计数的限制;
2)close()会终止TCP的双工链路。由于TCP连接的全双工特性,可能会存在这样的应用场景:local peer不会再向remote peer发送数据,而remote peer可能还有数据需要发送过来,在这种情况下,如果local peer想要通知remote peer自己不会再发送数据但还会继续收数据这个事实,用close()是不行的,而shutdown()可以完成这个任务。
close()和shutdown()的具体调用方法可以man查看,此处不再赘述。
以上就是本文要分析和解决的“由于TIME_WAIT太多导致无法对外建立新连接”问题所需要掌握的基础知识。下一篇笔记会在本文基础上介绍这个问题具体的解决方法。^_^
【参考资料】
1.《Linux Network Programming Volume 1》. Chapter 2 && Chapter 4
2. TCP RFC 793
3. Online Document: TCP StateTransition Diagram
================ EOF ===============
分享到:
相关推荐
【Linux网络编程笔记】TCP短连接产生大量TIME_WAIT导致无法对外建立新TCP连接的原因及解决方法,这是一个关于网络编程和Linux系统配置的问题。在TCP/IP通信中,TIME_WAIT状态是TCP连接生命周期的一部分,用于确保...
Linux Socket编程是Linux系统中进行网络通信的核心技术,它为应用程序提供了一种接口,使得不同进程之间或同一台机器上的进程之间可以通过网络进行数据交换。本笔记将深入探讨Linux Socket编程的基本概念、API使用...
这篇笔记深入探讨了Linux系统下的TCP/IP协议栈工作原理和实现细节,对于学习网络通信、操作系统内核以及网络安全的研究者非常有价值。 首先,TCP/IP协议栈分为四层模型:应用层、传输层、网络层和数据链路层。在...
- TCP连接的建立与断开涉及到状态机的概念,如SYN_SENT、ESTABLISHED、FIN_WAIT_1等。 4. **端口与IP地址**:网络通信中,端口与IP地址是识别主机和进程的关键。端口号范围从0到65535,其中0到1023是系统保留端口...
### ACE网络编程学习笔记知识点详解 #### 一、面向对象中间件体系结构 **1.1 主机基础设施中间件** 主机基础设施中间件的主要目的是封装不同的底层实现,例如socket和线程,提供统一的接口给上层应用。这种封装有...
然而,在TCP三次握手过程中可能出现某些异常情况,例如由于网络延迟导致的一个连接请求报文段在某个网络节点长时间滞留,然后在延迟后到达服务器,造成服务器误以为是新的连接请求。这种情况下,客户端实际上没有...
笔记可能讲解IP路由、TCP连接建立与关闭、UDP数据发送、套接字编程等。 7. **内核模块**:Linux内核支持动态加载和卸载模块,这对于开发和调试非常有用。笔记可能涵盖模块的编译、加载、卸载方法,以及模块间依赖的...
在深入探讨UNIX网络编程时,我们关注的核心是TCP/IP连接的生命周期,特别是涉及套接字关闭的细节。在TCP连接的生命周期中,关闭连接通常涉及四个阶段,这被称为"四次挥手"(Four-Way Handshake)过程。下面将详细...
根据提供的文件信息,我们可以归纳出以下关于Unix网络编程的关键知识点: ### 一、TCP/IP连接建立与拆除 在TCP/IP协议栈中,建立一个可靠的连接是通过三次握手完成的,而断开连接则是通过四次挥手来实现的。 1. *...
本笔记将深入探讨Linux应用开发的详细流程,包括操作系统接口的使用、网络通信、设备驱动以及用户界面的设计。以下是对每个主题的详细阐述: 1. **Linux操作系统接口**:Linux作为开源的操作系统,提供了丰富的API...
TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,是互联网协议族的核心部分。TCP 提供了一种确保数据准确无误传输的服务,包括数据的分片、确认机制、超时重传、数据...
9. **TIME_WAIT (2MSL)**:TCP连接关闭后,进入TIME_WAIT状态,等待两倍的MSL(最长报文段生存时间),以确保所有分片能到达目的地并被确认,防止旧数据报的重传干扰新连接。 10. **TCP可靠传输**:TCP通过序列号、...
Linux下C语言开发笔记整理涵盖了从基础知识到网络通信的多个方面,主要围绕在Unix/Linux系统环境下使用C语言进行软件开发的各项技术与理论。以下是从文件提供的信息中提炼的知识点。 ### Unix/Linux系统基本命令和...
Linux C系统编程是计算机科学领域中的一个重要主题,它涉及到操作系统层面的编程,主要使用C语言在Linux环境下进行。这一主题涵盖了多个关键知识点,包括文件操作、进程管理、内存管理、信号处理、网络编程以及系统...
《LinuxUNIX系统编程手册》是一本专为初学者设计的权威指南,涵盖了Linux和UNIX操作系统中的核心编程概念和技术。这本手册深入浅出地讲解了如何在这些类UNIX环境中进行系统级编程,包括与内核交互、管理进程、处理...
这里的“webserver(1)_linux高并发服务器_源码”是一个针对该主题的实践项目,它采用Preactor模式并利用了Linux的epoll LT模型来处理大量并发连接。以下是对这些关键知识点的详细说明: **Linux高并发服务器:** 在...
Java基础是编程世界中至关重要的一个领域,尤其对于那些想要深入理解并掌握Java这门语言的开发者来说。"Core Java"涵盖了Java语言的基础概念、语法、数据类型、控制结构、类与对象、接口、异常处理、集合框架以及多...
攻击者通过伪造大量IP地址发送SYN包,使得服务器在等待确认时积累大量半连接,耗尽服务器资源,导致正常连接无法建立,造成DDoS攻击。检测SYN攻击的方法是观察服务器上有大量半连接状态且源IP地址随机。 另外,TCP...
MySQL调优-以5.6版本为例-详细笔记文档总结 MySQL调优是指对MySQL数据库服务器的优化,以提高其性能、稳定性和安全性。在本文中,我们将讨论MySQL 5.6版本的调优,包括字符集设置、存储引擎设置、应用层面优化、...