TCP连接状态排查:TIME_WAIT与CLOSE_WAIT的成因与解决 有一回我负责的某个代理集群在凌晨准时亮起告警TIME_WAIT连接数超过5万。群里的第一反应是“开tcp_tw_recycle试试”我让同事先别急因为把内核版本和网络环境翻出来一看这台服务器处在NAT环境里开启这个参数很可能比不开更糟。后来我们花了几个小时把连接状态清洗了一遍发现TIME_WAIT虽然数量大但并没有挤占端口真正让服务出问题的是业务侧悄悄爬升的几百个CLOSE_WAIT。这篇文章就从那次排查开始把TIME_WAIT和CLOSE_WAIT的成因、影响和解决措施完整梳理一遍也适合刚被这类告警轰炸过的读者对照使用。1. 第一步不是调参数而是看清楚连接状态的全貌1.1 一条命令拿到状态分布遇到任何“连接数暴涨”的告警我都不建议直接套用网上搜来的内核参数。先把连接状态分布看清楚判断是哪种状态在涨、涨幅有多快、主要集中在哪个端口/对端IP这个信息比什么调优都重要。现在的Linux发行版里ss已经取代了老旧的netstat读取的是内核socket统计速度快输出也更准确。我常用的命令组合是这样的# 查看TCP连接统计概览 ss -s # 按状态统计数量 ss -ant | awk {print $1} | sort | uniq -c | sort -rn第二条命令会给出类似这样的输出28000 TIME-WAIT 12000 ESTABLISHED 900 CLOSE-WAIT 42 FIN-WAIT-2 10 SYN-SENT 4 LAST-ACK看到这个分布心里要先有个框架ESTABLISHED是正常通信中的连接数量大通常代表并发高TIME_WAIT是主动关闭方收尾时的短暂状态CLOSE-WAIT才是需要警惕的异常爬坡信号。接下来每一类状态往哪个方向查是完全不同的路径。1.2 十种TCP状态里哪些才值得紧张TCP的状态机里一共有十几种状态但日常排障真正需要关心的只有几个。我用一个表把它们按“病不病”分组方便排查时对号入座状态出现位置是否需要紧张说明ESTABLISHED全链路不紧张正常通信数量大说明并发高SYN_SENT / SYN_RECV握手阶段视情况瞬时高正常持续高可能说明连接队列满或握手被丢弃FIN_WAIT_2主动关闭方一般对端迟迟不响应主动方的FIN时会出现堆积可用tcp_fin_timeout兜底TIME_WAIT主动关闭方一般不紧张内核主动收尾60秒后自然消失只有端口耗尽才需要处理CLOSE_WAIT被动关闭方非常需要紧张对端已关闭本端应用却不关闭socket会一直赖着不走LAST_ACK被动关闭方视情况发送FIN后等最后的ACK异常堆积往往与丢包相关我的经验是看到TIME_WAIT多先想“业务模式”看到CLOSE_WAIT持续涨直接想“代码bug”。这也决定了后续排查的方向完全不一样。2. TIME_WAIT为什么会出现先分清“应有的收尾”和“值得优化的场景”2.1 2MSL机制不是白设计的TIME_WAIT经常被运维同学当成“网络病”其实它是TCP协议里一个非常重要的可靠性收尾机制。一个连接进入TIME_WAIT说明本端是这个连接中主动发起关闭的一方在发送完最后一个ACK之后内核需要等待一段时间再彻底释放这个连接。Linux默认情况下这段等待时间是60秒。为什么是60秒因为TCP假设一个报文在网络中的最大存活时间是MSLMaximum Segment LifetimeLinux取默认值30秒TIME_WAIT的持续时间就是2倍的MSL也就是60秒。如果最后一拍ACK在网络上丢了对端会重发FIN本端还能在TIME_WAIT期间再补一个ACK同时TIME_WAIT也保证了旧连接的报文在网络里彻底消失不会串扰到新连接。可以这样理解你从旧项目组离职交接之后还要留一段时间联系方式万一对方那边还有什么遗漏你能再补上。TIME_WAIT就是这个“交接缓冲期”。它本质上是协议为了保证数据不丢、不错而牺牲的一点资源占用不是故障。2.2 哪些业务场景容易出现成片TIME_WAIT明白了TIME_WAIT是主动关闭方的状态那么“服务器上出现大量TIME_WAIT”的场景就很好推断了这台服务器要么是高并发地主动断开连接要么它的对端在大量主动关闭连接。通常出现在这几类环境里代理/网关类服务Nginx、HAProxy这类七层代理转发完请求后经常主动断开与客户端的连接客户端侧成片TIME_WAIT代理侧也可能有。压测工具/脚本类大批量短请求一次压测跑下来压测机上的TIME_WAIT数量往往非常夸张因为每一次HTTP请求都伴随连接建立和主动关闭。事务型API服务且客户端没有开启连接复用每个业务请求都新建连接、用完就断QPS一高服务的TIME_WAIT数量就跟着飙。数据库连接频繁开关后端服务每次访问数据库都新建连接跑完业务又主动关闭数据库和业务服务器都会堆积TIME_WAIT。在有长连接池、连接复用做得好的系统里TIME_WAIT一般是平缓的一旦某个版本把连接池配置改了或者压测工具没开复用TIME_WAIT就会快速升起。所以看到TIME_WAIT上涨先回看业务行为别急着动内核参数。2.3 TIME_WAIT堆积后的真正风险TIME_WAIT数量大本身不太可怕因为每个TIME_WAIT连接占用的内存非常小和ESTABLISHED连接不同它不承载发送缓冲区、接收缓冲区基本就是内核里一个很小的结构体。真正需要担心的是四元组端口资源被暂扣。TCP连接用“本地IP 本地端口 对端IP 对端端口”唯一标识。当大量TIME_WAIT集中在同一个“本地IP 对端IP:对端端口”维度上时本地端口会被不断耗用四元组暂时无法复用最终可能出现本地端口耗尽新连接无法建立。端口耗尽时业务方最常见的报错是connect() failed: Cannot assign requested address。这时候再去看TIME_WAIT数量会发现它已经不是“会不会有问题”的问题而是已经堵死了。所以判断TIME_WAIT是否需要处理标准只有一条是否影响到新连接的建立。如果连接一直正常TIME_WAIT再大也只是一个“性价比很低的优化点”不必半夜爬起来调内核。3. CLOSE_WAIT爬坡代码层才是主战场3.1 一次真实排查的完整链路我印象很深的一次CLOSE_WAIT排障是某后端服务的CLOSE_WAIT在一个小时内从300涨到接近2万。CPU没有飙高内存平稳服务看起来还能响应但连接数增长趋势让人不踏实。第一步是先缩小范围。CLOSE_WAIT状态下套接字是“对端已经关闭本端还没关闭”的状态所以用下面两条命令看它的来源# 看CLOSE_WAIT连接的对端地址分布 ss -ant state close-wait # 看这些连接属于哪个进程 ss -antp state close-wait对端地址分布一出来就发现了规律大量CLOSE_WAIT集中在某个自建对象存储服务的IP段。也就是说这些连接是服务调用对象存储客户端之后遗留下来的。第二步看进程资源。用ss -antp state close-wait定位到PID后我顺手查了进程的fd文件描述符使用情况ls /proc/PID/fd | wc -l ulimit -nfd总数已经逼近ulimit -n的上限如果在监控系统里晚一点介入accept()多半会开始失败。这也解释了为什么服务没有CPU飙高、却已经出现部分请求异常。第三步是看代码行为。最终定位到问题是项目里某个对象存储客户端在读取响应后没有在finally块里关闭连接。部分调用路径读到了数据就正常返回另一部分路径读到一半遇到异常直接抛出连接就悬空在那里没人回收。再加上任务队列不断堆积新的调用又不断创建连接CLOSE_WAIT就越积越多。修复很朴素在finally里强制关闭连接同时给读操作设置了超时时间发布后CLOSE_WAIT在几分钟内就回落了。3.2 CLOSE_WAIT堆积会怎样拖垮整个服务CLOSE_WAIT不像TIME_WAIT有60秒寿命它没有内核超时兜底。应用层不调用close()这个连接就永远是CLOSE_WAITfd会一直被占用。拖垮服务的路径非常清晰应用层没关闭socketCLOSE_WAIT连接持续累积。每个连接占用一个fd。fd数接近ulimit -n上限后新的accept()或open()开始失败。健康检查接口因为无法创建新连接响应异常上游开始把节点摘掉。流量被转移到剩余节点剩余节点也被同样的方式拖垮最终整个集群不可用。这个过程里服务进程的CPU、内存可能看上去完全正常但服务就是“假死”了。这也是CLOSE_WAIT比TIME_WAIT更阴险的地方它不会主动消失只会在背后慢慢蚕食文件描述符直到系统被一次性打穿。3.3 为什么它比TIME_WAIT更值得第一时间处理TIME_WAIT有内核定时器在收尾你不处理它60秒之后它也会自己消失CLOSE_WAIT没有这样的机制应用层不调用close()它就永远待在那里。TIME_WAIT的根因通常来自“连接建立太频繁”这类业务模式问题CLOSE_WAIT的根因则几乎都是代码质量问题——资源释放路径存在漏洞、异常分支没处理、超时设置缺失。所以我的处理优先级是CLOSE_WAIT一涨就先拉代码会议TIME_WAIT再涨也最多先做个观察性记录。CLOSE_WAIT涨起来之后留给排查的时间窗口非常短因为fd耗尽后你连调试工具都打不开了。排查步骤常用命令/操作能得到什么结论观察状态分布ss -ant | awk {print $1} | uniq -c | sort -rn确认是哪类状态在堆积定位对端ss -ant state close-wait确认CLOSE_WAIT集中连向什么服务定位进程ss -antp state close-wait确认是哪个进程持有的连接检查fdls /proc/PID/fd | wc -l判断是否接近ulimit -n瓶颈确认代码路径代码审计 strace -p PID -e traceclose确认socket是否被关闭、在哪个调用链上泄漏4. 解决措施内核参数、应用代码和架构选择按优先级排4.1 内核参数哪些有效、哪些慎用、哪些别抄网上关于TIME_WAIT的内核调优文章非常多但不少是老版本内核思路照抄很容易踩坑。我在实际生产中验证下来按下面这张表处理比较稳妥内核参数作用适用边界建议net.ipv4.tcp_tw_reuse 1允许主动发起连接时复用TIME_WAIT状态的连接对出方向连接有效即本机作为客户端发起连接需配合tcp_timestamps1可用适合NAT环境/客户端场景net.ipv4.tcp_timestamps 1时间戳选项tw_reuse生效的前提需要两端都开启默认开启不要动net.ipv4.tcp_fin_timeout控制FIN_WAIT_2状态的超时时间只对FIN_WAIT_2起作用不直接缩短TIME_WAIT不要指望它缩短TIME_WAITnet.ipv4.tcp_max_tw_bucketsTIME_WAIT数量的兜底上限超过后内核会丢弃部分TIME_WAIT连接兜底保护参数保持默认即可调小容易误伤net.ipv4.ip_local_port_range本地发起连接时可用的端口范围扩大范围可以缓解端口耗尽可适当放宽但治标不治本net.ipv4.tcp_tw_recycle快速回收TIME_WAIT老版本内核参数存在NAT环境丢连接问题新内核已移除不要开特别提醒一下tcp_tw_recycle这套参数在早期内核里确实能加快TIME_WAIT回收但它依赖时间戳机制来判断报文新旧导致同一个NAT网关后面多个设备发出的连接因为时间戳不是严格递增被内核误判为旧报文而丢弃。很多公司开完之后出现“手机Wi-Fi下部分页面刷不出来”这种诡异问题最后都追溯到它。Linux 4.12以后的内核已经删掉了这个参数旧文章里的配置在新系统上也不会生效。4.2 应用侧最有效的三件事调内核参数只是给问题腾出空间真正治本的一定在应用层。我在项目上反复验证下来下面三件事是性价比最高的第一连接复用。数据库客户端、Redis客户端、HTTP客户端、对象存储客户端全部开启连接池尽量复用连接避免每次请求新建连接。一个高QPS的服务如果能把连接复用好TIME_WAIT数量能直接下降一个数量级。第二超时控制。调用外部依赖时设置connectTimeout、socketTimeout、readTimeout。CLOSE_WAIT的典型成因是“对端关了连接本端应用还卡在read上不知道要主动关闭”读超时能兜底至少保证线程不会被悬空连接拖死。第三资源关闭。凡是使用socket、Response对象、数据库连接的地方必须在finally或语言自带的资源管理语法里关闭。排查CLOSE_WAIT问题我最后十次有八次都在代码里找到同样的漏洞异常路径上没有关闭资源。4.3 不同角色的服务器处理方法不一样同样的状态数量出现在不同角色的机器上处理动作完全不同。我给三个最常见角色列了一张对照表服务器角色容易出现的状态重点动作Nginx/HAProxy等代理客户端侧TIME_WAIT多、与后端之间也可能TIME_WAIT多开启upstream keepalive合理配置keepalive_timeout减少与后端的重复建连后端API服务CLOSE_WAIT多偶发TIME_WAIT连接池 线程池 超时设置 资源释放四个一起查压测机/大批量脚本TIME_WAIT成片出现压测工具开连接复用或降低并发、加长单连接存活时间以Nginx为例如果日志里大量请求来自同一客户端且每个连接都只发一次请求客户端的TIME_WAIT就会非常明显。此时最有效的办法不是去Nginx里调什么参数而是引导客户端使用长连接如果客户端不可控再考虑在Nginx侧调整keepalive_timeout但它能减少的是Nginx到客户端之间的连接频率对TIME_WAIT的减少效果有限。4.4 优化之后如何验证效果每次调完参数、改完代码我都会按下面这套流程验证一轮而不是改完就收工先记录优化前的基线数据包括ss -s的状态快照、端口占用、fd使用率。然后优化完成后观察至少30分钟重点看状态分布是否回落而不是只看总数。再做一轮压测模拟高峰期流量观察TIME_WAIT、CLOSE_WAIT是否在测试结束后缓慢爬升还是维持在一个稳定水位。压测时特别要注意连接失败率。如果优化后TIME_WAIT数量下来了但请求成功率没有回升说明优化点没打中问题。反过来如果是CLOSE_WAIT问题验证标准就是fd使用率是否明显下降CLOSE_WAIT数量是否不再单调上涨。另一个我后来养成的习惯是把ss -s或者按状态分类的连接数做成监控项推送到监控系统按时间序列展示。绝对值阈值告警很容易误报或漏报趋势变化往往比一个“5万”的数字更有价值。最后分享一个排查顺序上的个人体会遇到这类问题先看CLOSE_WAIT再看TIME_WAIT。TIME_WAIT有存活期它不会赖着不走CLOSE_WAIT只要代码没释放就会一直挂在你眼前等到fd耗尽时再处理就晚了。这个顺序帮我少走了很多弯路。