time_wait状态产生的原因,危害,如何避免
前言
请说说你对TCP连接中time_wait状态的理解
解答:
先上TCP的状态变迁图
这幅图来自《TCP IP详解卷1:协议 原书第2版中文》13.5 TCP状态转换图
这幅图来自《UNIX网络编程,卷1:套接字联网API》2.6.4 TCP状态转换图
1. time_wait状态如何产生?
由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP连接在2MSL连接等待期间,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。
2.time_wait状态产生的原因
1)为实现TCP全双工连接的可靠释放
由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的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个原因。
3)总结
具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。
3.time_wait状态如何避免
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免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)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。
另一比较深入的说法
TIME_WAIT状态的存在有两个理由:(1)让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。(2)防止lost duplicate对后续新建正常链接的传输造成破坏。lost duplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost duplicate加上incarnation connection,则会对我们的传输造成致命的错误。大家都知道TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000, len=1000, 则tcp认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。
Q: 编写 TCP/SOCK_STREAM 服务程序时,SO_REUSEADDR到底什么意思?
A: 这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息, 指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧 使用同一端口,此时 SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期 望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不可能。
------------------------------------
TCP状态以及握手详解
1、建立连接协议(三次握手)
(1)客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1.
(2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。
(3) 客户必须再次回应服务段一个ACK报文,这是报文段3.
2、连接终止协议(四次握手)
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段4)。
(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个FIN给客户端(报文段6)。
(4) 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。
CLOSED: 这个没什么好说的了,表示初始状态。
LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
ESTABLISHED:这个容易理解了,表示连接已经建立了。
FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。
https://cloud.tencent.com/developer/articles/107?q=hot
近日遇到一个线上服务 socket 资源被不断打满的情况。通过各种工具分析线上问题,定位到问题代码。这里对该问题发现、修复过程进行一下复盘总结。
先看两张图。一张图是服务正常时监控到的 socket 状态,另一张当然就是异常啦!
图一:正常时监控
图二:异常时监控
从图中的表现情况来看,就是从 04:00 开始,socket 资源不断上涨,每个谷底时重启后恢复到正常值,然后继续不断上涨不释放,而且每次达到峰值的间隔时间越来越短。
重启后,排查了日志,没有看到 panic ,此时也就没有进一步检查,真的以为重启大法好。
情况说明
该服务使用Golang开发,已经上线正常运行将近一年,提供给其它服务调用,主要底层资源有DB/Redis/MQ。
为了后续说明的方便,将服务的架构图进行一下说明。
图三:服务架构
架构是非常简单。
问题出现在早上 08:20 左右开始的,报警收到该服务出现 504,此时第一反应是该服务长时间没有重启(快两个月了),可能存在一些内存泄漏,没有多想直接进行了重启。也就是在图二第一个谷底的时候,经过重启服务恢复到正常水平(重启真好用,开心)。
将近 14:00 的时候,再次被告警出现了 504 ,当时心中略感不对劲,但由于当天恰好有一场大型促销活动,因此先立马再次重启服务。直到后续大概过了1小时后又开始告警,连续几次重启后,发现需要重启的时间间隔越来越短。此时发现问题绝不简单。这一次重启真的解决不了问题老,因此立马申请机器权限、开始排查问题。下面的截图全部来源我的重现demo,与线上无关。
发现问题
出现问题后,首先要进行分析推断、然后验证、最后定位修改。根据当时的表现是分别进行了以下猜想。
ps:后续截图全部来源自己本地复现时的截图
推断一
socket 资源被不断打满,并且之前从未出现过,今日突然出现,怀疑是不是请求量太大压垮服务
经过查看实时 qps 后,放弃该想法,虽然量有增加,但依然在服务器承受范围(远远未达到压测的基准值)。
推断二
两台机器故障是同时发生,重启一台,另外一台也会得到缓解,作为独立部署在两个集群的服务非常诡异
有了上面的的依据,推出的结果是肯定是该服务依赖的底层资源除了问题,要不然不可能独立集群的服务同时出问题。
由于监控显示是 socket 问题,因此通过 netstat 命令查看了当前tcp链接的情况(本地测试,线上实际值大的多)
/go/src/hello # netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' LISTEN 2 CLOSE_WAIT 23 # 非常异常 TIME_WAIT 1
发现绝大部份的链接处于 CLOSE_WAIT 状态,这是非常不可思议情况。然后用 netstat -an
命令进行了检查。
图四:大量的CLOSE_WAIT
CLOSED 表示socket连接没被使用。 LISTENING 表示正在监听进入的连接。 SYN_SENT 表示正在试着建立连接。 SYN_RECEIVED 进行连接初始同步。 ESTABLISHED 表示连接已被建立。 CLOSE_WAIT 表示远程计算器关闭连接,正在等待socket连接的关闭。 FIN_WAIT_1 表示socket连接关闭,正在关闭连接。 CLOSING 先关闭本地socket连接,然后关闭远程socket连接,最后等待确认信息。 LAST_ACK 远程计算器关闭后,等待确认信号。 FIN_WAIT_2 socket连接关闭后,等待来自远程计算器的关闭信号。 TIME_WAIT 连接关闭后,等待远程计算器关闭重发。
然后开始重点思考为什么会出现大量的mysql连接是 CLOSE_WAIT 呢?为了说清楚,我们来插播一点TCP的四次挥手知识。
TCP四次挥手
我们来看看 TCP 的四次挥手是怎么样的流程:
图五:TCP四次挥手
用中文来描述下这个过程:
Client: 服务端大哥,我事情都干完了,准备撤了
,这里对应的就是客户端发了一个FIN
Server:知道了,但是你等等我,我还要收收尾
,这里对应的就是服务端收到 FIN 后回应的 ACK
经过上面两步之后,服务端就会处于 CLOSE_WAIT 状态。过了一段时间 Server 收尾完了
Server:小弟,哥哥我做完了,撤吧
,服务端发送了FIN
Client:大哥,再见啊
,这里是客户端对服务端的一个 ACK
到此服务端就可以跑路了,但是客户端还不行。为什么呢?客户端还必须等待 2MSL 个时间,这里为什么客户端还不能直接跑路呢?主要是为了防止发送出去的 ACK 服务端没有收到,服务端重发 FIN 再次来询问,如果客户端发完就跑路了,那么服务端重发的时候就没人理他了。这个等待的时间长度也很讲究。
Maximum Segment Lifetime 报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
这里一定不要被图里的 client/server 和项目里的客户端服务器端混淆,你只要记住:主动关闭的一方发出 FIN 包(Client),被动关闭(Server)的一方响应 ACK 包,此时,被动关闭的一方就进入了 CLOSE_WAIT 状态。如果一切正常,稍后被动关闭的一方也会发出 FIN 包,然后迁移到 LAST_ACK 状态。
既然是这样, TCP 抓包分析下:
/go # tcpdump -n port 3306
# 发生了 3次握手
11:38:15.679863 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [S], seq 4065722321, win 29200, options [mss 1460,sackOK,TS val 2997352 ecr 0,nop,wscale 7], length 0 11:38:15.679923 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [S.], seq 780487619, ack 4065722322, win 28960, options [mss 1460,sackOK,TS val 2997352 ecr 2997352,nop,wscale 7], length 0 11:38:15.679936 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 1, win 229, options [nop,nop,TS val 2997352 ecr 2997352], length 0 # mysql 主动断开链接 11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL负载均衡器发送fin包给我 11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回复ack给它 ... ... # 本来还需要我发送fin给他,但是我没有发,所以出现了close_wait。那这是什么缘故呢?
src > dst: flags data-seqno ack window urgent options src > dst 表明从源地址到目的地址flags 是TCP包中的标志信息,S 是SYN标志, F(FIN), P(PUSH) , R(RST) "."(没有标记)data-seqno 是数据包中的数据的顺序号ack 是下次期望的顺序号window 是接收缓存的窗口大小urgent 表明数据包中是否有紧急指针options 是选项
结合上面的信息,我用文字说明下:MySQL负载均衡器 给我的服务发送 FIN 包,我进行了响应,此时我进入了 CLOSE_WAIR 状态,但是后续作为被动关闭方的我,并没有发送 FIN,导致我服务端一直处于 CLOSE_WAIR 状态,无法最终进入 CLOSED 状态。
那么我推断出现这种情况可能的原因有以下几种:
- 负载均衡器 异常退出了,
这基本是不可能的,他出现问题绝对是大面积的服务报警,而不仅仅是我一个服务
- MySQL负载均衡器 的超时设置的太短了,导致业务代码还没有处理完,MySQL负载均衡器 就关闭tcp连接了
这也不太可能,因为这个服务并没有什么耗时操作,当然还是去检查了负载均衡器的配置,设置的是60s。
- 代码问题,MySQL 连接无法释放
目前看起来应该是代码质量问题,加之本次数据有异常,触发到了以前某个没有测试到的点,目前看起来很有可能是这个原因
查找错误原因
由于代码的业务逻辑并不是我写的,我担心一时半会看不出来问题,所以直接使用 perf
把所有的调用关系使用火焰图给绘制出来。既然上面我们推断代码中没有释放mysql连接。无非就是:
- 确实没有调用close
- 有耗时操作(火焰图可以非常明显看到),导致超时了
- mysql的事务没有正确处理,例如:rollback 或者 commit
由于火焰图包含的内容太多,为了让大家看清楚,我把一些不必要的信息进行了折叠。
图六:有问题的火焰图
火焰图很明显看到了开启了事务,但是在余下的部分,并没有看到 Commit 或者是Rollback 操作。这肯定会操作问题。然后也清楚看到出现问题的是:
MainController.update 方法内部,话不多说,直接到 update 方法中去检查。发现了如下代码:
func (c *MainController) update() (flag bool) { o := orm.NewOrm() o.Using("default") o.Begin() nilMap := getMapNil() if nilMap == nil {// 这里只检查了是否为nil,并没有进行rollback或者commit return false } nilMap[10] = 1 nilMap[20] = 2 if nilMap == nil && len(nilMap) == 0 { o.Rollback() return false } sql := "update tb_user set name=%s where id=%d" res, err := o.Raw(sql, "Bug", 2).Exec() if err == nil { num, _ := res.RowsAffected() fmt.Println("mysql row affected nums: ", num) o.Commit() return true } o.Rollback() return false }
至此,全部分析结束。经过查看 getMapNil 返回了nil,但是下面的判断条件没有进行回滚。
if nilMap == nil {
o.Rollback()// 这里进行回滚 return false }
总结
整个分析过程还是废了不少时间。最主要的是主观意识太强,觉得运行了一年没有出问题的为什么会突然出问题?因此一开始是质疑 SRE、DBA、各种基础设施出了问题(人总是先怀疑别人)。导致在这上面费了不少时间。
理一下正确的分析思路:
- 出现问题后,立马应该检查日志,确实日志没有发现问题;
- 监控明确显示了socket不断增长,很明确立马应该使用
netstat
检查情况看看是哪个进程的锅; - 根据
netstat
的检查,使用tcpdump
抓包分析一下为什么连接会被动断开(TCP知识非常重要); - 如果熟悉代码应该直接去检查业务代码,如果不熟悉则可以使用
perf
把代码的调用链路打印出来; - 不论是分析代码还是火焰图,到此应该能够很快定位到问题。
那么本次到底是为什么会出现 CLOSE_WAIR 呢?大部分同学应该已经明白了,我这里再简单说明一下:
由于那一行代码没有对事务进行回滚,导致服务端没有主动发起close。因此 MySQL负载均衡器 在达到 60s 的时候主动触发了close操作,但是通过tcp抓包发现,服务端并没有进行回应,这是因为代码中的事务没有处理,因此从而导致大量的端口、连接资源被占用。在贴一下挥手时的抓包数据:
# mysql 主动断开链接
11:38:45.693382 IP 172.18.0.3.3306 > 172.18.0.5.38822: Flags [F.], seq 123, ack 144, win 227, options [nop,nop,TS val 3000355 ecr 2997359], length 0 # MySQL负载均衡器发送fin包给我 11:38:45.740958 IP 172.18.0.5.38822 > 172.18.0.3.3306: Flags [.], ack 124, win 229, options [nop,nop,TS val 3000360 ecr 3000355], length 0 # 我回复ack给它
希望此文对大家排查线上问题有所帮助。为了便于帮助大家理解,下面附上正确情况下的火焰图与错误情况下的火焰图。大家可以自行对比。
- 正确情况下的火焰图 : https://dayutalk.cn/img/right.svg
- 错误情况的火焰图 : https://dayutalk.cn/img/err.svg
我参考的一篇文章对这种情况提出了两个思考题,我觉得非常有意义,大家自己思考下:
- 为什么一台机器几百个 CLOSE_WAIR 就导致不可继续访问?我们不是经常说一台机器有 65535 个文件描述符可用吗?
- 为什么我有负载均衡,而两台部署服务的机器确几乎同时出了 CLOSE_WAIR ?
来自:http://blog.csdn.net/shootyou/article/details/6622226
http://blog.csdn.net/shootyou/article/details/6615051
在服务器的日常维护过程中,会经常用到下面的命令:
[plain] view plain copy print ?
- netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
它会显示例如下面的信息:
TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1
常用的三个状态是:ESTABLISHED 表示正在通信,TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。
具体每种状态什么意思,其实无需多说,看看下面这种图就明白了,注意这里提到的服务器应该是业务请求接受处理的一方:
这么多状态不用都记住,只要了解到我上面提到的最常见的三种状态的意义就可以了。一般不到万不得已的情况也不会去查看网络状态,如果服务器出了异常,百分之八九十都是下面两种情况:
1.服务器保持了大量TIME_WAIT状态
2.服务器保持了大量CLOSE_WAIT状态
因为linux分配给一个用户的文件句柄是有限的(可以参考:http://blog.csdn.net/shootyou/article/details/6579139),而TIME_WAIT和CLOSE_WAIT两种状态如果一直被保持,那么意味着对应数目的通道就一直被占着,而且是“占着茅坑不使劲”,一旦达到句柄数上限,新的请求就无法被处理了,接着就是大量Too Many Open Files异常,tomcat崩溃。。。
下 面来讨论下这两种情况的处理方法,网上有很多资料把这两种情况的处理方法混为一谈,以为优化系统内核参数就可以解决问题,其实是不恰当的,优化系统内核参 数解决TIME_WAIT可能很容易,但是应对CLOSE_WAIT的情况还是需要从程序本身出发。现在来分别说说这两种情况的处理方法:
1.服务器保持了大量TIME_WAIT状态
这种情况比较常见,一些爬虫服务器或者WEB服务器(如果网管在安装的时候没有做内核参数优化的话)上经常会遇到这个问题,这个问题是怎么产生的呢?
从 上面的示意图可以看得出来,TIME_WAIT是主动关闭连接的一方保持的状态,对于爬虫服务器来说他本身就是“客户端”,在完成一个爬取任务之后,他就 会发起主动关闭连接,从而进入TIME_WAIT的状态,然后在保持这个状态2MSL(max segment lifetime)时间之后,彻底关闭回收资源。为什么要这么做?明明就已经主动关闭连接了为啥还要保持资源一段时间呢?这个是TCP/IP的设计者规定 的,主要出于以下两个方面的考虑:
1.防止上一次连接中的包,迷路后重新出现,影响新连接(经过2MSL,上一次连接中所有的重复包都会消失)
2. 可靠的关闭TCP连接。在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。另外这么设计TIME_WAIT 会定时的回收资源,并不会占用很大资源的,除非短时间内接受大量请求或者受到攻击。
关于MSL引用下面一段话:
[plain] view plain copy print ?
- MSL 為 一個 TCP Segment (某一塊 TCP 網路封包) 從來源送到目的之間可續存的時間 (也就是一個網路封包在網路上傳輸時能存活的時間),由 於 RFC 793 TCP 傳輸協定是在 1981 年定義的,當時的網路速度不像現在的網際網路那樣發達,你可以想像你從瀏覽器輸入網址等到第一 個 byte 出現要等 4 分鐘嗎?在現在的網路環境下幾乎不可能有這種事情發生,因此我們大可將 TIME_WAIT 狀態的續存時間大幅調低,好 讓 連線埠 (Ports) 能更快空出來給其他連線使用。
再引用网络资源的一段话:
[plain] view plain copy print ?
- 值 得一说的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可 想而知,对于访 问量大的Web Server,会存在大量的TIME_WAIT状态,假如server一秒钟接收1000个请求,那么就会积压 240*1000=240,000个 TIME_WAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些 TIME_WAIT,所以对于新的 TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。
- HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个 request/response,一个主要原因就是发现了这个问题。
也就是说HTTP的交互跟上面画的那个图是不一样的,关闭连接的不是客户端,而是服务器,所以web服务器也是会出现大量的TIME_WAIT的情况的。
现在来说如何来解决这个问题。
解决思路很简单,就是让服务器能够快速回收和重用那些TIME_WAIT的资源。
下面来看一下我们网管对/etc/sysctl.conf文件的修改:
[plain] view plain copy print ?
#对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃,不应该大于255,默认值是5,对应于180秒左右时间
net.ipv4.tcp_syn_retries=2
#net.ipv4.tcp_synack_retries=2
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_orphan_retries=3
#表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_syn_backlog = 4096
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
##减少超时前的探测次数
net.ipv4.tcp_keepalive_probes=5
##优化网络设备接收队列
net.core.netdev_max_backlog=3000
[plain] view plain copy print ?
修改完之后执行/sbin/sysctl -p让参数生效。
这里头主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
这几个参数。
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的开启都是为了回收处于TIME_WAIT状态的资源。
net.ipv4.tcp_fin_timeout这个时间可以减少在异常情况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。
net.ipv4.tcp_keepalive_*一系列参数,是用来设置服务器检测连接存活的相关配置。
关于keepalive的用途可以参考: http://hi.baidu.com/tantea/blog/item/580b9d0218f981793812bb7b.html
2.服务器保持了大量CLOSE_WAIT状态
休息一下,喘口气,一开始只是打算说说TIME_WAIT和CLOSE_WAIT的区别,没想到越挖越深,这也是写博客总结的好处,总可以有意外的收获。
TIME_WAIT状态可以通过优化服务器参数得到解决,因为发生TIME_WAIT的情况是服务器自己可控的,要么就是对方连接的异常,要么就是自己没有迅速回收资源,总之不是由于自己程序错误导致的。
但 是CLOSE_WAIT就不一样了,从上面的图可以看出来,如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程 序自己没有进一步发出ack信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直 被程序占着。个人觉得这种情况,通过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
如果你使用的是HttpClient并且你遇到了大量CLOSE_WAIT的情况,那么这篇日志也许对你有用: http://blog.csdn.net/shootyou/article/details/6615051
在那边日志里头我举了个场景,来说明CLOSE_WAIT和TIME_WAIT的区别,这里重新描述一下:
服 务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常情况下,如果请求成功,那么在抓取完 资源后,服务器A会主动发出关闭连接的请求,这个时候就是主动关闭连接,服务器A的连接状态我们可以看到是TIME_WAIT。如果一旦发生异常呢?假设 请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭连接的请求,服务器A就是被动的关闭了连接,如果服务器A被动关闭连接之后程序员忘了 让HttpClient释放连接,那就会造成CLOSE_WAIT的状态了。
所以如果将大量CLOSE_WAIT的解决办法总结为一句话那就是:查代码。因为问题出在服务器程序里头啊。
统计在一台前端机上高峰时间TCP连接的情况,统计命令:
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
结果:
除了ESTABLISHED,可以看到连接数比较多的几个状态是:FIN_WAIT1, TIME_WAIT, CLOSE_WAIT, SYN_RECV和LAST_ACK;下面的文章就这几个状态的产生条件、对系统的影响以及处理方式进行简单描述。
发现存在大量TIME_WAIT状态的连接
tcp 0 0 127.0.0.1:3306 127.0.0.1:41378 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:41379 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39352 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39350 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:35763 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39372 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:39373 TIME_WAIT
tcp 0 0 127.0.0.1:3306 127.0.0.1:41176 TIME_WAIT
通过调整内核参数解决
vi /etc/sysctl.conf
编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后执行/sbin/sysctl -p让参数生效。
net.ipv4.tcp_syncookies = 1表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout修改系統默认的TIMEOUT时间
修改之后,再用命令查看TIME_WAIT连接数
netstat -ae|grep “TIME_WAIT” |wc –l
发现大量的TIME_WAIT 已不存在,mysql进程的占用率很快就降下来的,网站访问正常。
不过很多时候,出现大量的TIME_WAIT状态的连接,往往是因为网站程序代码中没有使用mysql.colse(),才导致大量的mysql TIME_WAIT.
根据TCP协议定义的3次握手断开连接规定,发起socket主动关闭的一方 socket将进入TIME_WAIT状态,TIME_WAIT状态将持续2个MSL(Max Segment Lifetime),在Windows下默认为4分钟,即240秒,TIME_WAIT状态下的socket不能被回收使用. 具体现象是对于一个处理大量短连接的服务器,如果是由服务器主动关闭客户端的连接,将导致服务器端存在大量的处于TIME_WAIT状态的socket, 甚至比处于Established状态下的socket多的多,严重影响服务器的处理能力,甚至耗尽可用的socket,停止服务. TIME_WAIT是TCP协议用以保证被重新分配的socket不会受到之前残留的延迟重发报文影响的机制,是必要的逻辑保证.
在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters,添加名为TcpTimedWaitDelay的
DWORD键,设置为60,以缩短TIME_WAIT的等待时间
http://kerry.blog.51cto.com/172631/105233/
修改之后,再用
netstat -ae|grep mysql
tcp 0 0 aaaa:50408 192.168.12.13:mysql ESTABLISHED nobody 3224651
tcp 0 0 aaaa:50417 192.168.12.13:mysql ESTABLISHED nobody 3224673
tcp 0 0 aaaa:50419 192.168.12.13:mysql ESTABLISHED nobody 3224675
发现大量的TIME_WAIT 已不存在,mysql进程的占用率很快就降下来的,各网站访问正常!!
以上只是暂时的解决方法,最后仔细巡查发现是前天新上线的一个系统,程序代码中没有使用mysql.colse(),才导致大量的mysql TIME_WAIT
如果你的服务器是Windows平台,可以修改下面的注册表键值:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters]
"TcpTimedWaitDelay"=dword:0000001e
此值是TIME_WAIT状态的最长时间。缺省为240秒,最低为30秒,最高为300秒。建议为30秒。
注释:
(
1,TCP结束的过程如下:
Server Client
-------------- FIN --------------> server: fin_wait_1
<------------- ACK --------------- client: close_wait server:fin_wait_2
<------------- FIN --------------- client发出fin之后就关闭
-------------- ACK -------------> server发出ack后进入time_wait状态
Time_Wait的默认时间是2倍的MLS,就是240秒钟。MLS是TCP片在网上的最长存活时间。
TIME_Wait的主要作用是保证关闭的TCP端口不立即被使用。因为当网络存在延迟时,可能当某个端口被关闭后,网络中还有一些重传的TCP片在发向这个端口,如果这个端口立即建立新的TCP连接,则可能会有影响。所以使用2倍的MSL时间来限制这个端口立即被使用。
现在的问题在于,4分钟的时间有点长。
因此,Time_wait的影响,我想,首先每个TCP连接都各自有个数据结构,叫TCP Control Block.Time_wait的时候这个数据结构没有被释放。所以当有太多的TCP连接时,内存可能会被占用很多。
2,To ValorZ:TIME_WAIT状态也称为2MSL等待状态,而不是2MLS,笔误吧!
每个TCP报文在网络内的最长时间,就称为MSL(Maximum Segment Lifetime),它的作用和IP数据包的TTL类似。
RFC793指出,MSL的值是2分钟,但是在实际的实现中,常用的值有以下三种:30秒,1分钟,2分钟。
注意一个问题,进入TIME_WAIT状态的一般情况下是客户端,大多数服务器端一般执行被动关闭,不会进入TIME_WAIT状态,当在服务器端关闭某个服务再重新启动时,它是会进入TIME_WAIT状态的。
举例:
1.客户端连接服务器的80服务,这时客户端会启用一个本地的端口访问服务器的80,访问完成后关闭此连接,立刻再次访问服务器的80,这时客户端会启用另一个本地的端口,而不是刚才使用的那个本地端口。原因就是刚才的那个连接还处于TIME_WAIT状态。
2.客户端连接服务器的80服务,这时服务器关闭80端口,立即再次重启80端口的服务,这时可能不会成功启动,原因也是服务器的连接还处于TIME_WAIT状态。
windows
TcpTimedWaitDelay和MaxUserPort设置
描述:确定 TCP/IP 可释放已关闭连接并重用其资源前,必须经过的时间。
关闭和释放之间的此时间间隔通称 TIME_WAIT 状态或两倍最大段生命周期(2MSL)状态。
此时间期间,重新打开到客户机和服务器的连接的成本少于建立新连接。
减少此条目的值允许 TCP/IP 更快地释放已关闭的连接,为新连接提供更多资源。如果运行的应用程序需要快速释放和创建新连接,而且由于 TIME_WAIT 中存在很多连接,导致低吞吐量,则调整此参数。
如何查看或设置: 使用 regedit 命令访问 HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/ Services/TCPIP/Parameters 注册表子键并创建名为 TcpTimedWaitDelay 的新 REG_DWORD 值。
将此值设置为十进制 30,其为十六进制 0x0000001e。
该值将等待时间设置为 30 秒。
停止并重新启动系统。 缺省值:0xF0,它将等待时间设置为 240 秒(4 分钟)。
建议值:最小值为 0x1E,它将等待时间设置为 30 秒。
MaxUserPort 描述:确定在应用程序从系统请求可用用户端口时,TCP/IP 可指定的最高端口号。
如何查看或设置: 使用 regedit 命令访问 HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/ Services/TCPIP/Parameters 注册表子键并创建名为 MaxUserPort 的新 REG_DWORD 值。
停止并重新启动系统。
缺省值:无 建议值:至少十进制 32768。
注:当在 Windows NT 或 Windows 2000 操作系统上调整 WebSphere Application Server 时,同时使用这两个参数。
希望本站的知识能给您的工作、学习和生活带来方便和乐趣!
http://blog.csdn.net/gzh0222/article/details/8491178
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)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。
另一比较深入的说法
TIME_WAIT状态的存在有两个理由:(1)让4次握手关闭流程更加可靠;4次握手的最后一个ACK是是由主动关闭方发送出去的,若这个ACK丢失,被动关闭方会再次发一个FIN过来。若主动关闭方能够保持一个2MSL的TIME_WAIT状态,则有更大的机会让丢失的ACK被再次发送出去。(2)防止lost duplicate对后续新建正常链接的传输造成破坏。lost duplicate在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个packet在路由器A,B,C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数,因此这个包有两种命运,要么最后TTL变为0,在网络中消失;要么TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并先于它达到了目的地,因此它的命运也就注定被TCP协议栈抛弃。另外一个概念叫做incarnation connection,指跟上次的socket pair一摸一样的新连接,叫做incarnation of previous connection。lost duplicate加上incarnation connection,则会对我们的传输造成致命的错误。大家都知道TCP是流式的,所有包到达的顺序是不一致的,依靠序列号由TCP协议栈做顺序的拼接;假设一个incarnation connection这时收到的seq=1000, 来了一个lost duplicate为seq=1000, len=1000, 则tcp认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。通过一个2MSL TIME_WAIT状态,确保所有的lost duplicate都会消失掉,避免对新连接造成错误。
Q: 编写 TCP/SOCK_STREAM 服务程序时,SO_REUSEADDR到底什么意思?
A: 这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用
端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,
指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧
使用同一端口,此时 SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期
望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不
可能。
TIME_WAIT问题
TIME_WAIT
这个是高并发服务端常见的一个问题,一般的做法是修改sysctl的参数来解决。但是,做为一个有追求的程序猿,你需要多问几个为什么,为什么会出现TIME_WAIT?出现这个合理吗?
我们需要先回顾下tcp的知识,请看下面的状态转换图(图片来自「The TCP/IP Guide」):
因为TCP连接是双向的,所以在关闭连接的时候,两个方向各自都需要关闭。先发FIN包的一方执行的是主动关闭;后发FIN包的一方执行的是被动关闭。主动关闭的一方会进入TIME_WAIT状态,并且在此状态停留两倍的MSL时长。
修改sysctl的参数,只是控制TIME_WAIT的数量。你需要很明确的知道,在你的应用场景里面,你预期是服务端还是客户端来主动关闭连接的。一般来说,都是客户端来主动关闭的。
nginx在某些情况下,会主动关闭客户端的请求,这个时候,返回值的connection为close。我们看两个例子:
http 1.0协议
请求包:
GET /hello HTTP/1.0
User-Agent: curl/7.37.1 Host: 127.0.0.1 Accept: */* Accept-Encoding: deflate, gzip
应答包:
HTTP/1.1 200 OK
Date: Wed, 08 Jul 2015 02:53:54 GMT Content-Type: text/plain Connection: close Server: 360 web server hello world
对于http 1.0协议,如果请求头里面没有包含connection,那么应答默认是返回Connection: close,也就是说nginx会主动关闭连接。
user agent
请求包:
POST /api/heartbeat.json HTTP/1.1 Content-Type: application/x-www-form-urlencoded Cache-Control: no-cache User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT) Accept-Encoding: gzip, deflate Accept: */* Connection: Keep-Alive Content-Length: 0
应答包:
HTTP/1.1 200 OK
Date: Mon, 06 Jul 2015 09:35:34 GMT Content-Type: text/plain Transfer-Encoding: chunked Connection: close Server: 360 web server Content-Encoding: gzip
这个请求包是http1.1的协议,也声明了Connection: Keep-Alive,为什么还会被nginx主动关闭呢?_问题出在User-Agent_,nginx认为终端的浏览器版本太低,不支持keep alive,所以直接close了。
在我们应用的场景下,终端不是通过浏览器而是后台请求的,而我们也没法控制终端的User-Agent,那有什么方法不让nginx主动去关闭连接呢?可以用keepalive_disable这个参数来解决。这个参数并不是字面的意思,用来关闭keepalive,而是用来定义哪些古代的浏览器不支持keepalive的,默认值是MSIE6。
keepalive_disable none;
修改为none,就是认为不再通过User-Agent中的浏览器信息,来决定是否keepalive。
1、 time_wait的作用:
TIME_WAIT状态存在的理由:
1)可靠地实现TCP全双工连接的终止
在进行关闭连接四次挥手协议时,最后的ACK是由主动关闭端发出的,如果这个最终的ACK丢失,服务器将重发最终的FIN,
因此客户端必须维护状态信息允许它重发最终的ACK。如果不维持这个状态信息,那么客户端将响应RST分节,服务器将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。
因而,要实现TCP全双工连接的正常终止,必须处理终止序列四个分节中任何一个分节的丢失情况,主动关闭的客户端必须维持状态信息进入TIME_WAIT状态。
2)允许老的重复分节在网络中消逝
TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个原来的迷途分节就称为lost duplicate。
在关闭一个TCP连接后,马上又重新建立起一个相同的IP地址和端口之间的TCP连接,后一个连接被称为前一个连接的化身(incarnation),那么有可能出现这种情况,前一个连接的迷途重复分组在前一个连接终止后出现,从而被误解成从属于新的化身。
为了避免这个情况,TCP不允许处于TIME_WAIT状态的连接启动一个新的化身,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个TCP连接的时候,来自连接先前化身的重复分组已经在网络中消逝。
2、大量TIME_WAIT造成的影响:
在高并发短连接的TCP服务器上,当服务器处理完请求后立刻主动正常关闭连接。这个场景下会出现大量socket处于TIME_WAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上。
我来解释下这个场景。主动正常关闭TCP连接,都会出现TIMEWAIT。
为什么我们要关注这个高并发短连接呢?有两个方面需要注意:
1. 高并发可以让服务器在短时间范围内同时占用大量端口,而端口有个0~65535的范围,并不是很多,刨除系统和其他服务要用的,剩下的就更少了。
2. 在这个场景中,短连接表示“业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接。
这里有个相对长短的概念,比如取一个web页面,1秒钟的http短连接处理完业务,在关闭连接之后,这个业务用过的端口会停留在TIMEWAIT状态几分钟,而这几分钟,其他HTTP请求来临的时候是无法占用此端口的(占着茅坑不拉翔)。单用这个业务计算服务器的利用率会发现,服务器干正经事的时间和端口(资源)被挂着无法被使用的时间的比例是 1:几百,服务器资源严重浪费。(说个题外话,从这个意义出发来考虑服务器性能调优的话,长连接业务的服务就不需要考虑TIMEWAIT状态。同时,假如你对服务器业务场景非常熟悉,你会发现,在实际业务场景中,一般长连接对应的业务的并发量并不会很高。
综合这两个方面,持续的到达一定量的高并发短连接,会使服务器因端口资源不足而拒绝为一部分客户服务。同时,这些端口都是服务器临时分配,无法用SO_REUSEADDR选项解决这个问题。
关于time_wait的反思:
存在即是合理的,既然TCP协议能盛行四十多年,就证明他的设计合理性。所以我们尽可能的使用其原本功能。
依靠TIME_WAIT状态来保证我的服务器程序健壮,服务功能正常。
那是不是就不要性能了呢?并不是。如果服务器上跑的短连接业务量到了我真的必须处理这个TIMEWAIT状态过多的问题的时候,我的原则是尽量处理,而不是跟TIMEWAIT干上,非先除之而后快。
如果尽量处理了,还是解决不了问题,仍然拒绝服务部分请求,那我会采取负载均衡来抗这些高并发的短请求。持续十万并发的短连接请求,两台机器,每台5万个,应该够用了吧。一般的业务量以及国内大部分网站其实并不需要关注这个问题,一句话,达不到时才需要关注这个问题的访问量。
小知识点:
TCP协议发表:1974年12月,卡恩、瑟夫的第一份TCP协议详细说明正式发表。当时美国国防部与三个科学家小组签定了完成TCP/IP的协议,结果由瑟夫领衔的小组捷足先登,首先制定出了通过详细定义的TCP/IP协议标准。当时作了一个试验,将信息包通过点对点的卫星网络,再通过陆地电缆,再通过卫星网络,再由地面传输,贯串欧洲和美国,经过各种电脑系统,全程9.4万公里竟然没有丢失一个数据位,远距离的可靠数据传输证明了TCP/IP协议的成功。
3、案列分析:
首先,根据一个查询TCP连接数,来说明这个问题。
netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
LAST_ACK 14
SYN_RECV 348
ESTABLISHED 70
FIN_WAIT1 229
FIN_WAIT2 30
CLOSING 33
TIME_WAIT 18122
状态描述:
View Code
命令解释:
View Code
如何尽量处理TIMEWAIT过多?
编辑内核文件/etc/sysctl.conf,加入以下内容:
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间
然后执行 /sbin/sysctl -p 让参数生效.
/etc/sysctl.conf是一个允许改变正在运行中的Linux系统的接口,它包含一些TCP/IP堆栈和虚拟内存系统的高级选项,修改内核参数永久生效。
简单来说,就是打开系统的TIMEWAIT重用和快速回收。
如果以上配置调优后性能还不理想,可继续修改一下配置:
vi /etc/sysctl.conf
net.ipv4.tcp_keepalive_time = 1200
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为20分钟。
net.ipv4.ip_local_port_range = 1024 65000
#表示用于向外连接的端口范围。缺省情况下很小:32768到61000,改为1024到65000。
net.ipv4.tcp_max_syn_backlog = 8192
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_tw_buckets = 5000
#表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数字,TIME_WAIT套接字将立刻被清除并打印警告信息。
默认为180000,改为5000。对于Apache、Nginx等服务器,上几行的参数可以很好地减少TIME_WAIT套接字数量,但是对于 Squid,效果却不大。此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的TIME_WAIT套接字拖死。