[转帖]线上大量CLOSE_WAIT的原因深入分析

大量,close,wait,原因,深入分析 · 浏览次数 : 0

小编点评

**文章分析** 文章分析主要集中在以下几个方面: 1. MySQL负载均衡器的出现问题及解决方法 2. CLOSE_WAIT状态及如何引发 3. 火焰图分析及错误分析 4. 代码逻辑分析及排版 **主要分析结果** 1. MySQL负载均衡器出现问题时,主要原因是服务端没有主动进行close操作。 2. CLOSE_WAIT状态如何引发主要是因为服务端没有进行回应。 3. 火焰图分析显示,服务端在遇到错误时会进行close操作,但由于关闭操作没有进行,导致服务端无法继续访问。 4. 代码逻辑分析主要集中在服务端关闭操作及close包的处理中。 **文章排版** 文章排版主要集中在以下几个方面: 1. 火焰图分析及错误分析 2. 代码逻辑分析及排版 3. 文章结论 **文章结论** 文章主要分析了 MySQL负载均衡器出现问题及解决方法、CLOSE_WAIT状态及如何引发、火焰图分析及错误分析及代码逻辑分析。文章排版主要集中在以上几个方面。

正文

这一次重启真的无法解决问题了:一次 MySQL 主动关闭,导致服务出现大量 CLOSE_WAIT 的全流程排查过程。

近日遇到一个线上服务 socket 资源被不断打满的情况。通过各种工具分析线上问题,定位到问题代码。这里对该问题发现、修复过程进行一下复盘总结。

先看两张图。一张图是服务正常时监控到的 socket 状态,另一张当然就是异常啦!

线上大量CLOSE_WAIT的原因深入分析

 

图一:正常时监控

线上大量CLOSE_WAIT的原因深入分析

 

图二:异常时监控

从图中的表现情况来看,就是从 04:00 开始,socket 资源不断上涨,每个谷底时重启后恢复到正常值,然后继续不断上涨不释放,而且每次达到峰值的间隔时间越来越短。

重启后,排查了日志,没有看到 panic ,此时也就没有进一步检查,真的以为重启大法好。

情况说明

该服务已经上线正常运行将近一年,提供给其它服务调用,主要底层资源有DB/Redis/MQ。

为了后续说明的方便,将服务的架构图进行一下说明。

线上大量CLOSE_WAIT的原因深入分析

 

图三:服务架构

架构是非常简单。 问题出现在早上 08:20 左右开始的,报警收到该服务出现 504,此时第一反应是该服务长时间没有重启(快两个月了),可能存在一些内存泄漏,没有多想直接进行了重启。也就是在图二第一个谷底的时候,经过重启服务恢复到正常水平(重启真好用,开心)。

将近 14:00 的时候,再次被告警出现了 504 ,当时心中略感不对劲,但由于当天恰好有一场大型促销活动,因此先立马再次重启服务。直到后续大概过了1小时后又开始告警,连续几次重启后,发现需要重启的时间间隔越来越短。此时发现问题绝不简单。这一次重启真的解决不了问题老,因此立马申请机器权限、开始排查问题。下面的截图全部来源我的重现demo,与线上无关。

发现问题

出现问题后,首先要进行分析推断、然后验证、最后定位修改。根据当时的表现是分别进行了以下猜想。

推断一

socket 资源被不断打满,并且之前从未出现过,今日突然出现,怀疑是不是请求量太大压垮服务

经过查看实时 qps 后,放弃该想法,虽然量有增加,但依然在服务器承受范围(远远未达到压测的基准值)。

推断二

两台机器故障是同时发生,重启一台,另外一台也会得到缓解,作为独立部署在两个集群的服务非常诡异

有了上面的的依据,推出的结果是肯定是该服务依赖的底层资源除了问题,要不然不可能独立集群的服务同时出问题。

由于监控显示是 socket 问题,因此通过 netstat 命令查看了当前tcp链接的情况(本地测试,线上实际值大的多)

  1. /go/src/hello # netstat -na | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
  2. LISTEN 2
  3. CLOSE_WAIT 23 # 非常异常
  4. TIME_WAIT 1

发现绝大部份的链接处于CLOSE_WAIT状态,这是非常不可思议情况。然后用netstat -an命令进行了检查。

线上大量CLOSE_WAIT的原因深入分析

 

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

线上大量CLOSE_WAIT的原因深入分析

 

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

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抓包分析下:

  1. /go # tcpdump -n port 3306
  2. # 发生了 3次握手
  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
  4. 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
  5. 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
  6. # mysql 主动断开链接
  7. 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包给我
  8. 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给它
  9. ... ... # 本来还需要我发送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_WAIT 状态,但是后续作为被动关闭方的我,并没有发送 FIN,导致我服务端一直处于 CLOSE_WAIT 状态,无法最终进入 CLOSED 状态。 那么我推断出现这种情况可能的原因有以下几种:

