网络与IO排查实战:从WebSocket断连到磁盘告警的完整链路分析
2026/9/16 4:21:51 网站建设 项目流程

日志里突然冒出一串错误,点开一看全是stream disconnected before completion: failed to send websocket request: io error: peer closed connection with,同一个时间段监控系统又给我发了一条“io性能明显下降了”的告警。这种场面我太熟了:网络和 IO 的问题从来不是孤立出现的,连接一断,写入日志的线程被拖住,磁盘 IO 跟着抖,最后所有指标都乱成一团。这一章我就把这几年做网络与 IO 问题排查的实战路径整理出来,从应用层到硬件层,从抓包到调参,给后端开发、运维和嵌入式调试的朋友一条能直接照着走的路。

1. 问题从哪里来:网络和IO“连体婴”的底层逻辑

1.1 一个真实的WebSocket断开故障

先说我最常碰到的一类现场。某服务依赖 WebSocket 接收实时消息,客户端在发起请求时报peer closed connection with,翻译过来就是对端主动关闭了 TCP 连接。很多人一看这行字就蒙了,觉得是网络不可靠,其实“对端关闭连接”这句话信息量很大:要么是对端进程崩溃或主动调用了 close,要么是中间设备(比如防火墙、负载均衡)觉得这条连接空闲太久,顺手把连接表项清掉了。

这个场景往往还会带出一堆 IO 异常。为什么?因为服务端通常用线程池处理连接,每个线程阻塞在 socket 读上。连接被对端断掉后,读操作抛异常,线程池快速消耗,新任务排队时间变长,里面再有写日志、读数据库的逻辑,整个进程的 IO 行为就变得很怪异。你只看监控,觉得磁盘 IO 突然飙升,实际上源头在连接状态。所以我一直建议,排查这类问题先看连接,再看 IO,不要一上来就折腾磁盘。

1.2 为什么先分层,再动手

网络和 IO 问题太庞杂,没有主线的话很容易被现象牵着走。我的习惯是把问题分成四层:链路层、网络层、传输层、应用层。链路层看网线、交换机指示灯、无线信号;网络层看 IP 连通性、路由、丢包;传输层看端口、TCP 状态、连接超时;应用层才看协议报文、业务逻辑、线程状态。

IO 问题也要分层:用户态应用在 read/write,内核态在处理页缓存、协议栈,硬件层在做 DMA、磁盘寻道、GPIO 电平翻转。同样是“IO 慢”,可能发生在任意一层。一个常见的错误是看到 iowait 高就认为是磁盘坏了,结果 strace 一挂,发现是进程在等网络 socket 数据,只是数据一直没来而已。

所以我做故障排查的第一步永远是先确认范围:是单个客户端连不上,还是整机服务不可用?是单条连接断开,还是所有连接都断?范围确定了,再一层一层往下钻,效率最高。这套思维在这一章里会反复用到。

2. 网络连接层排查:把“断连”的现场还原出来

2.1 最基本的连通性检查:Ping、路由和端口

网络问题排查,谁都会先 ping 一下,但 ping 通过不代表连接就能建立。防火墙可以丢弃 ICMP 却放行 TCP,所以真正可靠的三板斧是:ping看链路、mtr看路由、nctelnet看端口。

ping -c 10 192.168.1.100 mtr -rw 10 192.168.1.100 nc -vz 192.168.1.100 8080

mtr 比 traceroute 更好用,它会持续探测每一跳的丢包率。我遇到过不少情况是中间一跳路由器对 ICMP 限速,导致丢包显示很高,实际上 TCP 流量完全正常。所以 mtr 的结果不能盲信,要和实际业务连接结合判断。

端口这一层,nc -vz只要显示succeeded,就说明 TCP 三次握手成功了。如果失败,再回到本机看看端口有没有监听:

ss -tlnp | grep 8080 ss -tnp | grep 8080

ss比 netstat 快得多,-tnp能看到 TCP 连接状态和对应进程。排查断连问题,我最关注的是时刻表:连接处于ESTABLISHED的存活时间,还是大量TIME_WAIT,又或者是SYN_SENT一直发不出去。一种典型情况是服务器端有大量TIME_WAIT,这通常说明服务端主动断开连接很多,配合ss -s看统计,能快速判断是不是连接频率和端口复用配置的问题。

2.2 长连接为什么总是断:WebSocket、TCP保活与空闲超时

回到peer closed connection with这个错误。要查清是“对端主动断开”还是“网络设备静默丢弃”,光看日志不够,必须抓包。我通常这样抓:

