历时一周总算把导致服务大量 CLOSE_WAIT 的原因给找到了。打印任务调用栈果然的必备手段啊!

问题描述

Python 服务 A,用于接收心跳包确认其他服务是否存活。其他服务每 5 分钟向 A 发送一次心跳包;总计 < 100 个其他服务。

  • 05-11 19:30 ,首次出现 Python 服务大量 CLOSE_WAIT,至 13 日发现,总计 10k 左右 CLOSE_WAIT 的 TCP 连接。05-13 15:30 通过运维平台重新部署…
  • 05-14 16:30 ,再次出现。19:30 手动重启。
  • 其间给 Python 服务 A 添加了打印调用栈的模块 pdbx,通过运维平台重新部署
  • 05-17 19:00 ,再次出现。等待打印调用栈,不小心杀掉了服务…
  • 05-19 14:30 ,重现。
  • 05-20 08:30 ,查找原因,解决问题。

由于不是本人负责的服务,于 16 日凭兴趣开始有限介入,协助排查。现将排查流程一一记述,给自己和大家未来排查问题提供一个借鉴。

问题探究

CLOSE_WAIT 现象梳理

16 日拿到的信息只有前两次问题记录。

大量 CLOSE_WAIT 状态的 TCP 连接,Recv-Q 数据大小平均在 300 Bytes 左右。对比实际的 HTTP 包大小,恰好是一次 HTTP Request 请求大小。同时参考 Nginx 打印的日志,确定是一次 HTTP 请求的数据包大小,同时都以 Timeout 为结束。

OK,简单地建立起了第一个印象,TCP 断开连接的 FIN 包由 Nginx 请求超时后发起(proxy_read_timeout 300s) 。A 服务所在的 Linux 机器接收到 FIN 包,并由内核线程直接将套接字状态置位为 CLOSE_WAIT

A 服务没意识到 FIN 包?当然,接收了 FIN 包之后,再回应 ACK 包,这“四次挥手”的第二个数据包并不会被 A 服务所认知,而是由内核的 [ksoftirqd] 内核线程直接处理。看下内核处理 FIN 包的代码 (from net/ipv4/tcp_input.c)。