1、负载均衡器异常退出了,

这基本是不可能的,他出现问题绝对是大面积的服务报警,而不仅仅是我一个服务

2、MySQL负载均衡器的超时设置的太短了,导致业务代码还没有处理完,MySQL负载均衡器就关闭tcp连接了

这也不太可能,因为这个服务并没有什么耗时操作,当然还是去检查了负载均衡器的配置,设置的是60s。

3、代码问题,MySQL连接无法释放

目前看起来应该是代码质量问题,加之本次数据有异常,触发到了以前某个没有测试到的点,目前看起来很有可能是这个原因

查找错误原因

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

  1. 确实没有调用close
  2. 有耗时操作(火焰图可以非常明显看到),导致超时了
  3. mysql的事务没有正确处理,例如:rollback 或者 commit

由于火焰图包含的内容太多,为了让大家看清楚,我把一些不必要的信息进行了折叠。

线上大量CLOSE_WAIT的原因深入分析

 

图五:有问题的火焰图

火焰图很明显看到了开启了事务,但是在余下的部分,并没有看到Commit或者是Rollback操作。这肯定会操作问题。然后也清楚看到出现问题的是:

MainController.update方法内部,话不多说,直接到 update 方法中去检查。发现了如下代码:

  1. func (c *MainController) update() (flag bool) {
  2. o := orm.NewOrm()
  3. o.Using("default")
  4. o.Begin()
  5. nilMap := getMapNil()
  6. if nilMap == nil {// 这里只检查了是否为nil,并没有进行rollback或者commit
  7. return false
  8. }
  9. nilMap[10] = 1
  10. nilMap[20] = 2
  11. if nilMap == nil && len(nilMap) == 0 {
  12. o.Rollback()
  13. return false
  14. }
  15. sql := "update tb_user set name=%s where id=%d"
  16. res, err := o.Raw(sql, "Bug", 2).Exec()
  17. if err == nil {
  18. num, _ := res.RowsAffected()
  19. fmt.Println("mysql row affected nums: ", num)
  20. o.Commit()
  21. return true
  22. }
  23. o.Rollback()
  24. return false
  25. }

至此,全部分析结束。经过查看getMapNil返回了nil,但是下面的判断条件没有进行回滚。

  1. if nilMap == nil {
  2. o.Rollback()// 这里进行回滚
  3. return false
  4. }

总结

整个分析过程还是废了不少时间。最主要的是主观意识太强,觉得运行了一年没有出问题的为什么会突然出问题?因此一开始是质疑 SRE、DBA、各种基础设施出了问题(人总是先怀疑别人)。导致在这上面费了不少时间。

理一下正确的分析思路:

1、出现问题后,立马应该检查日志,确实日志没有发现问题;

2、监控明确显示了socket不断增长,很明确立马应该使用 netstat 检查情况看看是哪个进程的锅;

3、根据 netstat 的检查,使用 tcpdump 抓包分析一下为什么连接会被动断开(TCP知识非常重要);

4、如果熟悉代码应该直接去检查业务代码,如果不熟悉则可以使用 perf 把代码的调用链路打印出来;

5、不论是分析代码还是火焰图,到此应该能够很快定位到问题。 那么本次到底是为什么会出现CLOSE_WAIT呢?大部分同学应该已经明白了,我这里再简单说明一下:

由于那一行代码没有对事务进行回滚,导致服务端没有主动发起close。因此 MySQL负载均衡器 在达到 60s 的时候主动触发了close操作,但是通过tcp抓包发现,服务端并没有进行回应,这是因为代码中的事务没有处理,因此从而导致大量的端口、连接资源被占用。在贴一下挥手时的抓包数据:

  1. # mysql 主动断开链接
  2. 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包给我
  3. 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给它

希望此文对大家排查线上问题有所帮助。为了便于帮助大家理解,下面附上正确情况下的火焰图与错误情况下的火焰图。大家可以自行对比。

线上大量CLOSE_WAIT的原因深入分析

 

正确的火焰图

线上大量CLOSE_WAIT的原因深入分析

 

错误的火焰图

这里提出两个思考题,我觉得非常有意义,大家自己思考下:

  1. 为什么一台机器几百个 CLOSE_WAIT 就导致不可继续访问?我们不是经常说一台机器有 65535 个文件描述符可用吗?
  2. 为什么我有负载均衡,而两台部署服务的机器确几乎同时出了 CLOSE_WAIT ?
    <div id="blogExtensionBox" style="width:400px;margin:auto;margin-top:12px" class="blog-extension-box"><div class="blog_extension blog_extension_type1" id="blog_extension">
      <div class="blog_extension_card" data-report-click="{&quot;spm&quot;:&quot;1001.2101.3001.6470&quot;}">
        <div class="blog_extension_card_left">
        <img src="https://img-blog.csdnimg.cn/0b81383ee5884706b9f81c618a5384b4.png" alt="">
      </div>
        <div class="blog_extension_card_cont">
          <div class="blog_extension_card_cont_l">
            <span class="text">一口Linux</span>
            <div class="blog_extension_card_cont_r">
              <img class="weixin" src="https://g.csdnimg.cn/extension-box/1.1.6/image/weixin.png" alt="">
              <span>微信公众号</span>
              <img class="go" src="https://g.csdnimg.cn/extension-box/1.1.6/image/ic_move.png" alt="">
            </div>
          </div>
          <span class="style">嵌入式Linux硬核号主!</span>
        </div>
      </div></div></div>