tcpdump -i any -nn -s 0 port 8080 -w ws.pcap

抓完用 Wireshark 打开,过滤tcp.flags.reset == 1tcp.flags.fin == 1。如果看到对端直接回了 RST,那基本可以认定是主动断开;如果前期一直没有报文,后面另一端发了 FIN 或直接重传,那大概率是空闲超时。

空闲超时是最隐蔽的坑。WebSocket 本质上是 TCP 长连接,中间经过 Nginx 这类反向代理时,如果客户端不活跃,代理的proxy_read_timeout默认可能是 60 秒或 300 秒,超时就断。客户端自己不发心跳,或者心跳间隔比代理超时还长,就会周期性出现断连。

我的实践方案有两步。第一步,客户端 WebSocket 必须实现应用层心跳,也就是 ping/pong 帧,间隔建议不能超过服务端空闲超时的一半。第二步,如果服务端是自己写 socket,也要设置 TCP keepalive:

sysctl -w net.ipv4.tcp_keepalive_time=120 sysctl -w net.ipv4.tcp_keepalive_intvl=30 sysctl -w net.ipv4.tcp_keepalive_probes=3

但这只是内核兜底机制,应用层心跳的意义不只是保活,还包括及时感知掉线、触发重连。很多同学只做了一层,连接断了自己都不知道。

2.3 UDP通信“时通时不通”的排查方法

UDP 没有连接状态,问题比 TCP 更难啃。两台电脑用网络调试助手互发数据,经常出现 A 能收到 B 的,B 收不到 A 的。这种“单向通”的局面十有八九是防火墙。Windows 上要单独确认“入站规则”是否允许 UDP 端口,尤其是在“专用网络”和“公用网络”两个配置文件之间。

UDP 排查我习惯用nc -u配合抓包:

发送端:echo "hello" | nc -u 192.168.1.101 9999 监听端:nc -u -l 9999

如果监听端收到了,说明链路没问题。收不到,就用 tcpdump 看包有没有到网卡:

tcpdump -i any udp port 9999 -nn

如果 tcpdump 能看到包,但应用收不到,问题在应用层或系统防火墙;如果 tcpdump 完全看不到包,问题在链路或路由。还有一种情况是 MTU 过大,UDP 大包被分片,中间路由器丢弃分片,表现为小包能通、大包不通。这种时候把 MTU 从 1500 调到 1400 试试,或者强制应用层限制报文长度,通常就能解决。

3. IO性能排查:从告警“io性能明显下降了”开始

3.1 先回答一个思想问题:IO到底指什么

“IO 性能下降了”这句话能气死人,因为“IO”在不同人嘴里是完全不同的东西。数据库同学说的 IO 是磁盘读写;网络同学说的 IO 是网卡收发队列;Java 后端说的 IO 是 socket 读写和 NIO 事件循环;嵌入式同学说的 IO 是 GPIO 口输入输出。所以这句话出现在告警里,第一步一定是确认监控图表的指标来源。

我曾经被一个“IO 高”的告警折腾了半天,结果看到监控上标记的是io_wait,而这个值来自 CPU 空闲等待 IO 完成的比例。再往下查,发现是某进程在大量写日志到慢速磁盘,和网络没关系。反过来也有一次,网络系统显示eth0的每秒收包数暴涨,但吞吐很低,其实是小包攻击,CPU 全耗在中断上了。这两个场景都叫“IO 问题”,解法天差地别。

所以,我把“IO”至少分成三个层面:应用层(文件读写、socket 读写、标准输入输出)、系统层(页缓存、块设备 IO、内存映射)、硬件层(磁盘寻道、网卡 DMA、GPIO 翻转)。排查顺序永远是先确定问题落在哪个层,再决定用什么工具。

3.2 IO瓶颈定位:iostat、pidstat、strace三板斧

系统层 IO 最常用的命令组合,我习惯称为“三板斧”:

iostat -x 1 pidstat -d 1 strace -fp <pid> -e trace=read,write,futex,epoll_wait -ttT

iostat -x 1里我主要看几项:%utilawaitavgqu-sz。很多人以为%util100% 才是瓶颈,其实现在 NVMe 设备响应很快,瞬时大量 IO 会让队列堆积,%util反而不高,但avgqu-sz已经涨上天。这时候需要结合await(平均每次 IO 耗时)和svctm(设备服务时间)来看。await远大于svctm,说明 IO 在排队。

