如何杀掉 close_wait 状态的连接

CLOSE_WAIT 连接状态说明

在 tcp 连接状态中, LISTEN, ESTABLISHEDTIME_WAIT 可能是最为常见的三类状态. 相比而言 CLOSE_WAIT 就比较少见, 大多数情况下 CLOSE_WAIT 状态持续的时间会很短, 如果持续时间很长, 就意味着程序处理可能出现了异常. 如下图所示:

      TCP A                                                    TCP B

  1.  ESTABLISHED                                                  ESTABLISHED

  2.  (Close)
      FIN-WAIT-1  --> <SEQ=100><ACK=300><CTL=FIN,ACK>          --> CLOSE-WAIT

  3.  FIN-WAIT-2  <-- <SEQ=300><ACK=101><CTL=ACK>              <-- CLOSE-WAIT

  4.                                                               (Close)
      TIME-WAIT   <-- <SEQ=300><ACK=101><CTL=FIN,ACK>          <-- LAST-ACK

  5.  TIME-WAIT   --> <SEQ=101><ACK=301><CTL=ACK>              --< CLOSED

  6.  (2 MSL)
      CLOSED

在 A, B 两端都为 ESTABLISHED 的情况下, B 收到 A 的 fin 包后(比如 A 主动要断开该连接), 同时返回给 ack 包给 A, 连接状态就会变更为 CLOSE-WAIT, 此后, B 继续发送 fin 包给 A 后, 连接状态才能改变到 LAST-ACK. 由此可以猜想到 B 的连接状态如果一直处于 CLOSE-WAIT(系统不会回收该状态, 直到人为干预或重启程序), 就表示 B 没有发 fin 包给对方. 通常造成这种情况很大的一部分原因是 B 的服务程序没有正常关闭连接.

如何杀掉 CLOSE_WAIT 状态的连接

了解到原理后, 可以猜到有两种方式杀掉 CLOSE_WAIT 状态的连接:

重启程序

如果可以重启程序, 这种就是最有效的方式, 但所有连接都会释放, 重启后还是会出现新的 CLOSE_WAIT 连接. 这种方式最好是在修复程序后再重启.

不重启程序

要在不影响程序的前提下释放 CLOSE_WAIT, 就需要想办法干预到该 tcp 连接状态. 干预的方式大致可以分为 伪造数据包gdb 调试 两种方式:

伪造数据包

一些开源工具提供了干预 tcp 连接状态的方法, 比如 killcxkill-close-wait-connections, 这两种工具本质上都是伪造数据包向 tcp 连接的两端发送数据, 让原始的端响应进而引起 tcp 连接状态改变, 前者可以很好的处理 ESTABLISHED 状态的连接, 后者则由于难以干预到程序服务的连接会出现 kill 失败的情况.

gdb 调试

gdb 方式更为稳妥, 原理则主要是先获取到处于 CLOSE_WAIT 连接的 fd 句柄, 再通过 gdb 对该进程的执行关闭 fd 的操作. 可以参考文章 remove_close-wait-connection, 执行以下操作:

# 通过 ss 拿到对应连接的 fd

ss -tap | grep CLOSE-WAIT
CLOSE-WAIT 1   0       10.0.0.11:9999      10.0.0.12:56990      users:(("nc",pid=6117,fd=417)


# 通过 gdb 强制关闭对应的 fd

gdb -p 6117 -batch -ex 'print (int)close(417)'

上述两种方式各有优缺点, 不过也给我们提供了很多处理类似问题的思路. 但重要的是不管采用哪种方式, 都需要在测试环境做更多的测试.

参考

https://www.baeldung.com/linux/remove-close_wait-connection
https://killcx.sourceforge.net/
https://github.com/rghose/kill-close-wait-connections
https://access.redhat.com/solutions/437133