
排查Linux服务器问题十次有八次最后都会绕回TCP连接上。nginx突然502、数据库连接池被打满、docker端口死活起不来、Harbor推送镜像时冒出dial tcp超时的报错……这些看起来毫无关联的故障最终定位时无一例外都会落回到同一个地方这台机器上的TCP连接到底处于什么状态、被谁占用、为什么长时间不释放。这篇文章不打算按教科书顺序讲TCP/IP协议理论而是从实际运维视角把Linux上的TCP连接监控这件事彻底讲清楚怎么看看哪些指标看到异常怎么定位以及生产环境下怎么把连接监控变成日常巡检的一部分。适合正在和线上故障较劲的运维、后端开发也适合刚入门想搞懂连接概念的Linux新手——你会发现弄明白连接状态之后再回看各种报错思路会清晰很多。1. 先搞清楚TCP连接监控到底要看什么1.1 从状态机开始三次握手和四次挥手是理解一切的地基很多朋友一上来就背命令ss、netstat、lsof一套操作猛如虎但看到输出里那一堆ESTABLISHED、TIME_WAIT、CLOSE_WAIT就头皮发麻。问题不在于命令不认识你而在于你不认识这些状态背后的故事。TCP是面向连接的协议连接生命周期分为建立和释放两个阶段。建立时三次握手客户端发SYN服务端回SYNACK客户端再回ACK此时连接进入ESTABLISHED状态双方可以传输数据了。释放时四次挥手主动关闭方发FIN对端回ACK对端再发FIN主动关闭方回ACK中间会经历FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK这些过渡状态。我习惯用打电话来类比你拨号SYN对方接起来说喂SYNACK你说你好ACK通话建立挂电话时你说先这样吧FIN对方说好ACK对方又说那我挂了FIN你说拜拜ACK通话结束。这个类比虽然简化了细节但能帮你建立状态是双方协商的阶段性标记这个直觉。实际监控时我们不需要时刻盯着每一次握手而是看大量连接在这些状态上的分布情况。状态分布就像医院急诊室的分诊台——你是发烧、骨折、还是心梗对应的处理方式完全不同。TCP连接的每个状态都对应一种病情。1.2 监控连接的五维视角数量、状态、对端、队列、时长具体到监控动作不要只盯一个指标我从实践中总结出五个维度基本覆盖了所有连接类故障的排查方向。第一是连接数量。当前系统同时存在多少条连接这是最基础的量级感知。骤增通常意味着流量进来了或异常请求打进来骤降可能意味着服务挂了。第二是状态分布。五种主要状态的数量变化比总量更有价值。比如CLOSE_WAIT从个位数涨到几百说明应用层有bugSYN_RECV堆积说明半连接队列可能被打满。第三是对端地址分布。连接是和谁建立的是自己的客户端、是数据库、是外部爬虫还是攻击流量。通过聚合对端IP能快速圈定流量来源。第四是收发队列积压情况。ss输出里有Send-Q和Recv-Q这两个值代表数据是否卡在缓冲区里。Recv-Q持续大于0且涨不上去说明对端迟迟不读数据多半是应用层处理不过来。第五是连接存活时长。一条连接应该短命比如HTTP请求还是长命比如数据库连接池是有预期的。存活时间异常偏长的连接往往是泄漏的信号。这五个维度不用每次都看全但它们共同构成了一个异常检测坐标轴当你发现某个维度的值不正常时其他维度能帮你继续缩小范围。2. 从ss到/proc监控工具全盘点2.1 ss命令现代Linux下当之无愧的第一选择先给结论新系统上排查连接问题优先用ss而不是netstat。原因很简单——ss直接通过netlink从内核读取socket信息而netstat要解析/proc/net/tcp文件并逐行格式化输出在连接数很大的时候比如几万条ss的速度优势是肉眼可见的。ss常用组合我列一下# 查看所有TCP连接包含进程信息数字显示端口 ss -tanp # 只看某个状态的连接 ss -tan state established ss -tan state time-wait ss -tan state close-wait # 监听中的端口等同于 netstat -tlnp ss -tlnp # 只看连到某个端口上的连接 ss -tan sport :80 ss -tan dport :3306第一条命令是我用得最频繁的。输出里每一列的含义不用死记重点看State状态、Local Address本地地址、Peer Address对端地址这三列就够入门了。加上-p参数可以看到占用这个socket的进程PID和名称这个信息在排查端口占用时极其关键。给个实际体验在高并发机器上netstat -an可能要卡好几秒才能出结果ss基本是秒出。遇到线上问题大家都着急工具响应速度就是排查效率。2.2 netstat和lsof老牌工具依然有用武之地ss虽然全面但netstat和lsof在某些场景下依然是利器。netstat的好处是兼容性好老一点的Linux发行版上可能没有ss命令但netstat一般都在不过现在新发行版默认也不带netstat了需要装net-tools包。另外有些运维脚本还是基于netstat写的你接手旧系统时免不了要读这些脚本。所以netstat -tunap这种经典写法还是要能看懂。lsof的价值则在于文件维度。Linux下一切皆文件socket也是文件。当你需要回答谁占用了这个端口或者这个进程打开了哪些网络连接时lsof是最直接的# 查找占用某个端口的进程 lsof -i:8080 # 查看某个进程的所有网络连接 lsof -i -a -p 12345lsof输出里会显示COMMAND、PID、FD文件描述符、TYPEIPv4/IPv6、NODETCP/UDP、NAME地址和端口。FD列如果显示u表示socket看NAME列就能知道连接的对端是谁。说实话日常排查里我用ss -p更多但lsof -i:port查端口占用这一招在面试里也是高频考点值得记牢。2.3 数据源揭秘/proc/net/tcp里藏着最原始的数据ss和netstat只是工具它们的数据底层都来自内核。如果你想彻底掌握连接监控强烈建议直接读一读/proc/net/tcp这个虚拟文件。它是内核向用户态暴露的socket表每一行代表一条TCP连接。打开看一眼格式大致是sl local_address rem_address st tx_queue rx_queue tr tm-when retrnsmt uid timeout inode 0: 0100007F:1BB9 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 30000 1 ffff8d0e2b6e3700 100 0 0 10 0看起来像天书其实拆开就几个关键字段。sl是槽位编号local_address和rem_address是十六进制的小端IP地址加端口。st字段是状态码对应关系是01ESTABLISHED02SYN_SENT03SYN_RECV04FIN_WAIT105FIN_WAIT206TIME_WAIT07CLOSE08CLOSE_WAIT09LAST_ACK0ALISTEN。uid是归属用户inode可以关联到进程。为什么要懂这个因为在嵌入式环境、精简系统或者没有iproute2工具的情况下你可能只有这个文件可以分析。自己写个Python脚本读取并解析它就能做出一个专属的连接监控小工具比任何现成工具都灵活。我后面会给出一个简单的解析示例这里先记住一点所有连接信息的内核源头就是/proc/net/tcp以及IPv6对应的/proc/net/tcp6。2.4 工具选型对照表什么场景用什么家伙给一张表总结一下工具定位方便你快速对照选择工具核心优势典型用途适用场景ss快信息全netlink直读内核日常连接状态、监听端口查看首选工具中大连接数下无压力netstat兼容性好老系统通用旧脚本维护、老系统排查新装系统需要net-tools包lsof文件维度透视进程/端口关系查端口占用、进程socket列表想知道谁占着80端口时/proc/net/tcp最底层数据源无依赖自研监控脚本、嵌入式环境没有iproute2工具的环境tcpdump抓包验证看数据包交互过程三次握手分析、连接建立失败需要确认到底发包了没有时前四个解决连接在哪里、什么状态的问题tcpdump解决网络上发生了什么的问题。一个偏静态一个偏动态搭配使用才能还原完整现场。3. 五大关键状态深度解读每种状态都对应一种故障模式3.1 ESTABLISHED和SYN_SENT连接建立的绿灯与黄灯ESTABLISHED是健康的信号表示连接已建立且处于数据传输状态。大量正是正常的比如Web服务器通常在高峰时维持数千条ESTABLISHED连接如果你把进程对应的连接数画成曲线它能直接反映业务的实时压力。我看到很多监控系统会把ESTABLISHED数量作为核心指标配合CPU和内存一起看判断是否需要扩容。SYN_SENT则是本地已经发出SYN包但还没收到对端SYNACK的状态。大量SYN_SENT堆积说明网络不通、对端没有响应、或者对端防火墙在静默丢包。我自己遇到过数据库主备切换后连接失败当时ss里大量连接卡在SYN_SENTtelnet到对端端口也没有回应最后确认是新备库机器的防火墙规则没有放行。排查SYN_SENT的重点不在本机而在网络链路对端。顺便说一句SYN_SENT状态如果持续存在而非只是瞬态很可能与TCP重传有关可以结合tcpdump看有没有重传包确认是丢包还是对端真的没反应。3.2 TIME_WAIT多到爆主动关闭方的专属影子TIME_WAIT大概是互联网上被讨论最多的TCP状态。它出现在主动关闭连接的一方是四次挥手后最后一个ACK已经发出、等待可能迟到的数据包的状态。这个状态必须持续2MSL最长报文段寿命的两倍Linux默认约60秒目的是确保最后一个ACK如果丢失对端还能通过超时重发FIN不至于让旧连接的数据串扰到新连接上。所以TIME_WAIT本身不是故障它是TCP可靠性的基石。真正让人头疼的是量大的时候每个TIME_WAIT socket都占用一个本地端口尤其在主动大量发起短连接的场景下可能把本地端口范围耗光客户端报Cannot assign requested address。处理思路有两条一是通过参数复用TIME_WAIT连接二是扩大可用的本地端口范围。具体参数我在后面内核优化部分细讲。先记一个检查命令ss -tan state time-wait | wc -l如果你的服务是HTTP短连接模型几万条TIME_WAIT是常见现象不必恐慌看趋势而非绝对值。3.3 CLOSE_WAIT堆积最典型的应用层连接泄漏如果说TIME_WAIT是正常的累赘那CLOSE_WAIT就是bug的标志。CLOSE_WAIT出现在被动关闭方对端发了FIN本机内核回复ACK后连接进入CLOSE_WAIT此时应用层需要调用close()把这个socket关掉连接才会继续走完挥手流程。问题就出在这里——如果应用代码里有socket没有正确关闭典型的比如异常分支没处理、忘了释放连接资源CLOSE_WAIT就会一直挂着堆积下去。我有一次处理线上业务接口超时问题用一条命令看到CLOSE_WAIT数量高达7000多基本就是某个连接池的获取逻辑在超时异常时没把连接归还导致连接池被旧连接占满新请求全部排队等待。这个状态的可怕之处在于它不报错、不超时内核的TCP保活机制需要很久才触发只是默默把系统的文件描述符和内存耗光。所以监控CLOSE_WAIT数量比监控ESTABLISHED总量更有提前量意义它是应用层健康度的晴雨表。排查CLOSE_WAIT的标准手法先确认哪些进程产生了CLOSE_WAIT用ss -tanp | grep CLOSE-WAIT看进程名和PID再用strace -p PID跟踪这个进程的close和shutdown调用基本能定位到是哪个连接没有释放。3.4 FIN_WAIT2和LAST_ACK挥手走到一半的迷路人FIN_WAIT2出现在主动关闭方表示已经发了FIN并收到了对端的ACK但一直在等对端也发FIN过来。一般来说这个状态是瞬态的但如果对端应用一直没有关闭自己的socketFIN_WAIT2就可能长期残留。内核里net.ipv4.tcp_fin_timeout参数控制了这个状态的不可用超时时间默认60秒适当调低能加速回收。LAST_ACK是最后一次挥手的状态出现在先收到对端FIN、自己发了FIN但还没收到对端ACK的一方也就是被动方的最后一步。如果大量LAST_ACK堆积说明对端没有正确回复ACK通常需要对端系统或应用配合排查。这两个状态单独出现少量都没事但如果在监控里看到数量稳定增长就要留意是不是对端系统在装死。实际经验是客户端程序崩溃、服务器强制重启都会造成大量未完成挥手状态的连接残留。4. 命令行实战从看状态到定位故障的完整路径4.1 三秒统计当前连接状态分布拿到一台Linux机器第一步永远是先看整体分布别急着盯细节。一行命令快速统计ss -tan | awk NR1 {print $1} | sort | uniq -c | sort -rn | head输出大概是这样的5234 ESTAB 389 TIME-WAIT 128 CLOSE-WAIT 22 LISTEN 3 SYN-SENTNR1是跳过表头$1取状态列sort和uniq统计数量再按数量倒序排。这条命令基本上从我入行用到现在是排查连接问题的第一个动作。看到分布之后心里就有谱了ESTAB建得多是常态TIME-WAIT多是主动请求多CLOSE-WAIT多了要立刻警惕应用层问题SYN-SENT多则先怀疑网络。4.2 聚合出TOP N的远端IP和端口分布看完下一步通常是想知道连接都连到了哪里。特别是当连接数飙高时你得立刻知道是哪边的流量在打进来。聚合对端IP用这条ss -tan | awk NR1 $1ESTAB {print $5} | cut -d: -f1 | sort | uniq -c | sort -rn | head -n 10$5是对端地址列格式是IP:端口cut按冒号取出IP部分然后排序统计。输出了前10个对端IP每个IP的连接数一目了然。如果你是接入层服务看到某个IP的连接数异常高可能是攻击流量或者某个上游服务配置错误。想看端口维度的聚合把cut换成取端口部分-f2ss -tan | awk NR1 $1ESTAB {print $5} | cut -d: -f2 | sort | uniq -c | sort -rn | head -n 10在微服务场景里这个命令能快速看出流量集中在哪个端口有助于判断是不是某个下游服务触发了雪崩。4.3 从一次Harbor推送失败看完整排查路径刚才讲了一堆原理和单条命令现在串一个完整案例。这个报错我印象很深也是很多自建镜像仓库的团队踩过的坑harbor 推送失败 get https://192.168.209.133/v2/: dial tcp 192.168.209.133:443: connect: connection refused单独看后半段dial tcp ... connect: connection refused意思是TCP连接被对端拒绝也就是三层网络通常没问题但对端的443端口没有进程在监听或者防火墙主动回了RST。当时我的排查顺序是这样第一步在报错机器上直接测目标端口通不通curl -v https://192.168.209.133/v2/如果重演connection refused说明Harbor服务本身可能没起好。第二步登录Harbor服务器看443端口有没有监听ss -tlnp | grep 443如果没有输出说明Harbor的nginx容器可能没跑起来如果有监听再确认是否只监听在了IPv6或特定网卡上。第三步看Harbor容器状态docker ps -a | grep harbor常见原因就是某个Harbor组件容器意外退出或者磁盘满了导致nginx容器反复重启。这种时候TCP监控的意义在于它帮你把问题从网络层和应用层分开避免你在防火墙配置上浪费半天时间。4.4 用tcpdump抓包眼见为实的验证手段命令和状态能告诉你连接没建立起来但不告诉你为什么没建立起来。抓包是终极验证手段它能回答一个本质问题SYN到底有没有发出去、有没有收到回应。抓取到目标IP的443端口直接握手包tcpdump -i eth0 -nn tcp port 443 and host 192.168.209.133 -c 10如果只看到本机发出的SYN没有SYNACK回来大概率是中间网络丢弃或对端防火墙静默丢弃。如果看到SYN后紧跟着RST说明对端端口关闭。如果完整看到SYN、SYNACK、ACK三次握手那么TCP层面完全正常问题一定出在更上层的TLS或HTTP协议。抓包还有一个附加价值你可以顺便验证TCP时间戳是否开启。在内核里net.ipv4.tcp_timestamps开启时包里的时间戳选项会有变化有些跨主机做TCP连接排查的场景会涉及到判断两侧时间戳参数是否一致Windows下对应的设置就是tcp global timestamps参数抓包能一眼看出这个选项的状态。不过这里提醒一点tcp_tw_recycle依赖时间戳的旧方案早已废弃千万别按老文章开启后面内核优化部分细说。5. 把连接监控做成日常运维的一部分5.1 监控指标和告警阈值该盯哪些连接状态的监控不应该仅靠出问题时手动敲命令生产环境需要用脚本定时采集。把下面的统计逻辑写成一个脚本每分钟执行一次配合zabbix或Prometheus推送就能形成基础监控。#!/bin/bash # 采集TCP各状态数量 ss -tan | awk NR1 {print $1} | sort | uniq -c /tmp/tcp_state_$(date %Y%m%d%H%M).log # 统计总数 ss -tan | grep -c ^ESTAB ss -tan | grep -c ^TIME-WAIT ss -tan | grep -c ^CLOSE-WAIT告警阈值怎么定我的经验是不要用固定绝对值而是看趋势和基线。比如CLOSE_WAIT平时都是个位数突然涨到50以上且持续不降这个触发告警比等它涨到几百更有效。SYN_RECV也是同理正常nginx的backlog几乎不会超过几十如果频繁冲到几百要么是半连接队列太小要么是异常流量打进来。ESTABLISHED数量的绝对值没有通用阈值一台4核8G的机器和一台32核64G的机器合理值可能差十倍。建议先连续采集一周画出自己的基线然后设超过基线的1.5倍持续5分钟这类动态告警比拍脑袋设一个5000更靠谱。5.2 内核参数优化能调但要明白为什么调很多人一听说TIME_WAIT多了就去改内核参数但改之前得先理解参数的作用和副作用。以下是我实际调过的几个参数列出来供参考net.ipv4.tcp_tw_reuse复用TIME_WAIT连接作为新连接使用对主动发起方有效。这个参数在较新内核4.12默认开启且行为更安全建议保持开启配合时间戳机制可以安全复用。但我强烈建议不要再折腾tcp_tw_recycle——这个老参数在NAT环境会造成严重的连接问题内核在后续版本中已经移除了它网上的旧资料还在推荐这是个经典坑。net.ipv4.ip_local_port_range本机主动发起连接时使用的临时端口范围。默认通常是32768-60999对外大量发起短连接时容易耗尽可以扩到1024-65535。改完用sysctl -p生效。net.core.somaxconn和net.ipv4.tcp_max_syn_backlog这两个参数控制的是半连接队列和全连接队列的长度。nginx或Java应用收到大量短并发时backlog满了会丢连接表现为客户端连接超时或者nginx报accept queue相关错误。调大时要配合应用自身的backlog配置比如nginx的listen指令后的backlog参数只调内核不改应用等于白调。net.ipv4.tcp_fin_timeout控制FIN_WAIT2状态的等待超时默认60秒。对主动大量短连接的场景调到30秒能加快状态回收副作用是极端情况下可能提前释放还在传输数据的连接不建议调的太低。改任何参数前先在非生产环境压测验证。我用过一个原则能用代码解决的问题不调内核能用调参解决的问题不加机器参数永远排在方案最后一位。5.3 常见故障速查表最后给一张生产环境常用的速查表按症状反查方便你直接抄答案症状可能原因排查命令处理方向报connection refused端口无监听或防火墙回RSTss -tlnp | grep 端口tcpdump确认RST启动应用、放行防火墙规则Docker报ports are not available端口已被占用ss -tlnp | grep 端口杀掉占用进程或换端口客户端报Cannot assign requested address本地临时端口耗尽ss -tan | tail看大量TIME-WAIT开tcp_tw_reuse、扩ip_local_port_range服务端大量CLOSE_WAIT应用未释放socketss -tanp | grep CLOSE-WAIT定位进程修复应用连接管理bug大量SYN_RECV堆积半连接队列满或攻击流量ss -tan state syn-recv | wc -l调大tcp_max_syn_backlog排查来源IP定时任务连接超时keepalive或防火墙空闲断开tcpdump看是否出现RST/重传应用层加断线重连机制负载均衡后端频繁掉线连接持续时间超长被回收ss -tan sport :8080看连接时长调大内核keepalive时间或应用层心跳5.4 一台高并发机器的mySQL连接排查实录再分享一个实际经历可能比表格更直观。前两年我维护的一套线上系统某天数据库监控报警连接数徘徊在高位一直不降。当时数据库连接池配置了最大300个连接但实际连接数一度涨到700多大量新请求排队等待获取连接。一步步来。先在应用服务器上确认是谁在连数据库ss -tanp | grep :3306 | awk {print $6} | sort | uniq -c | sort -rn | head结果发现是某个订单处理服务贡献了大部分连接。然后又看该服务的连接状态分布ss -tanp | grep :3306 | awk {print $1} | sort | uniq -c | sort -rnCLOSE_WAIT数量占了将近一半。到了这一步基本锁定了这个服务的数据库连接池没有正确回收连接。后续查看代码发现某个查询在超时异常时连接没有被释放而是丢失了引用导致连接池越挖越空。修复代码之后CLOSE_WAIT归零连接数回落到200左右。整个排查过程不到半小时核心就是两条ss命令加一次状态统计。TCP连接监控并不需要多高深的技术栈关键是形成异常状态-进程定位-代码确认这样一条清晰的排查链路遇到问题时不慌按链路一步步走。最后分享一个个人小习惯我在每台服务器上都会定期记录TCP状态的基线数据平时没事看一眼把自己机器的正常长相记在脑子里。这样哪天线上出问题只要扫一眼状态分布我就能立刻感觉到哪里不对——这个感觉不是天赋就是看得多了、对比得多了积累出来的。你也一样多用、多记、多复盘连接监控这件事会变得越来越顺。