pidstat -d 1能定位到是哪个进程在读写:重点看kB_rd/skB_wr/s,再对应到进程名。如果找不到明显的高 IO 进程,但 iowait 很高,就用strace -fp挂在系统调用上。我曾经定位过一个诡异问题:进程在循环打开/proc下的文件,每次都能让系统产生大量页面缓存 IO,直接导致看似无缘无故的 IO 性能下降。

网络侧的系统指标我习惯用sar补充:

sar -n DEV 1 sar -n TCP,ETCP 1

TCP 的主动重传retrans/s如果长期不为零,说明网络链路有丢包,这时候应用层表现经常是“请求偶尔慢”。网卡收发包有错误的话,还要看rx dropsrx errors,很多服务器在跑满中断时,网卡驱动会丢包,这个在ifconfigip -s link里都能看到。

3.3 业务代码里的网络IO坑:BIO、NIO、连接池

Java 里 BIO 和 NIO 的区别能让很多人翻车。BIO 模型下一个 socket 连接通常占一个线程,线程池一旦不够用,后面请求全排队。我见过一个系统在峰值时连接数涨到 800,线程池 max 只有 500,结果所有请求的 RT 从 10ms 涨到 500ms,同时线程频繁切换,CPU 和 IO 双双恶化。

NIO 多路复用适合连接数多、单个连接活跃度低的场景。selectpollepoll三者的区别很简单:selectpoll每次都要把所有 fd 从用户态复制到内核态,epoll则靠事件驱动只返回有事件的那些 fd。连接数只有几十的时候无所谓,连接数上万,select的线性扫描就能把 CPU 吃光。

但别以为用了 NIO 就万事大吉。业务代码里最常见的坑是忘记设置 socket 超时。一个 HttpURLConnection 默认可能无限等待响应,一旦对端连接挂着,线程也会挂住。我这里给你一段最朴素的设置:

Socket socket = new Socket(); socket.connect(new InetSocketAddress("192.168.1.100", 8080), 3000); socket.setSoTimeout(5000);

超时时间也不是越大越好。设太短,慢业务可能正常但被误杀;设太长,故障时恢复慢。我的经验是内网调用超时 2~3 秒,外网 5~10 秒,再基于业务 P99 延迟留 2 倍以上余量。

连接池同样不可忽视。Java 的 HttpClient 连接池、数据库连接池、Redis 连接池,本质都是复用连接,减少握手开销。但连接池太小,请求会阻塞等待;连接池太大,空闲连接占用 fd,导致大量 TIME_WAIT。排查时我必看ss -s里的 TIME_WAIT 数量,如果超过几万,就要考虑net.ipv4.tcp_tw_reuse和连接空闲回收策略是不是需要调整。这里必须说明,单靠内核参数治标不治本,最核心的是让应用层正确管理连接生命周期。

4. 硬件与设备侧的IO排查:别忽略物理层

4.1 GPIO口配置:推挽、开漏、上下拉,为什么测出来电平不对

网络和 IO 的另一个主战场在嵌入式设备上。经常有朋友问:STM32 某引脚配成输入口,但外部给高电平时读到的还是 0;或者 ESP8266 扩展 IO 后电平不稳定。这类问题的根源往往不是网络协议,而是硬件 IO 配置错了。

先说推挽输出和开漏输出的区别。推挽输出能主动输出高电平和低电平,驱动能力强;开漏输出只能主动拉低,输出高电平要靠外部上拉电阻。如果在开漏输出模式下不加外部上拉,你拿万用表量到的“高电平”是悬空的,稍微有点干扰就会跳。这也是为什么很多工程师会把开漏接口和上拉电阻绑定记忆。

输入模式更要注意上下拉。外部电路断开时,引脚电平是浮动的,需要在代码里配置内部上拉或下拉,让默认状态确定。我用 STM32 的时候,第一件事就是反复核对数据手册,看引脚是不是和调试串口、JTAG 复用,像 STM32F0 系列 PF0/PF1 在部分封装上可能是 OSC 引脚,直接拿来当普通 IO 用之前要确认 Remap 和复用功能是否关干净。

排查 GPIO 问题时,我的顺序是:先查原理图和芯片手册,确认引脚功能;然后量硬件电平,确认外部电路的状态确实如预期;再查代码配置,看是在初始化里被复用功能覆盖了,还是在中断里被意外改档了。这三点都验证过,大概率能定位。

4.2 工业场景里的网络与IO地雷

