排查网络与IO问题,是每个后端开发者和运维都绕不开的硬仗。我做了十多年一线运维和架构,回头去看,线上那些最磨人的故障,十有七八都出在这两条路上——网络在悄悄丢包,或者磁盘、连接在某个瞬间堵住了。更麻烦的是,这两类问题经常缠在一起出现:应用层看到的是超时,抓包看到的是重传,系统指标里又是IO等待偏高,三者要对齐才知道问题到底卡在哪。
这一章,我把自己这些年处理网络与IO异常的经验,整理成一套可以直接照着用的排查方法。从最底层的原理讲起,逐步展开网络侧、IO侧的具体排查命令和案例,最后用一次完整的排障复盘把整套思路串起来。不管你是刚入行一两年的后端,还是已经被线上故障磨了许久的运维,这套方法都能直接搬过去用。我会尽量少说空洞的理论,多讲“你在终端里按下回车之后能看到什么、下一步该敲什么”这类实操细节。
1. 网络与IO为什么必须放在一起排查
1.1 网络传输的本质就是IO
很多人习惯把“网络问题”和“IO问题”当成两个独立领域,这是排障时最容易走弯路的地方。其实在网络通信协议栈里,socket就是一个文件描述符,你读写网络数据,本质上就是在做IO读写。只不过普通文件的数据写在磁盘扇区上,而socket的数据通过网卡发到对端,路径不同,底层的系统调用、内核缓冲、阻塞与非阻塞机制都是同一套。
理解了这一点,你就明白为什么一个Java服务在报WebSocket连接断开的时候,日志里会出现failed to send websocket request: io这样的错误——这个io不是指磁盘,而是指socket IO操作失败了。排查时如果不先建立这个认知,很容易被“明明是网络问题为什么会报IO错误”给绕进去。
我经常用一个很简单的类比来解释这种关系:网络和IO就像城市道路系统和物流仓库的关系,包裹(数据)既要在马路上跑(网络传输),又要在仓库里分拣暂存(IO缓冲)。马路堵了会影响包裹进出仓库,仓库叉车坏了也会让马路上排起长队。排障时不把两者放在一起看,你往往只能看到表面现象,抓不到真正的堵点。
1.2 一条数据在“网络+IO”链路里经历了什么
以一次最普通的HTTP接口调用为例,数据从客户端到服务端再回来,通常要经过这些环节:网卡接收中断触发驱动收包,数据从网卡DMA到内核缓冲区,协议栈做TCP/IP解析,然后数据被复制到socket接收队列,最后通过read系统调用进入应用程序的用户态缓冲区。服务端处理完后,响应数据再反向走一遍:用户态缓冲区 -> socket发送队列 -> 内核协议栈 -> 网卡DMA -> 物理链路。
任何一个环节出问题,表现到应用层可能就是同一个词——超时。网卡线缆老化会导致物理层丢包;协议栈缓冲区太小会导致TCP吞吐上不去;socket队列满了会导致read或write阻塞;应用进程内GC停顿会导致数据在用户态缓冲区里滞留。这就是为什么我排查问题从不先看代码逻辑,而是先沿着这条链路一层层排除。
1.3 我的三层定位法:先定性、再分层、后定位
这套方法听起来简单,但很多人执行不到位,一上来就抓包或改代码,最后查了半天发现是虚惊一场。我自己总结了三个步骤:先定性——判断问题到底是网络链路、IO性能还是应用逻辑导致的,这一步通常用几个系统指标和简单的连通性命令就能确定大方向;再分层——网络和IO各自都有很多层,比如网络有物理层、链路层、网络层、传输层、应用层,IO有用户态、内核态、设备层,要确定问题出在哪一层;后定位——缩小到具体进程、具体文件、具体连接,再用抓包或strace这类工具拿到铁证。
这套顺序在后面几节会反复用到。你只要记住一个原则:能用系统自带命令在两分钟内确认的事,就不要急着上抓包工具;能缩小到一个连接一个文件,就不要停留在“某某服务好像不太稳定”这种模糊结论上。
2. 网络问题排查实战:从网卡到应用协议逐层过
2.1 先分清现象:不通、卡顿、还是时断时续
网络问题的现象基本上就三类:完全不通、稳定卡顿、间歇性异常。我用一个简单方法来区分:先ping服务器的网关或内网地址,再ping公网地址,再telnet某个业务端口。
如果网关都ping不通,问题大概率在物理链路或网卡配置;如果内网通、公网卡,考虑路由和DNS;如果ping通但业务不通,问题在端口监听或防火墙;如果ping和业务都通,只是偶发超时,那就要重点排查TCP重传、连接复用和中间设备空闲超时。这三个方向用的命令、看的数据完全不一样,方向选错就会浪费大量时间。我自己就犯过好几次“一上来就抓包,结果抓了一小时发现问题根本不在这一层”的错误。
2.2 链路质量与带宽:ping、mtr、ethtool、iperf3的组合
排查有线网络卡顿或链路质量问题时,我习惯按这个顺序敲命令。
ping -i 0.2 目标IP,连续发几百个包,看丢包率和延迟抖动。如果只是延迟偶尔漂移,先记下来,不用太紧张。接着用mtr 目标IP看每一跳的延迟和丢包率,它可以快速定位是本地、运营商骨干还是对端的问题。注意有些节点丢包是ICMP限速导致的假象,需要多跑几次确认,不要一看某个节点丢包就觉得是那里坏了。
ethtool 网卡名同样值得一看。千兆网卡协商成百兆导致网速慢,是我见过频率最高的“低级”问题之一,网线老化、水晶头接触不良都可能导致协商降级。最后是iperf3 -s在服务端、iperf3 -c在客户端,用流量实测TCP/UDP带宽。这一步能排除“应用层看着慢,其实链路没问题”的情况。很多在线测速工具只是测浏览器下载速度,测出来慢不一定对,内网两台机器之间的问题,iperf3才是真正靠谱的手段。
2.3 端口与连接状态:ss、telnet、nc到底该怎么用
确认了链路没问题,下一步看端口和服务。ss -lntp可以列出所有监听端口和对应进程,我的经验是先确认端口真的在监听,再确认监听地址是0.0.0.0还是127.0.0.1——后者意味着只有本机自己能访问,外部自然连不上。
telnet和nc的区别在于:telnet适合快速看端口通不通,而nc(netcat)还可以用来做UDP测试、发送原始数据、做最简单的端口转发。UDP服务的排查用不了telnet,就得靠nc -uz 目标IP 端口这种写法。还有一点经常被忽略:不要只看“端口通”就觉得服务正常。TCP连接建立成功只能说明三次握手完成,应用层的健康状态需要用curl或业务自带的心跳接口去确认。
2.4 抓包与协议分析:从tcpdump到wireshark
如果端口、链路都正常,但业务依然异常,那就该上抓包工具了。tcpdump是我的首选,轻量、在服务端直接跑。最常用的两种写法:tcpdump -i eth0 host 目标IP and port 8080 -w /tmp/xxx.pcap,抓完用wireshark打开分析;如果只需要看握手和断开过程,tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0'可以过滤掉绝大部分数据包。
抓包最重要的不是看数据内容,而是看三类信号:TCP重传(Retransmission)、乱序(Out-of-Order)、零窗口(Zero Window)。重传多说明链路丢包或对端处理不过来,零窗口说明接收方缓冲区已满、应用层没及时读走数据。这两类信号正好对应网络和IO两个方向,拿到它们,问题归因就有了方向。
2.5 容易被忽略的配置类问题:从Ubuntu的netplan说起
有一类问题纯粹是配置错误导致的,现象千奇百怪。最典型的有三种:Ubuntu 18.04之后用netplan管理网络,改完网卡IP只执行service networking restart是没用的,必须netplan generate加netplan apply;DNS解析异常导致域名访问卡顿,但直接ping IP又是通的;Windows局域网里“网络发现已关闭”导致共享文件不可见,需要打开网络发现并启用网络和共享中心里的相关选项。
排查这类问题,我的建议是先看配置再看日志。ip addr看地址,ip route看路由,resolvectl status看DNS,netplan get看当前生效配置。很多“默认配置”第一次看可能觉得没问题,但一旦和实际网络拓扑不符,就会变成那种“看起来一切正常但就是不通”的悬案。
3. IO问题排查实战:找到谁在拖慢系统
3.1 先搞懂几个IO指标:吞吐、IOPS、延迟、利用率
我遇到过不少同事,一看到iostat打出来的%util是99%,就喊着“磁盘满了”,其实%util高不一定代表磁盘慢。机械硬盘在处理随机IO时,%util接近100%确实意味着饱和;但NVMe固态硬盘有很高的并行度,%util即使到100%,可能还有大量排队能力没有用完,这时更应该看实际延迟和吞吐量是否达到预期。
真正要关注的是这几个指标的组合:r/s和w/s代表每秒读写次数(IOPS),rkB/s和wkB/s代表吞吐量,await代表IO请求平均处理时间,%util代表设备繁忙程度。它们要配合着看,比如IOPS不高但await很高,可能是单个大块写入阻塞;IOPS很高且await也在涨,就是明显的磁盘压力。
3.2 三步定位IO瓶颈:iostat、pidstat、strace
第一次发现系统的IO性能明显下降时,别急着想着换硬件,按顺序做三步定位。
第一步,iostat -x 1 3,整体看是哪个设备有问题,确定是磁盘、网络还是其他块设备。第二步,pidstat -d 1定位到具体进程,看每个进程的读写IO情况。第三步,如果还定位不到,用strace -p 进程号 -e trace=read,write,openat,fsync 2>&1 | head -100,看进程在做哪些系统调用,判断是否有频繁的write或fsync在刷盘。
我有个习惯,查IO问题一定会顺手跑一下vmstat 1,直接看两个参数:wa(CPU等待IO的时间占比)和cs(上下文切换次数)。如果wa一直在20%以上,基本能确认IO是系统卡顿的元凶。
3.3 真实案例:日志刷盘把整个服务拖垮
之前处理过一个Java服务,运行一段时间后开始偶发超时,io指标很高。用pidstat定位后发现是一个日志框架线程在疯狂刷盘,原因是异步日志模式下,队列容量太小,产生日志的速度太快,背压导致刷盘线程长时间满负荷运行。加上磁盘本身是机械盘,随机写性能有限,整个IO路径就被拖住了。
解决办法并不复杂:把日志框架的异步队列调大,调整刷盘策略,对非关键日志降级,同时把日志和业务数据放到不同磁盘。改完之后,IO利用率立刻降下来,接口耗时也恢复正常。这个案例的启发性在于:应用层代码写得再快,如果底层IO路径规划不合理,一切都是白搭。
3.4 别忘了嵌入式与硬件场景的IO问题
网络与IO问题排查不止出现在服务器上。STM32开发里,PF0和PF1默认复用为JTAG功能,想当普通IO用必须先禁用JTAG,否则怎么初始化都是高阻态;FPGA的IO设计里,推挽、开漏、上拉的区别直接影响外部信号是否有效——开漏输出要外接上拉电阻才能输出高电平,很多人在这上面栽过跟头。
工业相机设备也常遇到IO触发问题。以海康相机IO拍照为例,接线必须区分PNP和NPN两种线型,接反了光耦不导通,软件里再怎么配置也不会触发拍照。我建议这类问题先拿万用表量一下IO电平,而不是反复改SDK参数。另外做PLC程序联调时,Factory IO这类虚拟仿真软件能省不少事,但仿真里IO映射和真实硬件的地址对应关系一定要核对清楚,地址映射错了,仿真跑得再顺,上现场一样乱。
4. 网络与IO交织的典型故障:连接异常断开
4.1 这类报错到底在说什么
很多开发都见过这种报错:stream disconnected before completion: failed to send websocket request: io error,或者stream disconnected before completion: io error: peer closed connection with。这种报错经常让人一头雾水,但其实拆开看就很简单。
peer closed connection with——对端主动关闭了连接。发送FIN包是正常关闭,发送RST包是异常关闭,两者含义完全不同,抓包一眼就能区分。failed to send websocket request——客户端在发送WebSocket请求时发现连接已经不可用。也就是说“客户端以为连接还活着,实际早就被对端或中间设备断掉了”。
这类故障的典型场景是:客户端维护了一个连接池或长连接复用机制,服务端或中间的负载均衡设备在空闲一段时间后主动断开了连接,而客户端没有及时感知,下次请求复用连接时才发现对方已经关了很久了。
4.2 用时间轴法定位连接断开问题
遇到这类问题,我最核心的排查手段是“时间轴对齐”:把应用日志、系统连接状态、抓包文件三者的时间戳放在一起对比。
具体操作是:先确认异常发生的时间点,在应用日志里找到对应的报错记录;再用ss -tnp | grep 端口查看当前连接状态,看有没有大量TIME_WAIT或CLOSE_WAIT;最后用tcpdump抓包,看断开时是收到FIN还是RST,以及最后一次数据交互到断开的间隔是多少。
有一个坑我要重点提示:负载均衡和云网关的空闲超时时间通常不会写在文档里,默认可能只有30到60秒。如果你的应用心跳间隔设置成90秒,那连接肯定会被中间设备掐断。这种情况抓包时甚至会看不到任何异常,因为FIN包是中间设备发的,服务端和客户端两边看起来都“很干净”。
4.3 从系统参数到应用层的修复思路
排查清楚之后,修复方向一般有三个。第一,在系统层调整TCP keepalive参数,缩短保活探测时间,比如把net.ipv4.tcp_keepalive_time从默认的7200秒调小,但这只能让本机更早发现连接失效,不能阻止中间设备断开。第二,在应用层加心跳机制,WebSocket协议本身就支持ping/pong帧,普通TCP连接则可以让客户端周期性发送轻量心跳数据,间隔要小于中间设备的空闲超时时间。第三,在客户端做连接失效快速重连,发请求前先确认连接可用,发现异常立即重建连接。
我个人更推荐应用层心跳加自动重连的组合,因为系统层的keepalive在很多云环境里不生效,中间设备不一定会转发保活探测包。
5. 一次完整排障复盘:接口偶发超时的根因
5.1 现场情况
有一次值班,线上一个核心接口的P99延迟从平时的50毫秒飙到800毫秒,并且伴随偶发超时。监控面板上,CPU、内存、带宽都没有明显异常,磁盘使用率也只是正常水平,但从监控图表能观察到IO util有周期性尖峰。
我先按照三层定位法做了初步判断:ping内网网关和外网地址都正常,ss查看业务端口还在监听,curl调用本地接口正常。于是我把问题方向初步定在网络链路和IO路径之间的某个环节,而不是业务代码逻辑。
5.2 逐层深入的过程
先跑vmstat 1,wa基本在0附近,排除磁盘IO阻塞。再跑ss -tnp统计连接数,发现TIME_WAIT连接数量很多,高峰期接近几十万。大量TIME_WAIT本身不是坏事,但结合客户端IP分布,我怀疑是连接复用策略有问题。
然后上tcpdump抓了五分钟包,很快看到大量TCP Retransmission,而且重传的报文都集中在几个和外部API服务交互的连接上,这些连接都使用了长连接池。进一步分析发现,这些连接的空闲时间超过了外部API服务所在网关的空闲超时阈值,连接被中间层静默关闭,客户端却还在复用旧连接。
5.3 根因是什么
问题其实是两个因素的叠加:外部API网关的空闲超时时间只有60秒,而我们的客户端心跳间隔设置成了120秒,超过超时时间的连接全部被网关断开;加上连接池里的连接没有及时清理和重建,新请求不断命中失效连接,触发TCP重传和重连风暴。重传本身消耗了额外网络带宽,同时很多线程在等待重连过程中阻塞,表现为接口延迟飙升。
这个根因里既有网络问题(连接被中间设备断开、重传),也有IO问题(线程阻塞、连接池等待),单独看任何一类数据都只能看到一半真相。
5.4 修复与验证
修复方案主要改了三处:把心跳间隔从120秒调整到45秒,确保比网关超时短;连接池增加空闲连接检测,失效连接直接剔除;客户端增加请求失败后的自动重连和重试机制,但重试需要加随机退避,防止同时重连打垮网关。
改动灰度上线后,TCP重传数量立刻下降,P99延迟在观察周期内稳定回到50毫秒以下。这次排障给我最大的感受是,连接类问题往往不是单一技术栈的问题,跨系统联调时要格外关注两边的超时和心跳参数是否匹配。
6. 一份随手可查的排障工具箱
6.1 网络排查常用命令速查
整理了一张表,适合贴在手边,出问题时直接对应查询:
| 排查目标 | 命令 | 说明 |
|---|---|---|
| 本机IP与网卡 | ip addr | 查看IP、掩码、MAC、网卡状态 |
| 路由与网关 | ip route | 确认默认网关,排查路由异常 |
| 连通性与延迟 | ping -i 0.2 目标IP | 连续ping,看丢包率和延迟抖动 |
| 链路每跳质量 | mtr 目标IP | 类似traceroute,显示每跳丢包 |
| 网卡速率 | ethtool 网卡名 | 看协商速率,排查百兆降级 |
| 带宽实测 | iperf3 -s/iperf3 -c | 内网带宽测试,比测速网站可靠 |
| 监听端口 | ss -lntp | 查看端口和对应进程 |
| 连接状态统计 | ss -s | 看TCP状态分布 |
| DNS解析 | dig 域名/nslookup 域名 | 排查DNS解析导致的卡顿 |
| 抓包 | tcpdump -i eth0 host IP and port 端口 -w file.pcap | 抓包后wireshark分析 |
6.2 IO排查常用命令速查
IO侧的命令同样整理成表:
| 排查目标 | 命令 | 说明 |
|---|---|---|
| 整体IO负载 | iostat -x 1 | 看设备util、await、IOPS、吞吐 |
| 系统负载与IO等待 | vmstat 1 | wa字段高说明进程在等IO |
| 进程级IO | pidstat -d 1 | 看具体进程的读写速率 |
| 实时IO排行 | iotop | 交互式查看哪个进程IO最高 |
| 打开的文件与连接 | lsof -p PID | 看进程打开的fd和socket |
| 系统调用追踪 | strace -p PID -e trace=read,write,fsync | 定位程序卡在哪个调用上 |
| 磁盘调度器 | cat /sys/block/sda/queue/scheduler | 查看IO调度器,确认是否需要调整 |
6.3 画拓扑图和记录排障过程
很多人忽略了一个非常实用的习惯:给系统画一张网络拓扑图。不需要多专业,用draw.io这类工具,把客户端、负载均衡、业务服务器、数据库、外部依赖画清楚,线上出问题时对着拓扑图走一遍链路,能省下大量时间。我在排障时会随手把时间轴、命令、输出截图、结论记录到一份文档里,这些记录就是以后遇到类似问题时的最宝贵参考。
另外,网络调试助手这类工具用来做TCP和UDP通信测试非常顺手,适合验证本机和服务端之间的收发能力。做批量部署时,同传软件在网络规划和分区规划合理的情况下能大幅提升效率,但前提仍然是先把网络排查思路理顺,否则同传过程中的卡顿和丢包也会被误判为软件问题。
最后分享一点个人体会。干这行这么久,我觉得排障最考验人的不是命令记得多熟,而是心态和方法。遇到网络与IO问题,先别急着怀疑“是不是代码写错了”或者“是不是要换机器”,深吸一口气,沿着链路一层层查下去,每一步都用数据说话,最终一定能找到那个真正的堵点。很多时候我们觉得某个问题诡异,其实只是没有把网络和IO这两条线放到一起看而已。希望这一章的实战内容,能让你下一次面对这类问题的时候,心里更有底。