Linux服务器CLOSE_WAIT持续增长:进程、文件描述符与关闭路径排查清单
先用 ss -tanp state close-wait 按 PID、本地端口、远端地址和持续时间归类,同时记录进程文件描述符数量、线程、请求错误率和版本。抓取少量代表连接,确认对端 FIN 已到且本机已 ACK。然后定位应用为何没有调用 close():异常分支漏清理、响应体未消费、连接池归还失败、线程卡住、协程取消未执行 finally,或子进程继承了描述符。修复后以连接数回落、FD 稳定、负载测试无持续增长为验收。不要把 tcp_fin_timeout、TIME_WAIT 复用或重启当根治。
本文目录(16 节)
直接答案
先用 ss -tanp state close-wait 按 PID、本地端口、远端地址和持续时间归类,同时记录进程文件描述符数量、线程、请求错误率和版本。抓取少量代表连接,确认对端 FIN 已到且本机已 ACK。然后定位应用为何没有调用 close():异常分支漏清理、响应体未消费、连接池归还失败、线程卡住、协程取消未执行 finally,或子进程继承了描述符。修复后以连接数回落、FD 稳定、负载测试无持续增长为验收。不要把 tcp_fin_timeout、TIME_WAIT 复用或重启当根治。
一、理解CLOSE_WAIT到底在等谁
TCP 的两个方向独立关闭。对端发送 FIN 后,本地确认该方向结束,Socket 进入 CLOSE-WAIT;本地应用仍可以发送剩余数据。RFC 9293 说明,在 CLOSE-WAIT 状态收到应用 CLOSE 后,应在先前发送完成后发送 FIN,并进入 LAST-ACK。
因此 CLOSE-WAIT 主要是在等本地应用关闭,不是在等内核超时自动删除。它与 TIME-WAIT 不同:TIME-WAIT 通常出现在主动关闭的一侧,由 TCP 协议计时。针对 TIME-WAIT 的 sysctl 不会修复应用持有的 CLOSE-WAIT。
二、先建立数量与增长速率基线
执行以下只读命令,保留时间戳:
ss -s
ss -tanp state close-wait
ss -tanp state established
每隔固定周期采样总数,并按本地端口、远端 IP、PID/进程名聚合。单个短请求结束后短暂出现并快速消失通常正常;稳定增长且不回落才是泄漏信号。不要把一次瞬时快照直接定性为事故。
容器环境要在正确 Network Namespace 中运行 ss,否则可能看不到目标 Socket。权限不足时 -p 不能显示进程,需使用受控的管理员权限,而不是依据端口猜 PID。
三、把Socket映射到PID和文件描述符
ss -p 可以显示使用 Socket 的进程。对目标 PID 统计 /proc/PID/fd 数量,再查 Socket inode 与文件描述符的对应关系。记录进程启动时间和实例 ID,避免 PID 重用导致误判。
ls -1 /proc/$PID/fd | wc -l
readlink /proc/$PID/fd/123
cat /proc/$PID/limits | grep -i 'open files'
不要在高负载生产机上无边界扫描所有 /proc/*/fd。先用端口和 PID 缩小范围。若同一 Socket 被多个进程或线程持有,检查 fork、进程池、热重载和 FD 继承;只有所有引用都关闭,底层描述才会释放。
四、用四元组定位哪类业务请求
Socket 的本地 IP/端口与远端 IP/端口能区分入口 HTTP、数据库、Redis、消息队列或第三方 API。结合负载均衡日志、应用请求 ID 和连接池指标,判断是服务端接受连接后未关闭,还是客户端访问上游后未清理。
若 CLOSE_WAIT 集中在一个远端地址,查看对端是否主动缩短 Keep-Alive、重启或超时关闭;但对端发 FIN 本身是合法行为,本地应用仍有责任处理 EOF 和关闭。不要把问题简单归咎于“对端断开太早”。
五、抓包确认FIN与应用行为
对一个代表四元组做短时、限量抓包。确认对端 FIN 到达、本机 ACK,以及本机之后是否发送 FIN。如果只有对端 FIN 与本机 ACK,长时间没有本机 FIN,与 CLOSE_WAIT 一致;若出现 RST 或重传,则继续检查不同故障。
抓包应限制接口、主机、端口、大小和时长,并遵守数据保护要求。TLS 载荷无需解密即可判断 TCP 标志。将包时间与应用日志、线程栈和 Socket 采样对齐,能定位收到 EOF 后执行路径停在哪里。
六、最常见原因是异常分支漏close
典型代码只在成功路径关闭响应或连接,遇到超时、解析异常、提前 return 或取消时跳过清理。应使用语言提供的上下文管理、defer、try/finally 或自动资源管理结构,让所有退出路径都执行 close。
HTTP 客户端还可能要求消费或显式关闭响应体,连接才能归还池。数据库 ResultSet、Statement 与 Connection 也可能层层持有。代码审查要覆盖重试、熔断、取消和异常嵌套,不能只看正常示例。
七、事件循环和线程阻塞也会延迟关闭
应用可能已经计划 close,但负责事件循环的线程被 CPU 密集任务、同步 I/O、死锁或无限队列阻塞。采集线程栈、事件循环延迟、协程数量和任务队列,查看关闭回调是否得不到调度。
多线程程序中,Linux close(2) 文档提醒:一个线程关闭描述符时,另一线程可能仍阻塞在使用同一底层 open file description 的系统调用中,该引用可持续到调用完成。不要并发、无协调地随机 close FD;应修复所有权和生命周期设计,避免 FD 重用竞态。
八、连接池泄漏与错误的健康策略
连接池把 Socket 生命周期隐藏在 acquire/release 后面。调用方忘记归还、借用超时未设置、连接验证失败后未销毁,都会逐步耗尽池和 FD。检查 active、idle、pending、acquire timeout、max lifetime 和 eviction 指标。
连接收到 EOF 后应标记不可复用并关闭,不能重新放回空闲池。健康检查只证明某一时刻可用,不替代归还路径。为借用记录采样调用栈或超时告警,比单纯扩大池容量更能找到根因;扩容只会延后耗尽。
九、FD上限只能缓解不能修复
提高 RLIMIT_NOFILE 可以给排障争取时间,但泄漏仍会继续,最终消耗更多内存与内核资源。先确认当前软硬限制、systemd Unit 的 LimitNOFILE、容器限制与应用实际读取值,避免只改 shell 的 ulimit 却不影响服务。
临时提高上限必须配合连接数与增长速率告警、明确到期时间和修复计划。不要把全系统限制无限调大,也不要用定时重启掩盖泄漏。重启只能释放现有 FD,会清掉最关键的现场证据。
十、为什么tcp_fin_timeout没用
tcp_fin_timeout 主要涉及孤立 FIN-WAIT-2 等状态,并不代表内核会替应用关闭 CLOSE_WAIT Socket。调低它、启用 TIME_WAIT 复用或调整 Keepalive 都不会让用户态自动调用 close。
TCP Keepalive 用于探测长时间空闲但看似建立的连接;CLOSE_WAIT 已经收到对端关闭信号,不需要再探测对端是否存活。系统参数只能在明确理解目标状态和副作用后调整,不能把所有“连接多”问题都套用同一模板。
十一、应急处置顺序
当 FD 接近上限时,先保存 ss 聚合、目标 PID 的 FD 数、线程栈、版本和少量抓包。然后隔离问题实例或从负载均衡摘除,优雅停止接收新请求,给正常连接排空窗口。若无法恢复,再逐实例重启,避免整个集群同时中断。
不要用脚本直接关闭任意 /proc/PID/fd/N,这可能因 FD 重用关闭错误资源,破坏应用状态。需要强制终止时以进程或实例为控制单元,并确保健康实例承接流量。记录处置对连接和错误率的影响。
十二、修复后的回归测试
构造正常完成、对端提前 FIN、读取超时、解析异常、客户端取消、上游重启和连接池满等场景。每轮请求结束后观察 CLOSE_WAIT 和进程 FD 是否回到稳定基线。持续负载应验证增长率接近零,而不是只确认功能响应 200。
在代码层加入资源生命周期测试与静态检查;在运行层监控按 PID/端口聚合的 CLOSE_WAIT、FD 使用率、连接池 pending 和事件循环延迟。发布采用 Canary,若新版本曲线异常只回滚对应实例,并保留采样证据。
FAQ
CLOSE_WAIT多一定是网络问题吗?
通常不是。它表示对端关闭信号已经到达,本地应用还未完成关闭。网络问题可触发断开,但持续积累的直接责任通常在本地资源生命周期。
重启服务能解决吗?
重启会释放进程持有的 Socket,可作为容量告急时的临时恢复,但不会修复代码或连接池泄漏。重启前应保存 PID、FD、线程栈和四元组证据。
调低tcp_fin_timeout有用吗?
对 CLOSE_WAIT 没有根治作用。该状态在等应用 close,不是等待普通 FIN 超时。应定位持有文件描述符的进程和退出路径。
如何判断是哪个接口泄漏?
按本地端口、远端地址和 PID 聚合 ss 输出,再结合应用请求、上游服务和连接池指标。必要时对少量代表连接抓包并获取线程栈。
少量CLOSE_WAIT是否需要告警?
短暂少量可以是正常过渡。告警应关注持续时间、增长速率、占 FD 比例和是否超过业务基线,而不是固定看到一条就报警。
总结
CLOSE_WAIT 持续增长的核心是:对端已经关闭,本地应用仍持有 Socket。先用 ss 将连接映射到 PID、四元组和文件描述符,再用限量抓包与线程栈定位异常路径、连接池或事件循环阻塞。修复必须保证所有成功、失败和取消路径都关闭资源;系统参数、扩容与重启只能应急,不能替代应用生命周期修复。
参考资料
- RFC Editor — RFC 9293, Transmission Control Protocol: https:
/ / www. rfc- editor. org/ info/ rfc9293/ - Linux man-pages — ss(8): https:
/ / man7. org/ linux/ man- pages/ man8/ ss. 8. html - Linux man-pages — socket(7): https:
/ / man7. org/ linux/ man- pages/ man7/ socket. 7. html - Linux man-pages — close(2): https:
/ / man7. org/ linux/ man- pages/ man2/ close. 2. html