L4 层网络上总是能遇到奇奇怪怪的错误,记录一下备查。

故障现象:大量异常连接状态

FIN\_WAIT2

CLOSE\_WAIT

服务器大量存在 TCP 状态为 FIN_WAIT2、CLOSE_WAIT 的连接,总计可能有几千上万。

经确认 HAProxy 配置为 maxconn 10000。确认为连接数达到 HAProxy 上限导致了拒绝服务。

背景信息:请求链路

flowchart LR
Browser --> |:8686| HAProxy
HAProxy --> |:31200| ingress-nginx
ingress-nginx --> |dispatch| kubernetes-pods

结合 FIN_WAIT2、CLOSE_WAIT TCP 连接提供的信息,及现场请求链路。

确认:

  • FIN_WAIT2 发生在客户浏览器到 HAProxy 应用
  • CLOSE_WAIT 发生在 HAProxy 到各后端服务的统一路由 ingress-nginx

且两种状态的 TCP 连接数量差不多,故初步判定两者具有相关性。

背景知识:CLOSE_WAIT 异常状态

CLOSE_WAIT 状态发生在被动关闭侧(HAProxy)收到主动关闭侧(ingress-nginx)发送的 FIN 包,响应 ACK 之后。

一般大量观察到 TCP CLOSE_WAIT 状态,说明被动关闭侧进线程陷入阻塞,无法正常处理 TCP 连接关闭动作。

可能的情况有:

  • 工作线程同步等待第三方资源,无法处理其他请求
  • 工作线程发生死锁,无法处理其他请求
  • 工作线程异常死亡,未补充新的工作线程,无法处理其他请求
  • 等等…

背景知识:FIN_WAIT2 异常状态

FIN_WAIT2 状态停留在挥手的第三个阶段(主动关闭侧)。此时收到了 ACK ,但未收到被动关闭侧的 FIN 。

一般会有两种情况会导致 FIN_WAIT2 :

  1. 未发送 FIN:被动关闭侧进线程阻塞,已经无法自主(被动)关闭连接。(被动关闭侧的 ACK 是由内核自动响应主动关闭侧的 FIN ,用户态/应用侧不会感知)
  2. 未接收 FIN:主动关闭侧未收到 FIN 包。

故障分析:TCP 状态分析

结合 FIN_WAIT2 和 CLOSE_WAIT 状态,以及 HAProxy 的工作流程。

可以认为:

  • HAProxy 因浏览器侧连接空闲超时,主动尝试关闭连接,但由于没有收到挥手第三个阶段的 FIN 包,致线程阻塞,TCP 连接状态为 FIN_WAIT2;
  • 而阻塞期间,该工作线程持有的后端代理 TCP 连接,也因 ingress-nginx 空闲超时主动尝试关闭,但因线程阻塞,HAProxy 侧无法关闭连接,TCP 连接状态为 CLOSE_WAIT。

故障分析:未接收到 FIN 包

为了确认为何没有收到挥手第三个阶段的 FIN 包,对特定活跃的客户端 IP 做了抓包尝试。

首先观察到大量的乱序、重传包

同时也明确观察到确实存在挥手阶段未接收到第三阶段的 FIN 包(当然,大部分还是正确挥手结束连接了)

基于此,基本认定为丢包

其他疑问:FIN_WAIT2 为什么长时间存在?

考虑四次挥手时的特殊场景,如果主动关闭方决定断开连接(挥手第一阶段),而被动关闭方尚未发送完数据。根据协议:

  1. 被动关闭方系统内核自动回复 ACK(挥手第二阶段),而被动关闭方的应用程序可以选择继续把未发送完的数据发送后,才进行关闭连接动作(挥手第三/四阶段)。
  2. 主动关闭方即使决定断开连接(挥手第一阶段)。也可以在 FIN_WAIT2 状态下继续接受被动关闭方未完的数据。

FIN_WAIT2 状态的 TCP 连接仍由 HAProxy 应用持有,并受 timeout client-fin 控制。由于该参数未配置时,使用 timeout client 的配置(配置为 1h),故长期存在。

而如果 HAProxy 线程终止,FIN_WAIT2 状态的 TCP 连接就可以被认为是孤儿连接,由内核参数 /proc/sys/net/ipv4/tcp_fin_timeout 控制,此时默认 60 秒就会很快结束了。

其他疑问:为什么挥手第三阶段的 FIN 包没有重传?

这是一个反直觉的动作,TCP 连接应该是有状态的,是可靠的。但事实上,重传存在成本。参考相关资料后,发现此时受 /proc/sys/net/ipv4/tcp_orphan_retries 参数控制,而默认配置为 0 ,即不重传。