
半夜两点被电话叫醒说线上服务有一堆连接异常。登录服务器一看ss -s显示当前 TCP socket 数量比平时翻了几倍到处是CLOSE_WAIT。那一瞬间你就明白如果平时就把 TCP 连接监控这摊事搞清楚这种夜里的“惊喜”能少一大半。Linux 下的 TCP 连接监控表面上看就是敲几条命令实际牵扯到连接状态机、内核参数、文件描述符、端口资源、应用层线程模型这些盘根错节的东西。很多朋友一台机器上部署了几个服务管它三七二十一只要端口还在监听就觉得没事。但 TCP 连接这玩意儿是有状态的不同状态代表不同的问题连在哪儿、卡在哪儿、耗了多少资源看一眼就知道个大概。这篇文章我按自己排查问题的思路把 TCP 连接监控从工具到原理、从状态解读到实战排障整个串一遍争取让你看完之后遇到连接类问题能自己摸出门道。1. 连接监控究竟在监控什么1.1 从一次事故说起先说个我实际遇到的案例。一个 Java 写的微服务部署在 4 核 8G 的云主机上平时连接数稳定在两三百。某天发版之后监控图有根曲线突然像坐了火箭一样往上蹿QPS 没怎么变连接数却涨到 8000 多CPU 倒是没爆但用户开始反馈请求变慢。上去一看ss -ant里大片大片的CLOSE_WAIT服务进程的文件描述符计数接近上限。原因其实不复杂——代码里有个 HTTP 客户端连接池读响应超时之后没有关闭底层 socket服务端把连接关了客户端这边停留在CLOSE_WAIT不动。这就是 TCP 连接监控的典型价值它不直接告诉你业务哪里错了但它能精准指出问题发生的“层”。连接数异常往往是应用层问题的早期信号而且从状态分布上能反推问题的性质。1.2 监控对象拆开看TCP 连接监控本质上是监控三个维度的东西第一是连接的存在性和数量。某个 IP 和端口是否在监听、有没有进程绑定、当前建立了多少连接。这解决的是“服务在不在”的问题。第二是连接的状态分布。每个 TCP 连接都处于某种状态从SYN_SENT到ESTABLISHED再到TIME_WAIT。不同状态的堆积对应不同的问题。这解决的是“服务到底健不健康”的问题。第三是连接消耗的资源。每个 TCP 连接至少占一个文件描述符还要占用端口、内存。连接数一旦膨胀哪怕业务本身不报错系统资源也会被慢慢吃干。这个维度解决的是“还能撑多久”的问题。很多人喜欢直接看netstat -an | wc -l这种粗粒度的数字但如果能把三个维度分开看排障效率会高很多。我自己的习惯是看总量先判断有没有异常看状态分布判断异常类型看资源消耗判断容量的余量。这三个配合起来基本能把 80% 的连接问题定性。2. 工具选型和基础用法2.1 ss 与 netstat 的对比提到 Linux 看连接绕不开的是netstat和ss这两个命令。很多人有路径依赖习惯用 netstat但说实话netstat属于 net-tools 套件已经处于维护停滞状态而且它是通过解析/proc/net/tcp这类虚拟文件系统来获取信息连接一多速度肉眼可见地拖沓。ss属于 iproute2 套件直接读取内核 socket 的 inode 信息速度和信息量都更胜一筹。我一般直接推荐用ss原因很实际对比项ssnetstat数据来源内核 socket 结构直接读取解析 /proc/net/tcp大数据量下速度快慢状态过滤条件功能丰富可按状态精确过滤条件有限显示本机统计支持-s汇总支持逐项统计但不直观ss的效率优势在大规模连接场景下尤其明显。我在一台有 5 万多个 TIME_WAIT 的机器上试过ss -s基本秒出netstat -s要吭哧吭哧好几秒。线上排障每一秒都很关键工具选对了省很多事。2.2 最能打的几条 ss 命令我日常用得最多的几条命令整理出来可以直接收藏# 查看本机 TCP 连接的整体统计 ss -s # 查看所有 TCP 连接显示端口、状态、收发队列 ss -ant # 只看某个状态比如 TIME_WAIT ss -ant state time-wait # 查看某个端口上的所有连接 ss -ant | grep :8080 # 查看某个状态在某个端口上的连接数 ss -ant state established | grep :8080 | wc -l # 显示进程信息排查哪个进程占有大量连接 ss -antp | grep :8080 # 只看监听端口 ss -tln这里有个细节值得注意ss输出的 Recv-Q 和 Send-Q对ESTABLISHED状态的连接来说表示收发缓冲区里积压了多少字节没处理。如果 Send-Q 长期不为零说明对端不消费数据可能是对端应用卡死如果 Recv-Q 长期不为零说明本端应用不读数据可能是线程阻塞。这两个队列的长度是应用是否“及时消费数据”的直观指标排查延时问题的时候很管用。2.3 配套查看文件的工具只靠 ss 还够不全面。连接背后是文件描述符文件描述符背后是进程。所以我还习惯搭配lsof和fuser一起用# 查看某个进程打开的所有 TCP socket lsof -i -a -p PID # 查看某个端口被哪个进程占用 lsof -i :8080 # 快速列出占用端口的进程 fuser -v 8080/tcp这些工具组合起来才能完成“连接异常 → 定位进程 → 定位代码”这条完整的链路。后面讲排障场景时我会再演示怎么把它们串起来用。3. TCP 状态机监控排障的地图3.1 三次握手和四次挥手连接监控如果只看数字不看状态等于拿到了一张没有图例的地图。TCP 是一个面向连接的状态机协议客户端和服务端各自维护一组状态通过报文段驱动状态流转。建立连接的过程大家应该很熟客户端发SYN进入SYN_SENT服务端收到后回SYNACK进入SYN_RECV客户端收到后回ACK并进入ESTABLISHED服务端收到ACK也进入ESTABLISHED。这个过程就是经典的“三次握手”。断开连接的过程稍微曲折一点。主动关闭方发FIN进入FIN_WAIT_1对端收到FIN回ACK本端进入FIN_WAIT_2对端再发自己的FIN进入LAST_ACK本端收到FIN后回ACK进入TIME_WAIT等待 2MSL 后彻底关闭。如果本端是被动关闭方收到FIN后先回ACK进入CLOSE_WAIT等应用主动调close()之后才会真正挥手。这些状态说起来简单但每一种在线上都有对应的“坑”。比如SYN_RECV大量堆积基本可以怀疑受到了 SYN 泛洪或者服务端 backlog 满了CLOSE_WAIT大量堆积大概率是应用代码忘了关连接FIN_WAIT_2堆积往往是对端一直不关闭连接又不发FIN属于半开连接。3.2 几个高价值状态的深度解读TIME_WAIT。这个状态是主动关闭方在发送最后一个ACK之后进入的要等 2 个最大分段生存期2MSL之后才消失。为什么要等这么久两个原因一是防止最后一个ACK丢了对端重发FIN自己有重传的机会二是防止旧连接的重复报文干扰新连接。TIME_WAIT 多本身不是病它说明系统经历了大量主动关闭的连接这在短连接场景下非常常见。但 TIME_WAIT 数量太多会占用本地端口导致Cannot assign requested address这类报错。内核参数net.ipv4.tcp_tw_reuse和net.ipv4.ip_local_port_range就是用来缓解这个问题的后面排障章节细说。CLOSE_WAIT。这个状态的语义很明确对端已经发来了FIN本端内核回了ACK但应用层还没调用close()。换句话说连接已经半死了应用还攥着不撒手。CLOSE_WAIT堆积几乎必然是应用代码 bug比如输入流没关、连接池复用逻辑错误、超时后没有释放资源。我在实践中观察到的规律是CLOSE_WAIT 数量如果持续不降往往伴随文件描述符数不断爬升因为每个CLOSE_WAIT连接都占着一个 fd。等到 fd 耗尽新连接就建立不起来了服务整个就瘫痪了。SYN_RECV。这个状态表示服务端已经收到SYN并回了SYNACK但还没收到最终的ACK。如果这个状态大量堆积一种情况是客户端恶意不发最后的 ACK也就是 SYN 泛洪另一种情况是服务端的半连接队列backlog太小正常的握手都排不上队。区分这两种情况可以看/proc/net/stat/syn_cookies或者内核日志里有没有possible SYN flooding on port的告警。也可以直接看netstat -s里SYNs to LISTEN sockets dropped的数量。3.3 连接失败对应的状态连接建立失败通常表现为两种错误Connection refused和Connection timed out。前者说明服务端回了 RST可能是端口没监听、防火墙拦截、或者是服务端 accept 队列满了之后主动拒绝后者说明发出的 SYN 石沉大海大概率是网络不通、对端主机不可达或者被防火墙静默丢弃。这两个错误的排查思路完全不同。refused是本机或对端的问题重点查服务进程和端口监听timeout重点查网络链路、路由、防火墙规则。理解状态机之后你看到错误提示就知道大概往哪方向查。4. 实操搭一套连接监控的脚本4.1 状态分布统计脚本光靠人工敲命令不能算监控。实践中最简单实用的做法是写一个脚本定时把 TCP 各状态的数量统计出来落到日志或者推到监控系统。下面这个脚本是我自己常用的版本#!/bin/bash # tcp_state_monitor.sh # 统计本机各 TCP 状态数量追加写入日志 LOG_DIR/var/log/tcpmon mkdir -p $LOG_DIR LOG_FILE$LOG_DIR/tcp_state_$(date %Y%m%d).log # 思路ss -ant 输出第一列是状态用 awk 统计即可 STATS$(ss -ant | awk NR1 {print $1} | sort | uniq -c | sort -rn) # 顺便统计总的连接数和文件描述符占用 TOTAL$(ss -ant | wc -l) FD_COUNT$(ls /proc/你的进程PID/fd | wc -l) echo $(date %F %T) total$TOTAL fds$FD_COUNT $LOG_FILE echo $STATS $LOG_FILE这个脚本的输出大概是2026-02-10 00:30:01 total142 fds89 89 ESTAB 37 TIME_WAIT 16 LISTEN配合crontab每分钟跑一次* * * * * /usr/local/bin/tcp_state_monitor.sh有条件的团队可以把这段输出直接转给 Prometheus 之类的系统写成文本采集格式或者用pushgateway推上去。总之核心逻辑就是定期采样、保存历史、画出趋势。4.2 端口和监听状态检查除了统计状态分布还需要关注端口监听是否正常。有时候进程假死端口还监听着但完全不再 accept 新连接有时候进程重启失败端口没了。所以我会在脚本里加一段端口检查# 检查关键端口是否监听 for port in 8080 8081; do if ss -tln | grep -q :$port\b; then echo port $port is listening else echo port $port is DOWN fi done这里用ss -tln而不是netstat -an因为监听属于 TCP 的LISTEN状态-l专门过滤监听端口输出干净。4.3 监控本地端口资源余量本地端口范围是很容易被忽略的资源。一个客户端进程对外发起大量短连接时每次连接会占用一个本地临时端口连接关闭后进入TIME_WAIT还会短暂占用。如果本地端口耗尽报错Cannot assign requested address。查看本地端口范围cat /proc/sys/net/ipv4/ip_local_port_range默认一般是32768 60999也就是接近 2.8 万个可用临时端口。如果系统上有大量短连接这个数字很容易被吃满。脚本里可以这样持续监控剩余端口数PORT_RANGE($(cat /proc/sys/net/ipv4/ip_local_port_range)) TOTAL_PORTS$(( ${PORT_RANGE[1]} - ${PORT_RANGE[0]} )) USED_PORTS$(ss -ant | wc -l) FREE_PORTS$(( TOTAL_PORTS - USED_PORTS )) echo free local ports: $FREE_PORTS虽然精确计算还涉及端口复用等参数但作为一个粗略的趋势判断这个数字够用了。4.4 文件描述符告警文件描述符是连接监控绕不开的资源。每个 socket 都是一个 fd但 fd 还被普通文件、管道等占用所以只监控连接数不监控 fd 数会漏掉风险。比如某个程序因为日志文件句柄没关fd 数缓慢爬升最终导致无法新建连接这种案例并不少见。查看系统级 fd 上限和当前用量cat /proc/sys/fs/file-max cat /proc/sys/fs/file-nrfile-nr输出三列系统分配的文件句柄数、未分配的、最大上限。当已分配数接近上限时就该查是哪个进程泄漏了。进程级则看ls /proc/PID/fd | wc -l或者用lsof -p PID | wc -l。把这两个指标纳入监控脚本告警阈值设成“已使用 fd 数超过上限的 80%”基本能提前避免大部分连接耗尽的故障。5. 常见连接问题排查实录5.1 CLOSE_WAIT 久居高位的处理如果说连接监控里只能记住一种异常状态那我推荐记住CLOSE_WAIT。它几乎是应用层 bug 的代名词看到它数值居高不下基本可以断定是某个服务的代码没有正确关闭 socket。排查步骤是这样走第一步确认现象。执行ss -ant state close-wait | head -20看看这些半死连接集中在哪几个端口上。如果集中在 8080那问题基本锁定在 8080 对应的服务里。第二步找到进程。ss -antp state close-wait | grep :8080输出里的进程 PID 和名称直接指向嫌疑人。如果显示进程名是 Java那大概率是 HTTP 客户端连接池或者数据库连接池的问题。第三步挖代码。用lsof -p PID | grep TCP | grep close_wait看这些连接对应哪些 socket再回代码里排查对应的读超时分支有没有在 finally 里关闭连接。常见病根有HttpClient响应流没有 close、连接池设置了过长的空闲超时、业务线程在循环里创建连接而没释放。第四步临时止血。有的团队会调低net.ipv4.tcp_keepalive_time或 rely 于SO_KEEPALIVE让内核回收僵死连接但说句实话这只是临时手段。CLOSE_WAIT 的收尾动作必须由应用层调用 close 完成内核无法代劳。真正治本还是修代码把连接的释放逻辑理清楚。5.2 TIME_WAIT 过多导致的端口耗尽短连接高并发的场景下TIME_WAIT 堆积是最常见的现象之一。每次主动关闭连接本地端口要等 2MSL 才能复用默认配置下 2MSL 是 60 秒期间这个端口不能分配给新连接。如果每秒新增几千个短连接60 秒内需要的端口数量就是几万个很容易撞上本地端口上限。遇到这种问题按顺序处理先确认内核真的存在端口耗尽风险。看ss -s输出里的timewait数量再看本地端口范围估算一下最大并发连接数。如果判断风险确实存在再考虑调参# 快速回收 TIME_WAIT允许复用仅在确认环境适用时开启 sysctl -w net.ipv4.tcp_tw_reuse1 # 调大本地端口范围增加可用端口总量 sysctl -w net.ipv4.ip_local_port_range1024 65535关于tcp_tw_reuse有一个点必须提醒它只对“客户端主动发起的新连接”生效而且要求远端时间戳比旧连接的新。在 NAT 网络环境里开启tcp_tw_reuse偶尔会引发连接串扰的问题需要验证业务场景再决定。更稳妥的做法其实是改造应用层用连接池复用长连接从根上减少短连接的产生。另外微信地推那句老话也成立tcp_tw_recycle千万别随便开。它比tcp_tw_reuse激进得多会导致同一 NAT 出口后面多个主机的连接被误判时间戳而重置属于经典的“省小事惹大祸”的参数。5.3 SYN_RECV 大量堆积和半连接队列塞满SYN_RECV 堆积直接反映在“服务端新连接建立缓慢”甚至“拒绝新连接”上。发现大量 SYN_RECV 后先别急着怀疑攻击看一下是不是 backlog 太小。Linux 里半连接队列的上限跟两个参数有关net.ipv4.tcp_max_syn_backlog和应用程序listen()时的 backlog 参数。对于 Java 的ServerSocketbacklog 默认 50 左右Nginx 的listen backlog默认 511。如果服务本身短平快正常流量下很少出现 SYN_RECV 堆积真堆起来了要区分原因。排查动作# 查看半连接队列溢出计数 netstat -s | grep SYNs to LISTEN sockets dropped # 查看 syn cookies 使用情况 sysctl net.ipv4.tcp_syncookies如果SYNs dropped计数在持续增长说明确实有握手被丢弃。可以调大 backlog 或优化应用 accept 逻辑。如果确认是 SYN 泛洪net.ipv4.tcp_syncookies1可以在极端场景下帮你暂缓压力但 syncookies 机制本身不建议常态依赖因为它打破了 TCP 状态协商的完整性很多高级特性会失效。5.4 连接数整体飙升但业务量未变这种情况我在实践中见过好几回特征很典型连接数稳定上涨但 QPS、响应时间都波澜不惊像是有什么东西在“偷建连接”。一条更细的排查思路是看ss -ntp里的对端地址。如果对端 IP 全来自同一个网段或某个中间组件比如反向代理、网关、监控探针问题可能在那一环如果对端是大量随机的内网 IP再看看是不是负载均衡器把健康检查频率调高了。用ss -ant | awk {print $5} | cut -d: -f1 | sort | uniq -c | sort -rn做个简单的“连接来源排行榜”一眼就能看出谁在大量建立连接。还有一类容易被忽略的大量ESTABLISHED连接但其实没有任何数据在传输。这往往和 TCP keepalive 配置有关。默认net.ipv4.tcp_keepalive_time是 7200 秒意味着 2 小时才探测一次对端是否还在空闲连接占着资源不释放。如果是内网环境可以适当调小到 600 或 300让内核主动清理僵死连接。但这一步同样要和业务特性对齐别把长轮询或者 WebSocket 的正常空闲连接给误杀了。5.5 端口被占用导致的启动失败最后聊一个和“连接监控”看起来关系不大、但运维中经常碰到的场景服务启动时报Address already in use。这其实是连接资源没有释放干净的变体。排查时用ss -tlnp | grep :8080找到占用进程。有时候进程明明已经退出了端口还被占用那多半是处于TIME_WAIT状态的僵尸连接在“占坑”。对于解释器或者服务快速重启的场景这个问题尤其频繁。如果确认是需要立即启动的服务可以临时设置net.ipv4.tcp_tw_reuse1或者给服务进程设置SO_REUSEADDR套接字选项。Java 里ServerSocket默认就带了SO_REUSEADDR所以这类问题在 Java 服务上相对少见在 C/C 自己写 socket 的场景里倒是经常遇到。6. 监控体系建设的一些体会把单点的脚本做成体系才能发挥真正的价值。我见过一些团队监控面板上堆了十几个 TCP 相关的图表可出了问题还是两眼一抹黑因为图表之间没有逻辑关联。我自己在实践中最受益的三条经验写在这里供参考。经验一监控指标要分层。最外层看总量和状态分布做粗粒度告警中间层看关键端口的连接来源分布和队列积压做故障定位最内层看进程的 fd 数、线程数、GC 情况做根因分析。三层指标的告警阈值要分开避免一层抖动触发一堆无意义告警。经验二连接类告警必须关联业务指标。单纯说“连接数大于 X”意义有限。更好的做法是把连接数变化和 QPS、P99 延迟放同一张图上对比观察。连接数涨而 QPS 没变大概率是连接泄漏或者健康检查异常连接数涨而 QPS 同步涨那只是业务波动不值得深夜爬起来。看多了两者的关联你对“什么才算异常”的感觉会准确很多。经验三重视趋势而不是单点阈值。我一开始做告警时设置的是绝对阈值比如连接数超 5000 就报警。结果高峰期正常波动就误报了好几次。后来改成看“变化率”比如 5 分钟内连接数翻倍才告警误报率明显下降。TCP 连接的绝对数量跟业务形态强相关不同服务之间没有可比性但同一服务的异常增长速率往往才是更靠谱的异常信号。7. 最后分享一个实用小技巧排查 TCP 连接问题时我常被问到有没有什么“快人一步”的操作。有一个小习惯我觉得很值钱不要只敲一次 ss要连续敲三次每次间隔一秒。连接状态是随报文流动而快速变化的单次快照只能看到某一瞬间的情况而三次采样能看出状态变化的趋势。比如SYN_RECV是越积越多还是建建立立、自然消退CLOSE_WAIT是只增不减还是新旧交替这两者的处理思路完全不同。再补一条更实用的把常用排查命令整理成脚本或别名统一放在自己的工具库里。比如alias tcpconnss -ant | awk \NR1 {print $1}\ | sort | uniq -c | sort -rn alias tcplistenss -tlnp alias tcpclosedss -antp state close-wait alias tcpsynss -antp state syn-recv | head -20这样真正排查问题时一条命令就出结果不用现场回忆语法。把这些工具沉淀下来配合上面讲的状态机理解TCP 连接监控才算真正长在自己身上了。以后不管是半夜接到告警电话还是同事丢过来一个“连接数异常”的截图你都能比较从容地一步步拆解而不是对着满屏的ss输出干瞪眼。