工业现场的“网络与 IO”比办公室复杂得多,什么控制器双电源、网络防雷接口、RS485 接口、CC-Link 模块,我踩过的坑都能写一本书。先说 RS485,它是半双工总线,A、B 两端的终端电阻必须匹配,尤其是线缆超过几十米时,不加终端电阻会导致信号反射,表现就是通信时好时坏。排查时先用万用表量 A-B 之间的静态电压,正常应该在 0.2~0.5V 之间,低于这个范围基本就是总线没接好或终端电阻不对。

CC-Link 这类工业总线的 IO 地址映射也容易出问题。模块硬件组态里的起始地址和 PLC 程序里的软元件编号对不上,会直接导致 IO 数据错乱。我之前遇到过一次“网络正常但数据总是偏移”的奇葩问题,后来发现是组态配置文件里某个从站的站号和实际拨码开关不一致。所以调试工业网络,第一步就是确认拓扑:画一张网络拓扑图,标注每个节点的地址、拨码、连接线缆,比打开电脑瞎试高效得多。

另外一个容易被忽略的点是防雷和接地。网络上标配防雷接口不等于防雷无忧,接地通路不通,雷击浪涌依然可以通过网线耦合到设备里。现场排查如果看到设备外壳带电、通信偶发断连,首先要量接地电阻,再看防雷器的状态灯。这个问题在家庭或纯办公网络里很少见,但在厂区、户外设备中特别重要。

4.3 网络运维工具箱:我平时会随身带哪些命令和软件

下面这个表格是我在实际项目里反复用到的工具集合,按场景分类:

场景工具/命令常用姿势
连通性检查ping, mtr, traceroutemtr -rw 目标IP
端口和连接状态nc, telnet, ss, netstatnc -vz 目标IP 端口
抓包分析tcpdump, Wiresharktcpdump -i any port 80 -w out.pcap
带宽和延迟测量iperf3, pingiperf3 -c 服务器 -P 8
性能监控iostat, pidstat, sar, topiostat -x 1
系统调用跟踪strace, ltracestrace -fp 进程号
DNS 排查nslookup, dig, hostdig @8.8.8.8 域名(视网络环境而定)
网络发现arp, ip neigharp -a
硬件 IO 调试万用表、逻辑分析仪、示波器测电平、看时序

这不是让你背下来,而是遇到问题时知道该抄哪个家伙。比如“网络测速在线测网速”那种网页工具,适合日常体检,真要压测带宽,还是得上 iperf3,用多线程打满跑一轮。

5. 高频问题速查表:从症状到操作清单

我最后整理一个速查表,全是实际项目里反复出现的症状和对应的排查顺序,直接照着操作就行。

症状可能原因优先排查项
WebSocket 报 peer closed connection对端主动断开、空闲超时、中间设备清理连接抓包看 FIN/RST,检查心跳和代理超时
UDP 单方向收不到数据防火墙、NAT、广播域隔离先防火墙规则,再 tcpdump 看包是否到达网卡
网络 IO 高但吞吐很低小包过多、网卡中断不均、TCP 重传sar -n DEV, TCP看 pps 和重传率,调整网卡 RPS
磁盘 IO 性能明显下降磁盘故障、队列堆积、进程大量读写iostat -xpidstat -d,定位高 IO 进程
Java 线程全部阻塞连接池大小、socket 没设超时、BIO 模型线程 dump,检查waiting on monitor和 socket 读
Windows 提示网络发现已关闭网络发现功能、防火墙类型、工作组设置开启网络发现,确认防火墙是否允许入站“网络发现”
RS485 通信时好时坏接线、终端电阻、共地问题量 A-B 静态电压,检查接地
网卡大量 rx drops环形缓冲区太小、驱动中断过高ethtool -S查看丢包位置,调整rx-ring
大包不通小包正常MTU 过大,分片被丢弃两端统一 MTU,或应用层限制包长
端口不停出现 TIME_WAIT连接频繁建立/关闭,主动断开方在客户端开启长连接,调整连接池和 tcp_tw_reuse

最后再分享一个实战习惯:排查问题本身也是一个需要复盘的工作。我每次处理完复杂故障,都会把时间线、命令输出、根因和修复动作记到一个本地笔记里,下次遇到类似告警,先搜索自己的笔记。网络和 IO 这些东西,坑就那么几类,真正麻烦的是在慌乱中重复踩坑。按这一章的思路,先把问题分层,再从上往下查,很多所谓的疑难杂症其实都能用一套朴素的命令链顺下来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询