L4 层网络上总是能遇到奇奇怪怪的错误,记录一下备查。
故障现象:大量异常连接状态
服务器大量存在 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 :
- 未发送 FIN:被动关闭侧进线程阻塞,已经无法自主(被动)关闭连接。(被动关闭侧的 ACK 是由内核自动响应主动关闭侧的 FIN ,用户态/应用侧不会感知)
- 未接收 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 为什么长时间存在?
考虑四次挥手时的特殊场景,如果主动关闭方决定断开连接(挥手第一阶段),而被动关闭方尚未发送完数据。根据协议:
- 被动关闭方系统内核自动回复 ACK(挥手第二阶段),而被动关闭方的应用程序可以选择继续把未发送完的数据发送后,才进行关闭连接动作(挥手第三/四阶段)。
- 主动关闭方即使决定断开连接(挥手第一阶段)。也可以在 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 ,即不重传。