接到这个标题的时候,我第一反应是想起了上周四凌晨两点的那次线上告警。业务方反馈文件上传接口大面积超时,监控面板上网络IO和磁盘IO双双飘红,当时在场的几个同事第一句话都是“到底是网络问题还是IO问题”。这个问题其实问错了方向——网络和IO在操作系统层面根本不是两条独立的线,而是一条链路上的不同截面。排了这么多年故障,我越来越确定一件事:搞懂网络与IO的排查,本质上是搞懂数据从网线到进程缓冲区这一段路上,到底能在哪里卡住。
这篇东西不是教科书式的命令大全,而是基于真实场景的排查思路拆解。我先讲清楚网络和IO为什么总被放在一起讨论,再给出我自己一直在用的分层排查路径,然后落三个高频率复现的实战案例,最后把排查套路沉淀成一张可复用的核对表。搞运维、做后端、写网关的,应该都能从中找到对号入座的东西。
1. 为什么网络和IO的问题从来不分家
1.1 调度链路视角下的同源同根
很多人在排障时习惯先给问题定性,比如“这是网络抖动”“这是磁盘瓶颈”。但我在实际处理中吃过太多次亏:某次排查数据库写入慢,抓包显示客户端发出请求后长时间收不到ACK,直觉认为是网络包丢失,结果最后定位到服务端网卡中断被CPU绑核绑错,软中断处理不过来了。再往前查,是磁盘阵列控制器驱动升级后回写策略变化,导致IO栈占用CPU时间片过多。这一整条链下来,你说它是网络问题还是IO问题?
从调度链路看就清楚了。数据从对端发过来,先经网卡DMA到Ring Buffer,网卡触发硬中断,内核软中断接手把数据从Ring Buffer拷贝到sk_buff,接着协议栈逐层解析,TCP层处理序列号和窗口,最终数据落到socket接收缓冲区。进程通过read()/recv()从缓冲区取走数据,如果业务需要落盘,还得走一遍PageCache、块设备层、磁盘驱动。这条链路上,网络栈占CPU、内存拷贝占CPU、磁盘队列占CPU,任何一环CPU调度出问题,都会同时表现为网络延迟升高和IO性能下降。
所以我现在排障的第一原则是:不预设问题归属,先看整条链路上哪个环节的消耗异常。这也是下面所有排查动作的底层逻辑。
1.2 性能指标之间的哑铃效应
还有个现象很误导人——网络和IO的指标经常呈现“哑铃效应”:指标好看的时候都好看,指标恶化的时候一起恶化。比如你看到TCP重传率飙升,第一反应是链路质量问题,但重传的原因也可能是对端接收窗口长期为零。窗口为什么为零?因为对端进程不消费数据。进程为什么不消费?因为它在等磁盘IO,而磁盘IO在等其他锁。这种连锁反应在微服务架构里尤其常见,调用链长、线程池互相等待,一个IO卡点能通过连接池扩散成整个网络的“假丢包”。
理解了这一点,再去看那些标准排查工具,思路就完全不一样了——你不再是为了“测一测网络通不通”而用ping,而是为了“确认链路哪一段出现了CPU或缓冲区瓶颈”而用ping。
2. 分层剥洋葱:每层该看什么、用什么看
我的排查路径是固定的五层:接入层、链路层、协议层、系统层、应用层。每层都有对应的核心工具和判断阈值,一层一层过滤下去,基本能把问题域压缩到很小的范围。
2.1 接入层与链路层:先打掉物理层的问号
先说接入层。检查网卡状态的基本操作是ethtool eth0,重点看Speed和Duplex两项。我遇到过不少“千兆网口协商成千兆半双工”的破事,表现就是文件传输极慢且伴随大量CRC错误。判断命令:
ethtool eth0 | grep -E "Speed|Duplex"如果发现半双工,firmware或交换机端口配置的概率很大,先别急着调服务端,直接换网线或换端口验证。
链路层的检查重点是丢包。ping -f做泛洪测试,观察Loss百分比,但如果服务器是云主机,Linux内核默认会限速ICMP,泛洪丢包未必代表真实链路故障,这时候得换iperf3做真实数据流测试:
iperf3 -c <server-ip> -t 30 -i 5 -P 4输出里重点关注retr列,也就是TCP重传次数。重传率超过0.5%就该警惕了。我一般在测完带宽后会顺手测一下小包性能:iperf3 -c <server-ip> -t 30 -l 128B。小包吞吐能暴露CPU处理协议栈的开销问题,如果小包PPS上不去但大包吞吐正常,问题基本在软中断或CPU调度,而不是物理链路。
2.2 协议层:抓包不能只看Flags
很多人在协议层一上来就tcpdump抓包,然后盯着一堆SYN、ACK看半天。我的建议是先做一次粗过滤的延迟分层,再决定要不要抓包。流程是这样的:
- 先
ping拿RTT基线,判断链路固有延迟是否异常。 - 再用
hping3或curl -w拿到TCP建连时间、TLS握手时间、首字节时间,拆解延迟到底花在哪一段。 - 确定疑点在TCP或TLS层之后,再上
tcpdump。
抓包时的关键不只是Flags,更要看Seq/ACK的变化趋势。比如我排查过一个“偶发性请求超时”的case,抓包看到大量 Dup ACK,第一反应是真实丢包,但打开时间戳一对比,发现Dup ACK之间的间隔异常整齐,而且序列号并没有实际跳变——这是对端接收缓冲区不足导致的窗口探测,不是链路丢包。处理方式是完全不同的:前者找网络团队,后者调应用读取速度和socket缓冲区。
tcpdump的实用姿势是保存成pcap文件后用Wireshark看stream图,不要用命令行硬看:
tcpdump -i eth0 -s 0 -w /tmp/capture.pcap host <server-ip> and port 80802.3 系统层:软中断与队列是最大的盲区
系统层的排查重点不是CPU空闲率,而是软中断分布和队列积压。
先看中断分布:
cat /proc/interrupts | grep eth0多队列网卡要确认每个队列的中断是不是被irqbalance均匀分配到不同CPU。我踩过一个坑:某台物理机上跑着Kafka和ES,网络IO占满后只有一个CPU核心的软中断冲到100%,其他核闲着,整个节点吞吐直接腰斩。后来干脆把Kafka实例绑在特定CPU上,并用taskset固定网卡队列中断亲和性,问题才解决。
再看队列积压,两个命令配合用:
ifconfig eth0 | grep -E "RX errors|RX dropped|RX overruns" netstat -s | grep -E "listen queue|SYN"如果RX dropped持续增长,通常是Ring Buffer太小或者软中断处理不过来。临时调大Ring Buffer可以这样做:
ethtool -G eth0 rx 4096 tx 4096注意这是运行时修改,重启后失效。要持久化得写进网卡配置文件或systemd unit。
2.4 应用层:连接池和线程池往往是“真凶”
很多网络故障排查到最后,网卡、链路、协议栈全是干净的,问题出在应用层的资源管理上。连接池耗尽、线程池阻塞、超时设置不合理,这三类问题在表现上和网络故障几乎无法区分。
遇到这类情况,我通常先看两条命令的输出:
ss -s ss -lntss -s能看到当前socket总数和TCP连接状态分布;ss -lnt看监听队列的Send-Q列,如果Send-Q的值超过backlog设置,说明有连接在排队等待accept,要么accept循环写得太慢,要么accept后处理逻辑阻塞了。
线程池和IO线程的状态用jstack(Java场景)或gdbattach(C/C++场景)看。这里有个容易忽略的点:线程Dump要看Blocked和Waiting线程的数量变化趋势,单次Dump说明不了问题,要间隔10秒连续Dump三次以上对比。
3. 实战案例复盘:三个高频故障的完整排查链路
3.1 案例一:文件上传超时,根因却在CPU软中断
现象:业务方反馈文件上传接口偶发超时,单次上传15MB文件,成功率只有80%。监控上看网络出入带宽都没到瓶颈,磁盘IO也不高。
第一步,先拆延迟。curl -w输出显示建连耗时正常,但time_starttransfer达到6到8秒,说明请求到了服务端但响应迟迟不出来。此时怀疑点从网络转到服务端处理。
第二步,查服务端CPU。top看整体负载不高,但按1看每个核心,发现CPU2软中断占比长期在45%以上。cat /proc/interrupts确认所有网卡队列中断都落在CPU2上。
第三步,查Ring Buffer。ethtool -S eth0 | grep rx_dropped,数值在持续上涨。到这里链路已经清楚了:网卡中断全挤在一个核上,软中断处理不过来,Ring Buffer溢出丢包,TCP层表现为重传和延迟上升。
处理方式:把网卡多队列中断分散到4个核心上,具体操作是关闭irqbalance的自动均衡,手动设置RPS(Receive Packet Steering):
echo f0 > /sys/class/net/eth0/queues/rx-0/rps_cpus这里的f0是CPU掩码,表示使用第5到第8个核心处理收包软中断。改完后再看rx_dropped,停止增长,上传超时率从20%降到0.3%。
这个案例的教训是:当监控显示“网络延迟升高”时,先别急着甩锅给链路,先查本机软中断是否成为瓶颈。软中断这种指标太容易被top的整体负载掩盖了。
3.2 案例二:IO性能明显下降,PageCache回写踩了坑
现象:一台数据库服务器在做全量备份后,业务查询延迟突增,iostat显示util超过90%,但数据量并没有明显增长。
第一步,识别IO类型。iostat -x 1查看%util和await,发现await从5ms涨到50ms,而svctm基本没变。await高但服务时间正常的典型含义是:IO请求在队列里等待的时间变长,不是磁盘本身变慢。
第二步,查队列。iostat -x 1里的avgqu-sz从1左右涨到20以上,说明有大量IO在排队。但顺着IO栈往下看,/proc/meminfo里的Dirty值非常低,倒是Writeback很高——这说明脏页在快速写回。再查/proc/vmstat里的pgwriteback飙升。
第三步,定位写回原因。全量备份脚本在凌晨触发了sync操作,而内核默认的dirty_expire_centisecs是3000(30秒),大量脏页在30秒内集中写回,形成IO尖峰。
处理方式分两步:一是修改备份脚本,备份前先用fadvise排除文件缓存,避免备份过程把业务热数据挤出PageCache;二是调低dirty_ratio和dirty_background_ratio,让脏页更早、更平缓地写回:
sysctl -w vm.dirty_ratio=10 sysctl -w vm.dirty_background_ratio=3这个案例最有价值的点在于:IO性能下降不一定意味着硬件要坏了,多数情况下是缓存策略和业务负载不匹配。改参数前先确认内核为什么在某个时间点开始大量写回,否则调参只是治标不治本。
3.3 案例三:WebSocket频繁断连,“stream disconnected before completion”的真相
现象:一个实时消息服务,客户端每隔几分钟就报stream disconnected before completion: failed to send websocket request,服务端日志无异常,TCP层看不出明显重传。
第一步,查服务端连接状态。ss -s显示TCP连接数正常,TIME_WAIT略高但没到问题级别。再看Nginx和业务服务之间的代理超时配置,发现proxy_read_timeout设的是60秒,而业务侧有心跳机制但心跳间隔是90秒,Nginx在60秒内没收到任何数据就直接断开了连接。
第二步,验证假设。把proxy_read_timeout调到120秒后,断连频率立刻下降80%,但仍有少量断连。继续看应用层日志,发现客户端在弱网环境下心跳发送本身也会超时,服务端收不到心跳就主动踢掉连接。
第三步,修复。正确的心跳方案是服务端在收到任何数据时重置idle计时器,而不是依赖固定时间间隔的主动探测。同时把Nginx的read timeout设成心跳间隔的3倍以上,给网络抖动留足缓冲。
这个案例和“IO性能下降”表面上看毫无关系,但本质上都是“某一层的机制和业务模型不匹配”。WebSocket断连大部分时候不是网络断了,而是协议栈里某一层的超时判断先于物理链路判断“连接死了”。
4. 排查工具箱与实践建议
4.1 常用命令速查
不同场景下,我最常依赖的命令组合基本固定下来了,整理成一张表方便快速定位:
| 排查目标 | 推荐命令 | 关键指标 |
|---|---|---|
| 网络延迟基线 | ping -f -c 1000 <ip> | loss%、rtt波动 |
| 带宽与重传 | iperf3 -c <ip> -P 4 -t 30 | retr列、Bandwidth |
| TCP连接状态 | ss -s && ss -lnt | Send-Q、TIME_WAIT数 |
| 网卡丢包 | ethtool -S eth0 | rx_dropped、rx_missed |
| 软中断分布 | cat /proc/interrupts | 对应网卡队列的CPU分布 |
| 磁盘IO队列 | iostat -x 1 | %util、await、avgqu-sz |
| PageCache回写 | `cat /proc/meminfo | grep -E "Dirty |
| TCP重传统计 | netstat -s | grep retrans | 重传次数增量 |
| 连接跟踪表 | cat /proc/sys/net/netfilter/nf_conntrack_count | 与max值对比 |
| 进程IO | pidstat -d 1 | 各进程IO读写速率 |
| socket缓冲区 | ss -mp | skmem中rmem/wmem占用 |
4.2 一次排障的完整动作顺序
如果是从零开始排查一个未知问题,我建议严格按下面的顺序走,不要跳步:
- 先确认影响面:影响单机还是集群?影响所有请求还是部分请求?这个信息决定了排查方向。
- 拉时间线:从监控上把网络延迟、TCP重传、磁盘util、CPU软中断四个指标叠在一张图上看,找到哪个指标先发生变化。
- 按“接入层 → 链路层 → 协议层 → 系统层 → 应用层”逐层向下,每层先用最简单命令确认正常再做深入排查。
- 任何一步发现异常,先记录现场(
ss -s、iostat、ethtool -S的输出保存下来),再动手调整。 - 调整一个变量,观察至少10分钟,确认有效再动下一个。
第4步特别重要。我见过太多次“改完配置问题暂时消失,但没人知道为什么消失”的案例。等下次复现,所有人又变成无头苍蝇。留现场这件事,排障时多花30秒,复盘时省3小时。
4.3 容易忽视的“第三层”问题
排障排得多了,会发现最难的往往不是技术参数,而是“机制选择”层面的问题。比如NIO和传统BIO在连接数过万时的表现差异,select的FD_SETSIZE限制,epoll的惊群问题,这些都不是靠调参数能解决的,而是架构设计时就该定好的。
遇到这类问题,我的建议是回头审视业务的技术选型是否匹配流量模型。一个长连接占比高、单连接数据量小的服务,和一个小连接占比高、单连接传输量大的服务,对网络事件模型的要求是截然不同的。前者用epoll LT模式就够了,后者可能需要ET模式来减少无效唤醒。这些概念单独拎出来都不难,但放在具体业务里,没踩过坑真的很难形成体感。
5. 把排查经验沉淀成SOP的几点心得
5.1 从“追着问题跑”到“照着单子查”
我早期排障完全是靠个人经验,遇到什么问题想什么问题。后来线上故障变多,多到我一个人cover不过来,才开始强制自己做SOP。现在团队内部有一套“三板斧”流程:第一板斧是固定时间窗口内的指标快照,第二板斧是常见原因的二分裁剪,第三板斧是变更记录联动。
以“IO性能明显下降”为例,三板斧的落地动作是这样的:先取10分钟内的iostat -x、pidstat -d、/proc/meminfo、dmesg -T四条命令输出;然后检查是否有备份、定时任务、或新版本发布等变更;最后才涉及具体的优化动作。这流程看着简单,但实际跑起来,至少帮我过滤掉了一半以上的“假故障”。
5.2 记录比修复更值钱
每次排障结束,我都会花15分钟把完整的排查链路写成一篇短记录,格式固定:现象 → 假设 → 验证 → 根因 → 修复 → 预防。这玩意单看一次似乎没什么用,但累计到几十篇之后,就会慢慢发现很多问题其实是同一类问题在不同业务侧的不同表现形式。
比如“网络超时”背后可能是软中断瓶颈、连接队列溢出、或者线程池耗尽,它们的共同点是都会在TCP层表现为RTO(重传超时)上涨。如果你只看TCP层指标,就会反复在“网络问题”里打转;但如果你有足够多的case记录,就能快速映射到其他几类可能性上。
5.3 工具只是放大器,不是银弹
最后说句实在话。很多人迷信工具,觉得Wireshark、perf、bpftrace一上,问题自动就出来了。但工具的粒度越细,噪声越大。没有前几层的粗过滤,直接上perf抓内核函数采样,大概率会被一堆无关符号淹没。
我现在的习惯是先用传统命令把问题压缩到一个明确的子系统里,再决定要不要上高级工具。这就跟你去医院看病一样,先量体温、验血常规,医生觉得有问题才让你做CT、核磁。一上来就做全身CT,既贵又找不准重点。
这个习惯形成之后,每次排障的节奏基本都很稳定。前10分钟定位到子系统,再花10分钟确认具体参数,最后留5分钟想清楚“为什么是这里”——三个10分钟之后,问题的答案通常已经摆在桌面上了。