/** 处理 FIN 包** 如果当前套接字是 ESTABLISHED 状态,则接收 FIN 包,进入 CLOSE-WAIT* 状态 * ...*/
void tcp_fin(struct sock *sk)
{struct tcp_sock *tp = tcp_sk(sk);inet_csk_schedule_ack(sk);sk->sk_shutdown |= RCV_SHUTDOWN;sock_set_flag(sk, SOCK_DONE);switch (sk->sk_state) {case TCP_SYN_RECV:case TCP_ESTABLISHED:/* 当前为 ESTABLISHED 状态,进入 CLOSE_WAIT */tcp_set_state(sk, TCP_CLOSE_WAIT);inet_csk(sk)->icsk_ack.pingpong = 1;break;// ... more omitted }// ... more omitted
}

更多关于网络相关的内核实现请参考 理解 Linux Kernel (8) - 网络

再结合 Recv-Q 恰好等于一次完整 HTTP 请求的条件,可以认识到 Python 线程根本没有 recv 请求数据。更别提意识到远端发出来 FIN 包。

到此为止,由前两次的问题得到的线索,只能认为 Python 服务 A 的所有工作线程都在哪段代码阻塞了。而我所能认知的唯一条件就是事务死锁(当然,我犯了比较大的错误)。事实上看代码根本就没有任何死锁代码。暂时无果。

打印线程调用栈 (一次失败的尝试)

集成了调用栈打印工具 pdbx (强烈安利,我们老大写的,很好用的工具)之后,按理说 18 日解决问题是非常容易的。

结果按照文档的描述,使用 kill -30 <PID> ,Python 服务进程莫名挂掉。功亏一篑啊。后来查看源码发现 pdbx 重定义了 USR1 信号的处理函数。T_T Linux x86 平台下,对应的信号值为 10.


额外地穿插一点信号相关的内容。signal 是一种异步处理机制,一般由执行流在退出内核态回到用户态之前 Check 当前任务的信号队列(为什么会主动 check 呢?当然是内核代码提前写好的流程),并主动针对获得的信号调用对应的信号处理函数。

可怕的是,之前竟然一直没有意识到信号值在不同的 CPU 架构下是不同的。USR1 在 x86 平台下是 10,在 平台下是 30 。而 BSD 又与 Linux 的编号方案不同。

Linux supports the standard signals listed below. Several signal numbers are architecture-dependent, as indicated in the “Value” column. (Where three values are given, the first one is usually valid for alpha and sparc, the middle one for x86, arm, and most other architectures, and the last one for mips. (Values for parisc are not shown; see the Linux kernel source for signal numbering on that architecture.) A dash (-) denotes that a signal is absent on the corresponding architecture.
Linux 支持的标准信号如下。一些信号值是平台相关的(Value 列,第一个值用于 alpha 和 sparc 平台,中间的值用于 x86、arm 和大量其他架构,最后一个值用于 mips 平台)

Signal     Value     Action   Comment
----------------------------------------------------------------------
SIGHUP        1       Term    Hangup detected on controlling terminal or death of controlling process
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGKILL       9       Term    Kill signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2

总结下,最靠谱的写法就是直接用信号的字符描述啦。kill -s USR1 <PID>


打印线程调用栈

19 日下午再次重现,等到 20 日周一上班,总是成功地拿到了线程的调用栈。

"CP Server Thread-10" tid=139694658537216at self.__bootstrap_inner()(threading.py:785)at self.run()(threading.py:812)at conn.communicate()(wsgiserver2.py:1532)... more omittedat instances = list(db.select('instance', vars, where='app_name=$app_name and host=$host and port=$port'))(xxx.py:233)at return self.query(qout, processed=True)(db.py:720)at self._db_execute(db_cursor, sql_query)(db.py:680)at print('%s (%s): %s' % (round(b-a, 2), self.ctx.dbq_count, str(sql_query)), file=debug)(db.py:623)at out.write(x)(webapi.py:512)"CP Server Thread-9" tid=139694666929920at self.__bootstrap_inner()(threading.py:785)at self.run()(threading.py:812)at conn.communicate()(wsgiserver2.py:1532)... more omitted at apps = list(db.select('application', vars, where='name=$name'))(xxx.py:149)at return self.query(qout, processed=True)(db.py:720)at self._db_execute(db_cursor, sql_query)(db.py:680)at print('%s (%s): %s' % (round(b-a, 2), self.ctx.dbq_count, str(sql_query)), file=debug)(db.py:623)at out.write(x)(webapi.py:512)"MainThread" tid=139695478638400at app.run()(xxx.py:326)at return wsgi.runwsgi(self.wsgifunc(*middleware))(application.py:341)at return httpserver.runsimple(func, server_addr)(wsgi.py:59)at server.start()(httpserver.py:177)at self.tick()(wsgiserver2.py:1956)at s, addr = self.socket.accept()(wsgiserver2.py:2008)at sock, addr = self._sock.accept()(socket.py:202)at pystack()(pdbx.py:181)at for filename, lineno, _, line in traceback.extract_stack(stack):(pdbx.py:169)... omit CP Server Thread-1 / Thread-2 / Thread-6 / Thread-7 / Thread-8
... same as other thread"CP Server Thread-4" tid=139695180908288at self.__bootstrap_inner()(threading.py:785)at self.run()(threading.py:812)at conn.communicate()(wsgiserver2.py:1532)... more omitted at instances = list(db.select('instance', vars, where='app_name=$app_name and host=$host and port=$port'))(xxx.py:233)at return self.query(qout, processed=True)(db.py:720)at self._db_execute(db_cursor, sql_query)(db.py:680)at print('%s (%s): %s' % (round(b-a, 2), self.ctx.dbq_count, str(sql_query)), file=debug)(db.py:623)at out.write(x)(webapi.py:512)"APScheduler" tid=139695219828480at self.__bootstrap_inner()(threading.py:785)at self.run()(threading.py:812)at self.__target(*self.__args, **self.__kwargs)(threading.py:765)at self._event.wait(wait_seconds)(blocking.py:28)at self.__cond.wait(timeout, balancing)(threading.py:622)at waiter.acquire()(threading.py:339)"CP Server Thread-3" tid=139695189300992at self.__bootstrap_inner()(threading.py:785)at self.run()(threading.py:812)at conn.communicate()(wsgiserver2.py:1532)... more omitted at instances = list(db.select('instance', vars, where='app_name=$app_name and host=$host and port=$port'))(xxx.py:233)at return self.query(qout, processed=True)(db.py:720)at self._db_execute(db_cursor, sql_query)(db.py:680)at print('%s (%s): %s' % (round(b-a, 2), self.ctx.dbq_count, str(sql_query)), file=debug)(db.py:623)at out.write(x)(webapi.py:512)"CP Server Thread-5" tid=139695172515584at self.__bootstrap_inner()(threading.py:785)at self.run()(threading.py:812)at conn.communicate()(wsgiserver2.py:1532)... more omitted at instances = list(db.select('instance', vars, where='app_name=$app_name and host=$host and port=$port'))(xxx.py:233)at return self.query(qout, processed=True)(db.py:720)at self._db_execute(db_cursor, sql_query)(db.py:680)at print('%s (%s): %s' % (round(b-a, 2), self.ctx.dbq_count, str(sql_query)), file=debug)(db.py:623)at out.write(x)(webapi.py:512)

OK,总计 10 个工作线程,全都阻塞在 out.write(x) ,多次打印调用栈结果相同。基本可以断定是 write 方法出了问题了。不过直到这个时候,都还是深陷在死锁原因中无法自拔(还是接触得太少了)。

虚度半个多钟… 后看代码了解到,其准备将 SQL 执行结果打印到标准错误。

def _debugwrite(x):try:out = ctx.environ['wsgi.errors']except:out = sys.stderrout.write(x)
debug.write = _debugwrite

果断去查看 Python 服务的文件描述符 lsof -p <PID> / ls -al /proc/<PID>/fd 。总算见着可信的证据了。

> lsof -p <PID>
python  139756 sysop    0r  FIFO     0,9       0t0 5210639 pipe
python  139756 sysop    1w   REG   253,1      5187 1188500 /home/www/wwwroot/logs/eswitch-console.9999.log
python  139756 sysop    2w  FIFO     0,9       0t0 5210641 pipe
python  139756 sysop    3u  IPv4 5210787       0t0     TCP 10.1.5.60:65442->10.1.6.104:mysql (ESTABLISHED)
python  139756 sysop    4r  FIFO     0,9       0t0 5210774 pipe
python  139756 sysop    5w  FIFO     0,9       0t0 5210774 pipe
python  139756 sysop    6r  FIFO     0,9       0t0 5210778 pipe
python  139756 sysop    7w  FIFO     0,9       0t0 5210778 pipe
python  139756 sysop    8u  IPv4 5210788       0t0     TCP *:distinct (LISTEN)
python  139756 sysop    9u  IPv4 5226722       0t0     TCP localhost:distinct->localhost:26032 (CLOSE_WAIT)
python  139756 sysop   10u  IPv4 5226858       0t0     TCP localhost:distinct->localhost:26052 (CLOSE_WAIT)
... more omitted
> ls -al /proc/<PID>/fd
lr-x------ 1 sysop sysop 64 May 20 13:47 0 -> pipe:[5210639]
l-wx------ 1 sysop sysop 64 May 20 13:47 1 -> /home/path/to/logs/xxx.log
l-wx------ 1 sysop sysop 64 May 20 13:47 2 -> pipe:[5210641]
lrwx------ 1 sysop sysop 64 May 20 13:47 3 -> socket:[5210787]
lr-x------ 1 sysop sysop 64 May 20 13:47 4 -> pipe:[5210774]
l-wx------ 1 sysop sysop 64 May 20 13:47 5 -> pipe:[5210774]
lr-x------ 1 sysop sysop 64 May 20 13:47 6 -> pipe:[5210778]
l-wx------ 1 sysop sysop 64 May 20 13:47 7 -> pipe:[5210778]
... more omitted

标准错误(fd = 2)指向了一个匿名管道(inode = 5210641)。但是找遍整个系统都没有这个匿名管道的读取方,只有写入方

> sudo lsof | grep 5210641
bash      139747          sysop    2w     FIFO                0,9       0t0    5210641 pipe
python    139756          sysop    2w     FIFO                0,9       0t0    5210641 pipe
python    139756 139764   sysop    2w     FIFO                0,9       0t0    5210641 pipe

OK,合理的解释出现了。管道的缓冲区大小有限(经过测试是 64KB),Python 服务在不断地向标准错误打印日志,从而导致匿名管道(inode = 5210641)的缓冲区数据不断积累,最终达到满的状态。这个过程大概需要一天多的心跳包才能积累得到。而系统调用 write 发现标准错误对应的文件已满,为等待 IO 就绪而陷入睡眠状态。同时,Main 线程在不断地 Accept 新的套接字的建立,而没有更多工作线程来处理新的请求,Nginx 因为连接超时直接发出来 FIN 包。最后,积累了几千/上万请求的 Python 服务也没法主动 close 已有的套接字(即 FIN,ACK 包因为没法发送而在本机不断积累 CLOSE_WAIT 状态的连接)

重定向标准错误的原因

至于为何会将标准错误重定向到一个匿名管道呢?百思不得其解,不过最后还是从应用负责人那儿得到了提示。新的发布方式采用了全新的启动命令:

ssh -fn xx.xx.x.60 'cd /home/path/to/xxx ; python -u xxx.py 8080 >> /home/path/to/logs/xxx.log'

一看就能和前面的原因衔接,使用 ssh 直接调用命名,不会建立一个终端,由 ssh 远程命令启动的任务,其标准输入/输出/错误默认将会通过管道重定向的 ssh server 端,并通过网络传回发起的 ssh client 端。

以发起 ssh -fn ... 的机器为 xx.192,目标机器为 xx.60

xx.192                       |           xx.60-----------------------------+-------------------------------| ssh client                 |          ssh serve           python serve+------------+               |        +------------+       +-------------+| 0 | stdin  |               |        | y | pipe[a]| <---> | 0 | pipe[a] || 1 | stdout |               |        | z | pipe[b]| <-+   | 1 | xxx.log || 2 | stderr |               |        |   |        |   +-> | 2 | pipe[b] || ...        |               |        | ...        |       | ...         || x | sock   | <--------------------> | w | sock   |       | ...         |+------------+               |        +------------+       +-------------+

Python 服务通过管道与 ssh serve 进行进程间交互,最终会将标准错误打印到 192 机器的标准输出/错误上(一般是终端,也就是我们将在终端上看到 Python 服务的错误日志)。而问题在于 ssh serve 任务被杀掉了,从而 pipe[b] 只能不断地向匿名管道写入数据,而没有任务读取数据,一天多时间也就将缓冲区写满了。

总结

到此为止,问题总算得到了圆满的解释,历时 5 天。最重要的就是通过调用栈定位代码阻塞的原因,否则就真是瞎找。对于匿名管道问题的发现、write 系统调用导致的线程睡眠,都是建立在调用栈信息之上的定向探究的过程。

  __                    __                  / _| __ _ _ __   __ _ / _| ___ _ __   __ _
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
|  _| (_| | | | | (_| |  _|  __/ | | | (_| |
|_|  \__,_|_| |_|\__, |_|  \___|_| |_|\__, ||___/                |___/

pipe 导致的 CLOSE_WAIT相关推荐

  1. 一次生产环境大量CLOSE_WAIT导致服务无法访问的定位过程

    1.症状 生产环境的一个服务突然无法访问,服务的交互过程如下所示: 所有的请求都是通过网关进入,之后分发到后端服务. 现在的情况是用户服务无法访问商旅服务,网关有大量java.net.SocketTi ...

  2. 记一次 OOM 的原因和处理 出现大量 close_wait,项目无法访问 activeMq和 poi 出现的 OOM

    大家好,我是烤鸭: 记一次项目无法访问的事件和处理.由于某个模块每隔几天就会出现无法访问,目前的最简单粗暴的方法就是重启. 1. 现象 项目内日志打印正常,经过dubbo的rpc服务和接口调用正常.h ...

  3. 服务器遇到Broken Pipe崩溃

    ==============================第1篇===================================================== 最近产品线中又碰到Brok ...

  4. SpringCloud 编译错误:caught when processing request to {}-unix://localhost:80: Broken pipe

    I/O exception (java.io.IOException) caught when processing request to {}->unix://localhost:80: Br ...

  5. close_wait状态和time_wait状态

    不久前,我的Socket Client程序遇到了一个非常尴尬的错误.它本来应该在一个socket长连接上持续不断地向服务器发送数据,如果socket连接断开,那么程序会自动不断地重试建立连接. 有一天 ...

  6. 云上机器CLOSE_WAIT过多的原因和解决办法

    文章目录 1.问题描述 1.情景再现 2.排查思路 1.我们先看一下TCP状态转换图: 2.知道了这种状态,那我们分析一下哪些情况会产生这种状态呢: 1.系统负载高 2.程序没有设置超时断开 3.查看 ...

  7. Atitit.http httpclient实践java c# .net php attilax总结

    Atitit.http httpclient实践java c# .net php attilax总结 1. Navtree>> net .http1 2. Httpclient理论1 2. ...

  8. 如何用 Netty 设计一个百万级推送服务?

    1. 背景 1.1. 话题来源 最近很多从事移动互联网和物联网开发的同学给我发邮件或者微博私信我,咨询推送服务相关的问题.问题五花八门,在帮助大家答疑解惑的过程中,我也对问题进行了总结,大概可以归纳为 ...

  9. 基于Netty的百万级推送服务设计要点

    1. 背景 1.1. 话题来源 最近很多从事移动互联网和物联网开发的同学给我发邮件或者微博私信我,咨询推送服务相关的问题.问题五花八门,在帮助大家答疑解惑的过程中,我也对问题进行了总结,大概可以归纳为 ...

最新文章

  1. Neo4j: 迁移MySQL的数据到Neo4j
  2. keepalived vrrp协议
  3. Spark SQL(五)之数据加载与存储
  4. c++ 函数指针_进化论——从函数指针到被结构封装的函数指针及参数的应用举例...
  5. 分析ASP.NET读取XML文件4种方法
  6. django url 路由设置技巧
  7. 旧的起点(开园说明)
  8. 管理感悟:错误是员工的,责任是领导的
  9. volatile详解
  10. ThinkPHP6校园疫情防控管理系统
  11. jmeter 参数为邮箱@出现%40解决办法
  12. error in opening zip file
  13. echarts实现中国地图踩过的坑--------有些省的value可以拿到,有些省的value拿不到
  14. 突破领英限制如何查找非好友电话,邮箱技巧
  15. 中关村连夜征集138项黑科技加入肺炎阻击战
  16. Java项目:springboot园区管理系统
  17. i5系列的服务器cpu,i5 系列 CPU 的所有型号。
  18. css 图片平铺和不平铺
  19. android便签工具下载,爱便签(多功能效率工具)
  20. 利用计算机或图形计算器在,利用图形计算器编写趣味数字游戏

热门文章

  1. 网上买书选三家,购物不要选当当
  2. 微软官方方式重装系统
  3. java开发 cad软件_java语言可以用于cad开发吗
  4. Macleod薄膜专题设计中高级课程
  5. 学习科学与技术(计划)
  6. 在线就能用!10个帮你修图+剪辑视频+PPT制作的网站
  7. Wincc服务器右下RT状态,西门子WinCC运行版7.4 (RT128)
  8. 循环链表构建及解决约瑟夫环、逢七过、链表逆置问题
  9. WEB前端设置A4页面布局的轮子: paper-css
  10. Kali linux 学习笔记(六十一)密码破解——离线破解(Hashcat、oclhashcat、RainbowCrack、John) 2020.4.6