请说说你对TCP链接中time_wait状态的理解javascript
解答:
先上TCP的状态变迁图html

这幅图来自《TCP IP详解卷1:协议 原书第2版中文》13.5 TCP状态转换图前端

这幅图来自《UNIX网络编程,卷1:套接字联网API》2.6.4 TCP状态转换图java
1. time_wait状态如何产生?
由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK以后会进入time_wait的状态,也就说该发送方会保持2MSL时间以后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP链接在2MSL链接等待期间,定义这个链接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。mysql
2.time_wait状态产生的缘由linux
1)为实现TCP全双工链接的可靠释放nginx
由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)为使旧的数据包在网络因过时而消失web
为说明这个问题,咱们先假设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状态。
TCP/IP详解--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)客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程当中的报文1.
(2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。所以它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通信。
(3) 客户必须再次回应服务段一个ACK报文,这是报文段3.
二、链接终止协议(四次握手)
因为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 {
至此,所有分析结束。通过查看 getMapNil 返回了nil,可是下面的判断条件没有进行回滚。
if nilMap == nil { o.Rollback()
总结
整个分析过程仍是废了很多时间。最主要的是主观意识太强,以为运行了一年没有出问题的为何会忽然出问题?所以一开始是质疑 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
在服务器的平常维护过程当中,会常常用到下面的命令:
- 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引用下面一段话:
- MSL 為 一個 TCP Segment (某一塊 TCP 網路封包) 從來源送到目的之間可續存的時間 (也就是一個網路封包在網路上傳輸時能存活的時間),由 於 RFC 793 TCP 傳輸協定是在 1981 年定義的,當時的網路速度不像現在的網際網路那樣發達,你能够想像你從瀏覽器輸入網址等到第一 個 byte 出現要等 4 分鐘嗎?在現在的網路環境下幾乎不可能有這種事情發生,所以我們大可將 TIME_WAIT 狀態的續存時間大幅調低,好 讓 連線埠 (Ports) 能更快空出來給其余連線使用。
再引用网络资源的一段话:
- 值 得一说的是,对于基于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文件的修改:
- #对于一个新建链接,内核要发送多少个 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
修改完以后执行/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_*一系列参数,是用来设置服务器检测链接存活的相关配置。
2.服务器保持了大量CLOSE_WAIT状态
休息一下,喘口气,一开始只是打算说说TIME_WAIT和CLOSE_WAIT的区别,没想到越挖越深,这也是写博客总结的好处,总能够有意外的收获。
TIME_WAIT状态能够经过优化服务器参数获得解决,由于发生TIME_WAIT的状况是服务器本身可控的,要么就是对方链接的异常,要么就是本身没有迅速回收资源,总之不是因为本身程序错误致使的。
但 是CLOSE_WAIT就不同了,从上面的图能够看出来,若是一直保持在CLOSE_WAIT状态,那么只有一种状况,就是在对方关闭链接以后服务器程 序本身没有进一步发出ack信号。换句话说,就是在对方链接关闭以后,程序里没有检测到,或者程序压根就忘记了这个时候须要关闭链接,因而这个资源就一直 被程序占着。我的以为这种状况,经过服务器内核参数也没办法解决,服务器对于程序抢占的资源没有主动回收的权利,除非终止程序运行。
在那边日志里头我举了个场景,来讲明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
这个是高并发服务端常见的一个问题,通常的作法是修改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: *
应答包:
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: *
应答包:
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。

一、 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链接的时候,来自链接先前化身的重复分组已经在网络中消逝。
二、大量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协议的成功。
三、案列分析:
首先,根据一个查询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套接字拖死。