</article>

与[转帖]线上大量CLOSE_WAIT的原因深入分析相似的内容:

[转帖]线上大量CLOSE_WAIT的原因深入分析

这一次重启真的无法解决问题了:一次 MySQL 主动关闭,导致服务出现大量 CLOSE_WAIT 的全流程排查过程。 近日遇到一个线上服务 socket 资源被不断打满的情况。通过各种工具分析线上问题,定位到问题代码。这里对该问题发现、修复过程进行一下复盘总结。 先看两张图。一张图是服务正常时监控到

[转帖]线上Java 高CPU占用、高内存占用排查思路

一、前言 处理过线上问题的同学基本上都会遇到系统突然运行缓慢,CPU 100%,以及Full GC次数过多的问题。当然,这些问题的最终导致的直观现象就是系统运行缓慢,并且有大量的报警。本文主要针对系统运行缓慢这一问题,提供该问题的排查思路,从而定位出问题的代码点,进而提供解决该问题的思路。 二、分析

[转帖]JVM在SUSE下奔溃的原因

今天同事去厂商部署项目的时候发现项目启动一会便会奔溃,但是项目已经在其他的厂商和自己公司的云服务器上稳定运行了很长时间,且经过了大量严格的功能、并发测试,这让我有些诧异,通过了了解,知道了对方机器的操作系统为SUSE,同事发回了jvm奔溃时生成的hs_err_pid文件。通过查看hs_err_pid

[转帖]彻底理解并解决服务器出现大量TIME_WAIT

https://zhuanlan.zhihu.com/p/567088021?utm_id=0 首先我们说下状态 TIME_WAIT 出现的原因 TCP的新建连接,断开连接的流程和各个状态,如下图所示 由上图可知:TIME_WAIT 是主动断开连接的一方会出现的,客户端,服务器都有可能出现 当客户端

[转帖]Linux中的Page cache和Buffer cache详解

1、内存情况 在讲解Linux内存管理时已经提到,当你在Linux下频繁存取文件后,即使系统上没有运行许多程序,也会占用大量的物理内存。这是因为当你读写文件的时候,Linux内核为了提高读写的性能和速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结

[转帖]DBWR与LGWR的写入机制

https://www.jianshu.com/p/6c87cb6cd320 读与写是每个数据库提供的最基本的功能。当数据库中出现第一个进程时,总免不了要将数据从磁盘上加载到内存中,一次数据库的物理I/O由此发生。而这对应着数据库的读事件。通常大多数情况下,数据库中不仅会伴随着大量的读,也会产生大量

[转帖]线上环境 Linux 系统调用追踪

线上环境 Linux 系统调用追踪 PingCAP 提到如何动态追踪进程中的系统调用,相信大家第一时间都能想到 strace,它的基本用法非常简单,非常适合用来解决 “为什么这个软件无法在这台机器上运行?” 这类问题。但如果需要分析线上服务 (特别是延迟敏感型)的某些系统调用的延迟时,strace

[转帖]线上问题零发生,闲鱼稳定性问题治理与监控优化

http://blog.itpub.net/28285180/viewspace-2940749/ 一、引言 闲鱼作为C2C电商交易平台,消息系统是导购链路上关键的一环。用户依赖聊天建立买家与卖家的信任,进一步获取商品信息。闲鱼消息的稳定性直接影响到闲鱼用户体验,成交效率。为强化闲鱼消息系统的稳定性

[转帖]线上一个隐匿 Bug 的复盘

前言 之前负责的一个项目上线好久了,最近突然爆出一 Bug,最后评估影响范围将 Bug 升级成了故障,只因为影响的数据量有 10000 条左右,对业务方造成了一定的影响。 但因为不涉及到资金损失,Bug 修复后对数据进行修补,所以最终级别也是较低的。 今天和大家分享这个线上隐匿的 Bug,也好在工作

[转帖]回放线上流量利器-GoReplay

https://www.cnblogs.com/Chary/p/16829396.html 一. 线上引流产生背景 日常大部分的测试工作都是在测试环境下,通过模拟用户的行为来对系统进行验证,包括功能以及性能。在这个过程中,你可能会遇到以下问题: 用户访问行为比较复杂,模拟很难和用户行为一致,模拟不够