Bootstrap

TIME_WAIT、CLOSE_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状态。

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、建立连接协议(三次握手)

  (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 命令进行了检查。

 

 

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 的四次挥手是怎么样的流程:

 

 

用中文来描述下这个过程:

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 状态。

那么我推断出现这种情况可能的原因有以下几种:

  1. 负载均衡器 异常退出了, 这基本是不可能的,他出现问题绝对是大面积的服务报警,而不仅仅是我一个服务
  2. MySQL负载均衡器 的超时设置的太短了,导致业务代码还没有处理完,MySQL负载均衡器 就关闭tcp连接了 这也不太可能,因为这个服务并没有什么耗时操作,当然还是去检查了负载均衡器的配置,设置的是60s。
  3. 代码问题,MySQL 连接无法释放 目前看起来应该是代码质量问题,加之本次数据有异常,触发到了以前某个没有测试到的点,目前看起来很有可能是这个原因

查找错误原因

由于代码的业务逻辑并不是我写的,我担心一时半会看不出来问题,所以直接使用 perf 把所有的调用关系使用火焰图给绘制出来。既然上面我们推断代码中没有释放mysql连接。无非就是:

  1. 确实没有调用close
  2. 有耗时操作(火焰图可以非常明显看到),导致超时了
  3. 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、各种基础设施出了问题(人总是先怀疑别人)。导致在这上面费了不少时间。

理一下正确的分析思路:

  1. 出现问题后,立马应该检查日志,确实日志没有发现问题;
  2. 监控明确显示了socket不断增长,很明确立马应该使用 netstat 检查情况看看是哪个进程的锅;
  3. 根据 netstat 的检查,使用 tcpdump 抓包分析一下为什么连接会被动断开(TCP知识非常重要);
  4. 如果熟悉代码应该直接去检查业务代码,如果不熟悉则可以使用 perf 把代码的调用链路打印出来;
  5. 不论是分析代码还是火焰图,到此应该能够很快定位到问题。

那么本次到底是为什么会出现 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

我参考的一篇文章对这种情况提出了两个思考题,我觉得非常有意义,大家自己思考下:

  1. 为什么一台机器几百个 CLOSE_WAIR 就导致不可继续访问?我们不是经常说一台机器有 65535 个文件描述符可用吗?
  2. 为什么我有负载均衡,而两台部署服务的机器确几乎同时出了 CLOSE_WAIR ?

 

来自:http://blog.csdn.net/shootyou/article/details/6622226

 http://blog.csdn.net/shootyou/article/details/6615051

 

;