网络服务跑着跑着突然变慢接口响应时间从几十毫秒飙到几秒登录不上、下单超时、页面转圈运维群里一片哀嚎。登录服务器敲下netstat -an | grep -i wait一看满屏的TIME_WAIT和CLOSE_WAIT密密麻麻几百上千条这时候很多人第一反应是“服务器是不是被攻击了”或者“内核参数没调好”然后开始疯狂搜“怎么减少 TIME_WAIT”照着网上抄一堆sysctl参数往机器上一贴结果问题该在还在。我处理过不少这类现场说实话TIME_WAIT和CLOSE_WAIT这两个状态本身是 TCP 协议正常工作的组成部分它们大量堆积只是“症状”真正要挖的是背后的连接管理逻辑出了什么问题。这篇内容就把这两个状态从协议原理、成因分类、排查路径到修复方案完整拆一遍适合后端开发、运维、SRE 以及任何需要跟网络连接打交道的同学不管你是刚接触netstat的新手还是已经调过几轮内核参数的老手都能从中找到可以直接复现的排查思路和避坑经验。1. 先把两个状态放回 TCP 生命周期里看1.1 TIME_WAIT 到底是谁在等等什么很多人把TIME_WAIT当成“连接没关干净”这个理解是偏的。TIME_WAIT是主动关闭连接的一方在发送最后一个ACK之后进入的状态标准停留时长是 2 倍的MSLMaximum Segment Lifetime报文最大生存时间Linux 上默认MSL是 30 秒所以TIME_WAIT默认持续 60 秒。它存在的意义有两个缺一不可。第一个作用是保证最后一个 ACK 能可靠到达对端。假设主动关闭方发完ACK就直接把连接信息抹掉而这个ACK在路上丢了被动关闭方会一直重传它的FIN此时主动关闭方已经没有任何上下文收到FIN只会回一个RST对端就会认为连接异常终止。留着TIME_WAIT状态就能在收到重传FIN时再补一个ACK过去。第二个作用是让这条连接四元组源 IP、源端口、目的 IP、目的端口在网络上残留的旧报文自然消亡防止它们串到之后复用同一四元组的新连接里造成数据错乱。你可以把它理解成“电话号码注销后的冷静期”号码刚释放就立刻分配给新用户前一个用户的朋友还可能打过来冷静期就是让这些迟到的呼叫自然失效。所以TIME_WAIT本身不是 bug它是协议层面的保护机制。问题只出在数量上当短连接高频产生、主动关闭方又集中在少数几台机器上时TIME_WAIT会在 60 秒窗口内快速累积把本地端口耗尽或者把连接表撑大。1.2 CLOSE_WAIT 是另一回事它指向的是代码CLOSE_WAIT的性质完全不同。它出现在被动关闭方对端发来了FIN本端内核回了ACK连接进入CLOSE_WAIT此时内核已经把“对端不会再发数据”这件事告诉应用层了接下来等应用层调用close()。只要应用层不关这个状态就一直挂着没有超时没有自动回收。这就是关键区别TIME_WAIT是内核按计时器自动走的你控制不了单个连接的停留时长只能调参数而CLOSE_WAIT是卡在应用层的内核在等你的代码。所以看到大量CLOSE_WAIT基本可以直接判定应用代码里有连接没被正确关闭常见于连接池泄漏、异常分支没走finally、流没关、Socket 包装类没释放等场景。我见过一个很典型的案例某服务用 HTTP 客户端调下游正常路径下连接会归还连接池但某个超时异常分支里直接throw出去了连接对象既没关闭也没归还每触发一次异常就泄漏一条。跑了一天CLOSE_WAIT堆到四千多条最终文件描述符耗尽服务假死。这种问题调内核参数一点用都没有必须回到代码里找。1.3 一张表把两者彻底区分开维度TIME_WAITCLOSE_WAIT出现方主动关闭方被动关闭方触发时机本端发完最后一个 ACK 后收到对端 FIN 并回 ACK 后由谁推进内核计时器自动推进等应用层调用 close()默认停留2MSL约 60 秒无上限直到应用关闭大量堆积的含义短连接多、主动关闭集中应用层连接泄漏或关闭不及时调内核参数是否有效部分有效缓解端口压力基本无效必须改代码风险本地端口耗尽、内存占用文件描述符耗尽、内存泄漏、服务假死把这张表记住排查时第一步就能判断方向对不对。看到CLOSE_WAIT多还去调tcp_tw_reuse属于南辕北辙。2. 排查之前先把观测手段备齐2.1 用对命令别只会一条 netstatnetstat是最常用的但它在连接数很大时性能很差而且部分新系统默认没装。我现在的习惯是分场景选工具# 统计各状态连接数最直观 ss -ant | awk {print $1} | sort | uniq -c | sort -rn # 只看 TIME_WAIT 和 CLOSE_WAIT 的明细带进程信息需要 -p ss -antp state time-wait ss -antp state close-wait # 按本地端口聚合看哪个服务端口在堆积 ss -ant state time-wait | awk {print $4} | awk -F: {print $NF} | sort | uniq -c | sort -rn | head # 按对端 IP 聚合看是不是集中在某几个下游 ss -ant state close-wait | awk {print $5} | awk -F: {print $1} | sort | uniq -c | sort -rn | headss比netstat快一个数量级因为它直接读内核的 netlink 接口而netstat是遍历/proc。连接上万的时候这个差距非常明显。另外ss的state过滤语法比grep靠谱grep TIME-WAIT有时候会把TIME_WAIT和别的字段误匹配。还有一个容易被忽略的点看全连接队列和半连接队列。ss -lnt输出里的Send-Q是 listen backlog 上限Recv-Q是当前已完成三次握手但还没被应用accept的连接数。如果Recv-Q持续接近Send-Q说明应用 accept 太慢这跟CLOSE_WAIT往往是同一批问题的两面。2.2 进程级定位到底是谁的连接光看状态数量不够得定位到进程。ss -antp能带出进程名和 PID但需要 root 权限。如果机器上跑着几十个服务直接按 PID 聚合ss -antp state close-wait | awk {print $NF} | sort | uniq -c | sort -rn | head -20输出里会形如users:((java,pid12345,fd678))一眼就能看出是哪个进程。拿到 PID 之后进一步看它打开了多少 fdls /proc/12345/fd | wc -l cat /proc/12345/limits | grep -i open files如果 fd 数量已经逼近limits里的软限制那CLOSE_WAIT堆积和 fd 耗尽就是同一条因果链服务随时可能因为Too many open files崩掉。这时候优先级要立刻拉高先止血再根治。2.3 抓包确认关闭时序别靠猜有些场景光看状态判断不了是谁先关的。比如你怀疑是下游服务主动断连导致本端堆积CLOSE_WAIT那就抓包看FIN的方向tcpdump -i eth0 -nn tcp port 8080 and (tcp[tcpflags] (tcp-fin|tcp-rst) ! 0) -c 50看FIN是从哪边发出来的。如果FIN来自对端本端回了ACK之后就停在CLOSE_WAIT那责任在本端应用没关如果本端发FIN后进入TIME_WAIT那说明本端是主动关闭方堆积原因就是短连接太频繁。抓包这一步很多人嫌麻烦跳过但它能省掉大量“猜来猜去”的时间尤其是跨团队扯皮的时候一个包序图比十句解释都管用。提示抓包过滤条件尽量收窄到具体端口和标志位全量抓包在高流量机器上会迅速吃满磁盘也可能影响业务。3. TIME_WAIT 堆积的成因分类与针对性处理3.1 短连接高频调用是头号原因TIME_WAIT堆积最常见的场景就是大量短连接。典型模式是服务 A 每次请求下游都新建一条 TCP 连接用完就关。假设 QPS 是 2000每条连接都由 A 主动关闭那 60 秒的TIME_WAIT窗口内就会累积约 12 万条连接本地可用端口默认ip_local_port_range大约 2.8 万个瞬间被打穿新连接直接报Cannot assign requested address。判断方法很直接看TIME_WAIT是否集中在本端作为客户端去连下游的端口上。用前面按对端 IP 聚合的命令如果发现TIME_WAIT大量指向同一个下游地址基本可以确认是短连接调用。处理方向按优先级排改成长连接 连接池。这是根治手段。HTTP 用 keep-alive数据库用连接池RPC 框架一般自带连接复用。连接池的核心参数是最大连接数、空闲回收时间、最大存活时间配的时候要跟下游的承载能力对齐不能一味调大。如果确实无法长连接再考虑内核参数缓解比如开启tcp_tw_reuse允许复用TIME_WAIT状态的连接发起新连接仅对客户端主动连接有效并适当扩大ip_local_port_range。让对端主动关闭。如果业务上能协商让服务端先关连接TIME_WAIT就落在服务端而不是客户端客户端端口压力就小了。但这只是转移压力不是消除服务端如果也短连接一样会堆。3.2 内核参数怎么调调之前先算清楚网上流传的参数模板很多但直接抄是有风险的。我按实际作用拆开讲每个参数为什么调、调到多少都要有依据。net.ipv4.tcp_tw_reuse允许将TIME_WAIT状态的 socket 复用于新的出站连接。它依赖 TCP 时间戳tcp_timestamps来判断旧报文是否已过期所以必须同时开启时间戳。这个参数对客户端角色有效对服务端监听端口无效。开启后能显著缓解客户端端口耗尽。net.ipv4.tcp_max_tw_bucketsTIME_WAIT状态 socket 的总数上限超过后内核会直接销毁最早的。这个值不宜设太小设太小会导致TIME_WAIT被强行回收破坏协议保护语义可能引发旧报文串扰。一般按内存和连接量估算默认值通常够用除非你明确知道自己在做什么否则不建议动。net.ipv4.ip_local_port_range本地临时端口范围。默认常见是32768 60999约 2.8 万个。如果客户端短连接压力大可以扩到1024 65535但要注意低端口段可能和某些服务冲突扩之前确认没有服务监听在这些端口上。net.ipv4.tcp_fin_timeout这个参数经常被误解。它控制的是孤儿连接应用已关闭但内核还在FIN_WAIT_2等待对端FIN的超时不影响TIME_WAIT的 2MSL 时长。很多人以为调小它就能减少TIME_WAIT这是错的。TIME_WAIT的时长由TCP_TIMEWAIT_LEN编译期常量决定用户态改不了。算一笔账假设你的服务作为客户端峰值 QPS 为 Q每条连接都是短连接且由本端主动关闭那么稳定状态下TIME_WAIT数量约为Q × 60。Q1000 时就是 6 万条已经超过默认端口范围。这个公式能帮你快速判断“参数调优”和“改长连接”哪个更紧迫。如果Q × 60远大于端口范围光调参数是撑不住的必须上连接复用。3.3 服务端视角的 TIME_WAIT 要不要管有一种情况是服务端自己产生大量TIME_WAIT服务端处理完请求后主动关闭连接。这在某些 HTTP 服务器配置比如关闭 keep-alive、或者keepalive_timeout设得很短下很常见。服务端TIME_WAIT堆积的风险主要是内存和连接表压力一般不会像客户端那样端口耗尽因为服务端用的是固定监听端口四元组的区分靠客户端 IP 和端口。但如果服务端TIME_WAIT数量到了几十万级别内存占用和哈希表查找性能都会受影响。这时候优先检查 keep-alive 配置是否合理把连接复用时间调长减少关闭频率。其次才是考虑tcp_max_tw_buckets兜底。我的经验是服务端TIME_WAIT超过 5 万就该看一眼配置了超过 20 万基本可以确定是连接复用没做好。4. CLOSE_WAIT 堆积九成是代码问题得这么查4.1 从状态分布反推泄漏点CLOSE_WAIT排查的第一步不是看代码而是看分布特征。同样是几千条CLOSE_WAIT分布不同根因完全不同。如果CLOSE_WAIT集中在连接某个特定下游的端口上说明是这个下游方向的连接管理有问题重点查调用这个下游的客户端代码。如果CLOSE_WAIT分散在多个下游那更可能是通用的连接池或框架层问题。如果CLOSE_WAIT的对端 IP 是同一个且端口在变化说明对端在频繁主动断连本端没跟上关闭。用这条命令把CLOSE_WAIT按对端聚合ss -ant state close-wait | awk NR1 {print $5} | awk -F: {print $1} | sort | uniq -c | sort -rn | head拿到集中的对端之后再结合进程信息确认是本端哪个服务。这一步做完排查范围能从“整个系统”缩小到“某个服务的某个调用链路”效率差好几倍。4.2 代码层面的四类典型泄漏定位到服务之后按这四类去翻代码命中率很高。第一类异常分支漏关连接。这是最常见的。正常路径有close()但抛异常时跳过了。Java 里典型的是try块里创建连接catch里只打日志没关Go 里是defer conn.Close()写在了错误的位置或者err ! nil时提前return导致defer没注册。检查方法搜所有创建连接的地方确认每一条退出路径都有对应的关闭动作。第二类连接池配置错误或归还逻辑缺失。用了连接池不代表不会泄漏。如果从池里借出连接后在某个分支没有归还比如return前忘了release池子里的连接就被“借走不还”表现出来就是连接数只增不减。有些池实现有泄漏检测比如借出超过一定时间打印堆栈把这个开关打开能直接定位到借出点。第三类流或包装类没关。Socket 的InputStream/OutputStream、BufferedReader、GZIPInputStream这类包装流如果只关了外层没关内层或者反过来都可能留下未释放的 fd。原则是谁创建谁关闭包装流关闭时会级联关闭底层流但前提是你确实调了最外层的close()。第四类异步回调里连接生命周期管理混乱。异步编程里连接对象在两个线程间传递容易出现“以为对方会关结果谁都没关”。这类问题最难查因为状态不直观。建议在连接对象上加引用计数或者用明确的 ownership 约定谁最后持有谁负责关。4.3 用工具把泄漏点钉死光看代码容易漏配合工具更稳。几个实用手段jstack / pprof 抓堆栈Java 服务jstack看线程栈里有没有大量线程卡在 socket 读写Go 服务用pprof看 goroutine 栈泄漏的连接往往对应着没退出的 goroutine。lsof 看 fd 类型lsof -p PID | grep -i sock能看到这个进程打开的 socket 数量和状态配合ss交叉验证。连接池自带监控主流连接池都暴露 active/idle/total 指标如果 active 持续上涨不回落基本就是泄漏。在测试环境复现把下游响应人为调慢或注入异常观察CLOSE_WAIT是否增长能快速验证某个异常分支是否是泄漏源。我个人的习惯是只要看到CLOSE_WAIT超过 500 条且持续增长就当 P1 处理先重启止血同时保留现场ss输出、堆栈、fd 列表用于事后分析。重启能清掉堆积但根因不解决几小时后必然复发。4.4 一个完整的排查记录示例说个模拟场景方便你把上面的方法串起来。某服务告警“响应变慢”登录机器$ ss -ant | awk {print $1} | sort | uniq -c | sort -rn 8213 ESTAB 4102 CLOSE-WAIT 312 TIME-WAITCLOSE_WAIT四千多TIME_WAIT才三百方向立刻明确不是短连接问题是连接泄漏。接着按对端聚合$ ss -ant state close-wait | awk NR1 {print $5} | awk -F: {print $1} | sort | uniq -c | sort -rn | head -3 3980 10.0.3.15 110 10.0.3.22 12 10.0.3.30集中在10.0.3.15这个下游。查进程$ ss -antp state close-wait | grep 10.0.3.15 | head -1 ESTAB ... users:((order-service,pid8842,fd10231))确认是order-service。看 fd 数量$ ls /proc/8842/fd | wc -l 10240 $ cat /proc/8842/limits | grep open files Max open files 10240 10240 filesfd 已经顶到软限制服务随时会崩。抓包看FIN方向确认是10.0.3.15主动断连本端回了ACK后没关。翻order-service调10.0.3.15的客户端代码发现超时异常分支里catch块只记了日志连接对象没关也没归还池。修复方式是在finally里统一归还并给连接池开启泄漏检测。上线后CLOSE_WAIT回落到个位数fd 稳定在两千左右。这个流程的核心就是状态统计定方向 → 对端聚合缩范围 → 进程定位找代码 → 抓包确认时序 → 修复后看指标回落。五步走完问题基本没有藏身之处。5. 常见问题速查与避坑清单5.1 高频疑问速查表现象可能原因优先动作TIME_WAIT 数万且集中在客户端端口短连接高频调用改连接池/长连接再考虑 tcp_tw_reuseTIME_WAIT 集中在服务端监听端口keep-alive 关闭或超时过短调长 keepalive_timeout检查连接复用CLOSE_WAIT 持续增长不回落应用层连接泄漏按对端聚合定位服务查异常分支和池归还CLOSE_WAIT 伴随 fd 逼近上限泄漏已到临界先重启止血保留现场再分析调了 tcp_fin_timeout 但 TIME_WAIT 没降参数作用对象搞错该参数不影响 TIME_WAIT停止无效调参开启 tcp_tw_reuse 后仍有大量 TIME_WAIT未开时间戳或场景是服务端确认 tcp_timestamps1服务端场景另找原因连接数正常但服务变慢可能是队列积压而非状态堆积看 ss -lnt 的 Recv-Q/Send-Q5.2 几个容易踩的坑坑一把 TIME_WAIT 当故障本身。几千条TIME_WAIT在正常短连接业务里很常见只要没到端口耗尽、没影响新建连接就不必紧张。真正要盯的是“新建连接失败率”和“端口可用数”而不是绝对数量。坑二盲目开 tcp_tw_recycle。这个参数在较新的内核里已经被移除老内核上开启它在 NAT 环境下会导致连接异常因为依赖时间戳且对同一源 IP 的不同客户端判断有缺陷。现在没有任何理由去开它看到教程里推荐这个参数的直接跳过。坑三CLOSE_WAIT 靠调参数解决。前面反复说了CLOSE_WAIT是应用层没关内核参数管不着。花时间调tcp_keepalive系列参数只能让内核更早探测到死连接治标不治本代码不改迟早复发。坑四只盯一台机器。短连接问题往往是调用方和被调方共同造成的。客户端TIME_WAIT多可能是服务端 keep-alive 没开服务端CLOSE_WAIT多可能是客户端断连太随意。排查时两端都要看别在自己这头死磕。坑五重启当解决方案。重启能清状态但不清根因。每次重启前一定把ss全量输出、进程 fd 列表、线程堆栈留存下来否则重启后现场没了下次复发还得从头查。5.3 监控上该埋哪些指标事后补救不如事前发现。建议在监控里固定采集这几项各 TCP 状态连接数按服务维度、进程 fd 使用率、新建连接失败计数、连接池 active/idle 数量。给CLOSE_WAIT设一个持续增长的告警比如 5 分钟内涨幅超过 200 且绝对值超过 500给 fd 使用率设 80% 预警。这些指标组合起来能在问题演变成故障之前就发出信号。我在实际项目里发现CLOSE_WAIT的增长速度比绝对值更有指示意义。绝对值高可能是历史遗留但持续快速增长几乎必然对应着正在发生的泄漏。把增长速率做成告警比单纯卡阈值灵敏得多也少了很多误报。另外连接池的 active 指标如果长时间贴着上限不回落即使CLOSE_WAIT还没堆起来也说明连接归还出了问题这时候介入成本最低。