
干运维这些年跟 TCP 连接打了无数次交道。线上服务抖动、接口响应变慢、日志里刷 connection reset、监控图里 ESTABLISHED 数量一路爬升最后查来查去多数都能在一串 Linux TCP 连接状态里找到答案。这篇内容想把我在日常监控和排查 TCP 连接时的一套方法完整梳理一遍从状态机怎么读到 ss、netstat、tcpdump、nstat 这些工具的协作再到半连接队列、全连接队列、TIME_WAIT 和 CLOSE_WAIT 这类高频问题的完整处理链路最后沉淀成可以自动巡检的脚本。后端开发、运维、SRE或者刚准备系统学 Linux 网络的新人都能找到能直接抄作业的东西。1. 监控 TCP 连接前先把状态机装进脑子里1.1 三次握手在状态表上留下的脚印看到一个连接数是 3000 还是 300 万其实说明不了太多真正有价值的是连接当前卡在哪个状态。三次握手的每一步都会在连接状态表里留下痕迹。客户端刚发出 SYN本地 socket 进入 SYN_SENT服务端收到后把连接挂到半连接队列状态是 SYN_RECV等到最后的 ACK 到位双方连接才变成 ESTABLISHED。所以我每次排查建连异常第一反应就是先看状态分布。如果客户端进程的 socket 长时间停在 SYN_SENT说明 SYN 报文发出去了但 SYNACK 一直没回来方向往往在网络层或者服务端入口如果服务端有大量 SYN_RECV说明服务端已经收到大量 SYN但没走完第三次握手这时候半连接队列多半已经顶满syncookies 也没起作用或者干脆是有人在对着端口发起异常 SYN 请求。这些判断用ss命令一行就能确认关键是你脑子里得先有这条状态链路。1.2 四次挥手和 TIME_WAIT、CLOSE_WAIT 的对应关系断开连接比建立更值得关注因为这里藏着两个高频故障状态。主动关闭的一方发出 FIN 后进入 FIN_WAIT1收到对端 ACK 后进入 FIN_WAIT2再收到对端的 FIN、回完最后一个 ACK就进入 TIME_WAIT。被动关闭的一方收到 FIN 后进入 CLOSE_WAIT应用层调用 close 后再发出 FIN进入 LAST_ACK直到收到对方最后的 ACK。监控里真正需要注意两种异常趋势一是 CLOSE_WAIT 只增不减这几乎可以断定是某个进程收到了服务端关闭连接的 FIN但自己一直没有调用 close属于典型的文件描述符泄漏二是 TIME_WAIT 数量大到把本地端口耗尽常见于大量短连接、并发又很高的场景。这两类问题我会在后面专门展开这里先记住TIME_WAIT 是主动关闭方留下的“安心等待”状态CLOSE_WAIT 是被动关闭方在等应用层释放含义完全不同。很多新手上来就看状态数量却分不清这两个排查方向直接就错了。1.3 一张状态速查表状态触发时机监控关注点LISTEN端口在监听等待客户端 SYN检查是否误暴露、backlog 是否合理SYN_SENT客户端发出 SYN 等待响应停留过久网络不通或被对端丢弃SYN_RECV服务端收到 SYN等待最终 ACK大量堆积半连接队列溢出的前兆ESTABLISHED握手完成正常传数据总量和分布是否异常上涨FIN_WAIT1 / FIN_WAIT2主动关闭方向对方挥手长期停留对端不响应或 FIN 丢失TIME_WAIT主动关闭方收到 FIN 后数量过大会挤占源端口但短时间大量出现是正常现象CLOSE_WAIT被动关闭方收到 FIN 未 close只增不减基本等于代码忘关连接LAST_ACK被动方发出 FIN 后长期停留对方没收到最后的 ACKCLOSING双方同时关闭少见出现后一般能靠超时机制收敛这张表不用死记用多了自然会形成条件反射。一般看到输出第一列是 LISTEN先确认端口对不对看到 ESTABLISHED 大量再看来源聚合看到 CLOSE_WAIT 或 TIME_WAIT基本就能猜到下一步要往应用层还是传输层走了。2. 日常巡检优先用 ssnetstat 该退居二线了2.1 为什么我用 ss 而不是 netstat很多老教程一开口还是netstat -anpt但新环境里 net-tools 不一定装ss 属于 iproute2 工具集基本是默认带。更重要的是两者获取数据的方式不一样netstat 在 Linux 上要走/proc/net/socket这类接口逐个遍历连接一多会慢有时候还会让你怀疑是不是卡死而 ss 使用 netlink 协议直接问内核要 socket 信息快且完整还能支持状态过滤、进程关联、定时器显示这些高级查询。我不是说 netstat 完全不能用临时救急、看个监听端口它没问题但如果要监控、统计、采样我会优先写 ss。很多线上脚本还在用 netstat 做定时统计连接一多 CPU 都耗在这上面了换成 ss 能明显舒服一些。至少在我负责的服务器上已经很少会打开 netstat 了。2.2 最常用的几组 ss 命令先看整机连接全局概览一条命令就够ss -s它会输出 sockets 总数、TCP 数量以及按状态切分的计数适合第一眼判断“这台机器连接有没有异常”。如果 TCP 数量明显超出你对业务量级的认知再往下钻。要看监听端口和对应进程尤其是“这个端口到底谁在听”ss -tnlp-t只看 TCP-n不做域名和服务名解析-l只看监听 socketp是为了显示进程。需要说明的是看到别人进程需要 root 权限所以排查时建议谁的业务谁用 root 跑否则 Process 列会为空。要看当前所有 TCP 连接我基本固定用这一组ss -tan state established ss -tan state time-wait ss -tan state close-waitstate 后面可以跟 established、syn-sent、syn-recv、fin-wait-1、time-wait、close-wait 这些状态名ss 会直接过滤比ss -tan | grep TIME_WAIT这种写法节省大量无效输出。输出结果里每一行就是一条连接四元组协议、收发队列、本端地址端口、对端地址端口。2.3 从单条连接上升到状态统计和四元组统计状态分布的快速统计可以这样写ss -tan | awk NR1{print $1} | sort | uniq -c | sort -rn注意第一条是表头所以 awk 里用NR1跳过。如果只要 ESTABLISHED 数量最稳妥的是ss -tan state established | tail -n 2 | wc -l我见过有人用ss -tan state established | wc -l直接统计结果多算了表头一行看着问题不大但自动采集脚本里一个脏数据能带偏整个告警。类似这种细节恰恰是脚本类监控最容易翻车的地方。按对端 IP 聚合也很有用。例如发现某个 IP 和本机建立了大量连接怀疑是异常扫描或调用方连接池没复用ss -tan | awk NR1{print $5} | sed s/:[0-9]*$// | sort | uniq -c | sort -rn输出里每个来源 IP 的连接数一目了然。不过要注意这条统计适合 IPv4 环境如果系统里混有大量 IPv6 地址就不能用sed s/:[0-9]*$//这种简单切分因为 IPv6 地址自己就带冒号解析逻辑要单独写。我通常把这个命令做成函数放进 shell profile线上有点风吹草动先看全局状态再看来源聚合基本能快速锁定大头在哪里。3. 连接队列和丢包统计从“连接数”看到“服务质量”3.1 半连接队列和全连接队列分别装什么如果说只看状态分布能知道“连接卡在哪”那连接队列则是解释“为什么会卡”的关键。服务端处理新连接时内核维护了两种队列。SYN 队列也叫半连接队列三次握手的第二拍之后、第三拍完成之前连接对象挂在这里。它只负责存“还没有完成握手”的 socket。等第三次握手的 ACK 到达连接会从半连接队列挪到 accept 队列也叫全连接队列。注意此时连接其实已经是 ESTABLISHED只等着应用进程调用 accept() 把它接走。如果应用进程太忙、一直不取或者取的太慢全连接队列就会堆积。所以当你用ss -tnl查看某个监听端口时会看到两个数字Recv-Q 和 Send-Q。对监听 socket 来说Recv-Q 表示当前全连接队列里等待被 accept 的连接数Send-Q 表示队列最大长度这个值是应用 listen backlog 和内核net.core.somaxconn取最小值得到的。Recv-Q 如果长期贴着 Send-Q基本就可以断定“内核已经把连接准备好了但应用没来得及接走”。你看到的每次建连变慢、客户端 timeout可能就是在队首等着被 accept。3.2 用 nstat 观察溢出计数器而不是靠感觉队列有没有真正溢出不能靠“感觉连接很多”来判断要看内核计数器。nstat是内核网络统计的直接出口nstat -az | grep -iE ListenOverflows|ListenDrops|TCPReqQFullDrop|TCPBacklogDrop|TCPSYNRetrans这些计数器的含义按我的实践解释一下ListenOverflows全连接队列溢出的总次数即连接已经完成握手但没地方放被超时回收或者让客户端反复重试ListenDrops全连接队列丢弃的连接数范围比溢出略宽包括溢出和其他监听层丢弃TCPReqQFullDropSYN 队列满了之后直接把新到的 SYN 丢弃的次数这个值一旦在增长半连接队列大概率已经顶满TCPBacklogDropsocket 接收缓冲区或 backlog 处理不过来导致的丢弃TCPSYNRetransSYN 报文重传次数可以侧面反映对端建连极慢或握手被中间环节拦截。注意这类计数器看的是“增量”。第一次看nstat时看到一堆很大的累计数字很正常要再采样一次看两次之间的差值是否还在持续上涨如果只是某个高峰瞬间涨了一点随后归零不一定需要处理。老办法netstat -s也能看到 TcpExt 段的同类计数但数据是 /proc 导出的采样频率和精度都不如 nstat 顺手。新脚本我都会只用 nstat。3.3 队列相关的调优别一上来就乱调确认溢出确实在发生之后再动手调。最常见的一类是应用服务本身的 listen backlog 设置太小。很多框架的默认 backlog 只有 128 或 512在 QPS 稍高的服务上很容易撞到上限。应用层如果能在创建监听 socket 的地方把 backlog 调大效果最直接但真正生效的值还会被内核net.core.somaxconn卡住所以两边都要配合着看。需要调整的几个常用内核参数# 最大 SYN 半连接队列长度 net.ipv4.tcp_max_syn_backlog 4096 # 每个监听 socket 的 accept 队列上限 net.core.somaxconn 4096 # 半连接队列溢出时是丢包让客户端重试还是直接回 RST net.ipv4.tcp_abort_on_overflow 0 # SYN cookie 防御开关正常情况保持 1 net.ipv4.tcp_syncookies 1tcp_abort_on_overflow我要特别提醒默认 0 表示队列溢出时先选择丢包让客户端重试改成 1 则会直接向客户端发送 RST客户端会立刻看到 connection reset反而把“超时”的隐性问题变成“硬错误”。除非你明确知道自己在干什么否则不要轻易改成 1。我在生产上见过有人为了让“超时报错消失”而把它改掉结果雪崩得更厉害。tcp_syncookies是应对大量半连接请求的重要手段正常情况下建议保持开启但要注意它开启后部分高级 TCP 选项可能不可用对极端苛刻的内核调优场景需要权衡。这里的原则是先看监控计数再做最小改动每次只动一个参数并验证效果。4. 一次从抓包到定位的完整排查链路CLOSE_WAIT 和 TIME_WAIT 实战4.1 tcpdump 抓包确认握手到底断在哪一拍连接建立遇到瓶颈时ss 只能展示结果抓包才能看到过程。tcpdump 最常用的场景是观察三次握手每个阶段有没有回应。比如要盯某个服务的 8080 端口握手过程tcpdump -i eth0 -nn tcp port 8080 and tcp[tcpflags] (tcp-syn|tcp-ack) ! 0输出里通常能看到这样的序列客户端发 SYN服务端回 SYNACK客户端再回一个 ACK。如果只有第一个 SYN 反复出现且源端口一直在换说明服务端根本没有回应可能被防火墙丢弃、系统负载太高来不及收包也可能半连接队列满到连 SYN 都来不及处理如果服务端发了 SYNACK却看不到客户端回 ACK问题就偏向客户端侧比如客户端超时时间设置太短或者中间网络不稳定。一个容易踩的坑高流量环境千万不要直接tcpdump -i eth0 -nn不加任何过滤条件瞬间就能把磁盘写满。哪怕要抓全量也建议加上-c 100限定包数或者用-w file.pcap把现场留下来再慢慢分析。4.2 CLOSE_WAIT 持续堆积大概率是代码里连接没关CLOSE_WAIT 是这一排状态里最“不微妙”的异常之一。它的成因非常明确服务端收到了对端的 FIN也就是对方要求关闭连接但服务端的应用进程始终没有调用 close于是 socket 一直停留在 CLOSE_WAIT。只要这个状态只增不减别怀疑内核就是应用层在泄漏 socket。排查链路一般这样走。第一步看数量趋势ss -tan state close-wait | tail -n 2 | wc -l第二步找到这些连接挂在哪个进程上ss -tanp state close-wait第三步通过进程 id 看文件描述符是不是也在同步上涨ls -l /proc/pid/fd | wc -l如果数量持续上升基本就能定位到业务代码某处发完网络请求后没有可靠释放连接、循环里反复创建客户端对象、连接池配置了不合理的 keepAlive 但没有模拟对端异常断开等等。修复方向不外乎三件事确保所有网络读写路径都走到 close使用带连接池的客户端库给底层 socket 设置合理的读超时。这里还有个容易混淆的概念“TCP 粘包”并不是连接层的问题。TCP 是字节流不存在包边界所谓粘包是应用层在按照自己的协议解析数据时没有处理好流边界。排查连接监控时如果看到 CLOSE_WAIT 上涨不要和“应用层读到的报文不完整”混为一谈这是两套完全不同的排查路线。4.3 TIME_WAIT 过多别急着禁用它TIME_WAIT 是主动关闭方在连接进入 CLOSED 之前必须停留的状态时间大约是两倍 MSLLinux 内核里一般表现为 60 秒左右。它存在的意义是防止旧连接的迟到报文串扰到新连接并且保证最后一个 ACK 丢了还有机会重传。所以看到大量 TIME_WAIT 不用慌短连接高并发场景下这是常态。真正要担心的是本地端口被挤占。主动发起连接的一方出站连接用的源端口是从ip_local_port_range这个区间里选的区间默认通常是 32768 到 60999满打满算两万多个端口。每个连接进入 TIME_WAIT 会占住源端口一段时间如果每秒新建连接数量大端口周转不过来系统日志就会出现 connect 时找不到可用端口的报错。处理顺序我也按经验排一下。首选是应用侧改造把短连接改成连接池、长连接从源头减少 TIME_WAIT 的产生。其次可以在出站连接的一端打开net.ipv4.tcp_tw_reuse前提是同时开着tcp_timestamps它允许内核在安全前提下复用处于 TIME_WAIT 的端口对客户端出站连接比较有效。老文档里常出现的tcp_tw_recycle不建议碰在 NAT 环境下会引起很多诡异丢包有的内核版本里已经移除。另外注意net.ipv4.tcp_fin_timeout控制的是 FIN_WAIT2 的保留时间不是 TIME_WAIT别搞混。4.4 一个完整案例网关机新建连接超时的定位过程最后用一个实际处理过的场景来演示这条链路。现象是某网关机在业务高峰出现大量客户端建连超时服务端 CPU 和内存都不高看起来“莫名奇妙”。我按顺序做了这几件事先执行ss -s发现 SYN_SENT 数量异常偏多。再执行ss -tan state syn-sent看到大量发往同一台网关 IP 的连接都卡在 SYN_SENT说明服务端收到了请求但迟迟不回应。上服务端执行nstat -az对比两次采样TcpExtTCPReqQFullDrop和TcpExtListenOverflows都在持续增加。到这里基本定性为SYN 半连接队列和 accept 队列都出现了溢出应用进程来不及 accept新建连接在握手阶段就被拖到超时。最终修复是两手同时做应用把 listen backlog 从默认值提到 1024同时把net.core.somaxconn调到同样大小再观察 nstat 计数器确认两个溢出项不再增长后把备份的内核参数回滚保持最小改动。整个过程没有重启服务线上在分钟级内恢复。这种思路比拍脑袋改tcp_max_syn_backlog要靠谱得多因为你每一步都有计数器在给你反馈。5. 把 TCP 监控固化到日常脚本、指标与合理阈值5.1 一个够用的状态统计脚本排查是临时的监控是常态的。我习惯把最常用的状态统计写成一个简单的 bash 脚本放到 cron 里定期执行先把问题盯住#!/usr/bin/env bash # 全局概览 ss -s # 按状态统计 ss -tan | awk NR1{v[$1]} END{for(s in v) print s, v[s]} | sort -k2 -rn # 各监听端口的连接数按本端端口聚合 ss -tan | awk NR1{print $4} | awk -F: {print $NF} | sort | uniq -c | sort -rn # 关键异常数量 echo -n CLOSE_WAIT: ss -tan state close-wait | tail -n 2 | wc -l echo -n TIME_WAIT: ss -tan state time-wait | tail -n 2 | wc -l echo -n SYN_RECV: ss -tan state syn-recv | tail -n 2 | wc -l脚本的输出直接追加到日志文件配合监控图或者简单的判断逻辑就能在半夜收到告警而不是等用户先发现问题。数据要进时序库的话我更推荐把ss -s的状态计数、nstat 的关键计数器、每个进程的 fd 数作为基础指标按 30 秒一个周期采集。对大多数业务来说连接数不需要每秒精确30 秒足够发现趋势变化。5.2 告警阈值趋势比绝对值重要阈值设错比不设告警还危险。CLOSE_WAIT 偶发几个可能只是对端刚好重启但如果十分钟内只增不减说明应用侧在漏连接。TIME_WAIT 更是这样本地端口区间两万多个瞬间冲到一万不代表要挂持续半个小时不减才需要介入。我通常把判断逻辑写成增量判断本次采样值比上次高出一倍以上且连续三次都是这样才触发告警单次尖峰只记录不打扰。内核计数器同样适合用“增量差”做阈值比如两分钟内TcpExtTCPReqQFullDrop增长超过一定数量再结合ListenOverflows一起看能过滤掉很多干扰。阈值本身不是通用标准不同业务的连接量级差异太大关键是先把“正常基线”攒出来再在基线上叠加容忍度。5.3 别忘了文件描述符和容量规划每个 TCP 连接对应一个文件描述符应用能打开的 socket 数量同时受两个地方限制单进程的ulimit -n和系统级的fs.file-max。连接数监控如果只看网络层很可能漏掉这类资源瓶颈。我接过一个案例业务侧显示 ESTABLISHED 连接数在增长但服务进程报错无法创建新 socket一查ulimit -n还在 1024系统 limit 在高位是进程配置把它控制住了。这类问题用ss -tanp找到进程后顺手看下 fd 数基本不会误判。容量规划上我自己的经验是先关注连接生命周期和复用率其次才是数值本身。一个连接池把复用率做到 90% 以上的服务面对同样的 QPS 爆发产生的实际新建连接数可能只有短连接场景的十分之一。这也是我始终强调修 TIME_WAIT 和端口相关问题第一位永远是应用改造内核参数只是兜底。另一个和连接生命周期相关的参数是net.ipv4.tcp_keepalive_time默认 7200 秒也就是说空闲连接两小时后才开始探活。如果业务需要更早发现对端死链可以按需调低但探活频率太高也会增加不必要的网络包。最后再分享一个我个人的使用习惯遇到 TCP 相关故障我不会一上来就动内核参数而是先回答三个问题——连接卡在哪个状态、是否有队列溢出、内核计数器增量是否还在上涨。这三件事查完问题基本就能定性接下来无论调应用还是调内核都有了依据。希望你下次对着 ss 输出发呆的时候也能快速进入状态。