一、现象与影响面:连接数报警不等于连接数故障
症状:高峰期接口偶发超时或 connection refused,监控上连接数曲线陡增,或者 accept queue overflow 的字样出现在 netstat 统计里。影响面从”个别请求重试后成功”到”新连接完全建立不了”不等——accept 队列溢出时,三次握手会被悄悄丢弃,客户端只觉得慢,服务器日志里却一行错误都没有,这是 TCP 连接问题最迷惑人的地方。
先立一个判断原则:连接状态统计是分诊台,不是病历本。看到 TIME_WAIT 数以万计就重启服务、清连接,等于把体温计读数当病治。反模式一:把 TIME_WAIT 堆积本身当故障处理——它绝大多数时候是高流量短连接服务的正常代谢,真正的病灶往往在别处。先取证,按下面的顺序推。
排查主线:先做全局状态统计定方向,再分别进 TIME_WAIT 线与 accept queue 线。所有命令默认在出问题的那台服务器上跑,netstat 老旧机器没有 ss 时才用 netstat。
二、第一步:状态统计,先分清四种堆积
# 服务器上;ss 比 netstat 快得多,机器上有就优先用
ss -s
1. TCP: 312 (estab 45, closed 0, orphaned 0, synrecv 0, timewait 280/0), ports 0
1. ← 总数、estab、timewait 一目了然
1. 按状态细分,看清每种状态各有多少
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
1. 280 TIME-WAIT
1. 45 ESTAB
1. 12 CLOSE-WAIT
1. 3 LISTEN
拿到分布就完成了分诊,四种典型堆积的走向完全不同:
| 堆积的状态 | 语义一句话 | 大概率是 | 下一站 |
|---|---|---|---|
| TIME_WAIT 量大 | 主动关闭方在等协议规定的时间流逝 | 高 QPS 短连接,多为正常 | 第三节 |
| CLOSE_WAIT 量大 | 对方已关,本方应用迟迟不调 close() | 代码里 fd 没关,资源泄漏 | 第三节,但走 CLOSE_WAIT 分支 |
| ESTAB 逼近上限 | 连接都是活的 | 流量真的涨了,或客户端没复用连接 | 第五节预防 |
| SYN-RECV 量大 | 握手完成了一半 | 半连接洪水或攻击,另案处理 | 本文不展开 |
方向定了就进对应分支。TIME_WAIT 与 CLOSE_WAIT 是最容易混的一对,先用一张表把语义钉死。
三、TIME_WAIT 与 CLOSE_WAIT:一字之差,两种人生
| 维度 | TIME_WAIT | CLOSE_WAIT |
|---|---|---|
| 谁进入这个状态 | 主动关闭的一方 | 被动关闭的一方 |
| 在等什么 | 等 2MSL 计时走完,确保最后的 ACK 送达、旧报文自然消亡 | 等本方应用调 close(),内核不能替你关 |
| 持续多久 | Linux 固定约 60 秒,不可调——常被张冠李戴的 tcp_fin_timeout 管的是 FIN_WAIT_2,不是它 |
没有计时器,应用不 close 就永远在 |
| 数量多说明什么 | 刚才短连接流量大,正常代谢;占的是少量内存与端口资源 | 应用漏关连接,每一条都在泄漏一个文件描述符 |
| 该不该动手 | 一般不动;长期居高才考虑让客户端用长连接 | 立刻查代码:异常分支里的连接、HTTP 客户端没复用也没关、响应体没读完就丢弃 |
判断口诀:TIME_WAIT 多,看流量曲线对得上就对得上;CLOSE_WAIT 多,不用想,去查代码。CLOSE_WAIT 的验证也直接——挑一条挂着的连接看归属:
# 服务器上,找出 CLOSE_WAIT 挂在哪个进程上
ss -tanp state close-wait | head
1. Recv-Q Send-Q Local Address:Port Peer Address:Port Process
1. 0 0 10.0.0.5:8080 10.0.0.9:52114 users:(("java",pid=1234,fd=890))
1. ← pid 拿去对进程,基本都能对到一段没处理异常关闭的调用代码
TIME_WAIT 数量确实高到影响建连(本地端口耗尽,报错里出现 Cannot assign requested address)时,正解排序是:客户端上连接池或 keep-alive(消掉大部分短连接)> 出方向连接开 tcp_tw_reuse(仅对主动发起方生效,依赖时间戳选项)> 扩源端口范围。网上的老教程常推荐 tcp_tw_recycle——这个开关已在内核 4.12 中移除,它当年在 NAT 环境下会静默丢连接,新系统里配置了也不存在,别再抄。
状态分布正常、TIME_WAIT 也在合理范围,但建连还是失败,往下查 accept queue。
四、accept queue:全连接队列溢出,最安静的故障
监听中的 socket 背后有一条队列:三次握手完成的连接先排在这里,等应用调用 accept() 取走。队列长度 = min(应用传给 listen 的 backlog,内核上限 net.core.somaxconn)。应用 accept 得不够快,或者队列上限太小,溢出后新连接直接被丢——客户端重试表现成”偶发超时”,服务器错误日志里却常常一个字都没有。
# 服务器上;-l 只看监听 socket,此时 Recv-Q/Send-Q 的含义变了:
1. Recv-Q = 当前排在 accept 队列里的连接数;Send-Q = 队列上限
ss -lnt
1. State Recv-Q Send-Q Local Address:Port
1. LISTEN 0 128 0.0.0.0:8080 ← Send-Q 只有 128,上限偏小
1. LISTEN 197 200 0.0.0.0:9090 ← 197/200,队列正在满,抓到现行
1. 内核统计里找溢出计数
netstat -s | grep -i listen
1. 1024 times the listen queue of a socket overflowed
1. 1024 SYNs to LISTEN sockets dropped ← 计数在涨 = 实锤溢出
两个数字对着看:ss -lnt 的 Recv-Q 长期贴着 Send-Q,加上 netstat -s 的 overflow 计数持续增长,溢出成立。然后回答”为什么满”,只有两个方向:
| 方向 | 特征 | 验证 |
|---|---|---|
| 应用 accept 慢:工作线程全忙、锁竞争、GC 停顿、同步阻塞在慢依赖上 | CPU 或线程池先打满,队列只是结果 | 看进程 CPU/线程栈,压测复现时抓一次栈 |
| 队列上限太小:backlog 参数没调,或被 somaxconn 封顶 | 流量毛刺一来就溢出,平时 Recv-Q 很低 | ss -lnt 看 Send-Q;sysctl net.core.somaxconn 看内核上限 |
内核上限有个值得记住的版本事实:老内核 net.core.somaxconn 默认 128,从内核 5.4 起默认提到 4096。很多老机器、老镜像里它仍是 128,应用传再大的 backlog 也会被压到 128。调大是 sysctl -w net.core.somaxconn=4096 并写进 /etc/sysctl.conf 持久化,同时把应用侧 listen 的 backlog 参数一起调(如 Nginx 的 listen 8080 backlog=4096;,数值不超过 somaxconn 才有效)。
反模式二:只调大 backlog 不查应用 accept 能力。队列从 128 加到 4096,只是把溢出点从”高峰开始后 3 秒”推迟到”高峰开始后 30 秒”,accept 慢的根子还在。队列是缓冲不是产能。另外别开 tcp_abort_on_overflow 当”解决”——它只是把静默丢弃改成发 RST,让客户端立刻报错,适合定位,不适合长期开。
五、根因清单:按出现频率排序
| 根因 | 典型症状 | 一行验证 |
|---|---|---|
| 客户端没复用连接,纯短连接打高流量 | TIME_WAIT 数与 QPS 曲线同步起伏 | ss -tan 状态分布对照流量曲线 |
| 应用漏关连接(异常分支/未读完整响应) | CLOSE_WAIT 单调上涨不回落 | ss -tanp state close-wait 找进程 |
| accept 队列溢出:应用处理慢 | overflow 计数增长,伴随 CPU/线程池打满 | netstat -s 看 overflowed 计数 |
| accept 队列溢出:backlog 或 somaxconn 太小 | 毛刺期 Recv-Q 贴满 Send-Q | ss -lnt 与 sysctl net.core.somaxconn |
| 文件描述符上限太低(ulimit) | Too many open files,accept 直接失败 | cat /proc/<pid>/limits 对照进程连接数 |
| 抄了 tcp_tw_recycle 的老配置 | NAT 后用户批量连不上;sysctl 报不存在 | sysctl net.ipv4.tcp_tw_recycle,4.12 起已移除 |
六、预防
| 措施 | 做什么 | 边界 |
|---|---|---|
| 状态曲线化 | 把 ss -s 的 estab/timewait/close-wait 三个数采集进监控,突变即告警 | CLOSE_WAIT 的告警阈值应远低于 TIME_WAIT,几十条就该看 |
| 客户端长连接化 | HTTP 客户端开 keep-alive、数据库走连接池,从源头消短连接 | 连接池要配上限与空闲回收,否则换成 ESTAB 堆积 |
| 队列三件套对齐 | 应用 backlog、somaxconn、fd 上限在部署清单里一次性对齐 | 队列调大只是缓冲,accept 产能以压测为准 |
这套走法收拢成一句:先用状态分布分诊(TIME_WAIT 正常代谢、CLOSE_WAIT 直奔代码、ESTAB 看容量),再对 accept queue 用 Recv-Q/Send-Q 加 overflow 计数实锤——重启清不掉任何一条根因,只能清掉现场。
延展阅读:ss/netstat 这些观测命令的日常用法,见《Linux 常用命令》;单机队列调到头仍扛不住时,下一步是横向分担,见《多节点集群架构规划》。
版本核实(以官方文档为准):TIME_WAIT 在 Linux 上持续约 60 秒且无直接可调参数(tcp_fin_timeout 只作用于 FIN_WAIT_2)、CLOSE_WAIT 无内核计时等待应用 close、ss 对 LISTEN socket 的 Recv-Q/Send-Q 分别表示队列当前值与上限,均为长期稳定内核/工具行为;net.core.somaxconn 默认值自内核 5.4 起由 128 提升至 4096、net.ipv4.tcp_tw_recycle 自内核 4.12 起移除,为已核实的版本事实;tcp_tw_reuse 仅对主动发起方向生效且依赖时间戳选项。具体数值语义以所用内核版本的 ip-sysctl 文档为准。