网络与IO故障排查实战:从TCP重传到连接超时

网络与IO故障排查实战:从TCP重传到连接超时 排查网络与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适合快速看端口通不通而ncnetcat还可以用来做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 applyDNS解析异常导致域名访问卡顿但直接ping IP又是通的Windows局域网里“网络发现已关闭”导致共享文件不可见需要打开网络发现并启用网络和共享中心里的相关选项。排查这类问题我的建议是先看配置再看日志。ip addr看地址ip route看路由resolvectl status看DNSnetplan get看当前生效配置。很多“默认配置”第一次看可能觉得没问题但一旦和实际网络拓扑不符就会变成那种“看起来一切正常但就是不通”的悬案。3. IO问题排查实战找到谁在拖慢系统3.1 先搞懂几个IO指标吞吐、IOPS、延迟、利用率我遇到过不少同事一看到iostat打出来的%util是99%就喊着“磁盘满了”其实%util高不一定代表磁盘慢。机械硬盘在处理随机IO时%util接近100%确实意味着饱和但NVMe固态硬盘有很高的并行度%util即使到100%可能还有大量排队能力没有用完这时更应该看实际延迟和吞吐量是否达到预期。真正要关注的是这几个指标的组合r/s和w/s代表每秒读写次数IOPSrkB/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 traceread,write,openat,fsync 21 | head -100看进程在做哪些系统调用判断是否有频繁的write或fsync在刷盘。我有个习惯查IO问题一定会顺手跑一下vmstat 1直接看两个参数waCPU等待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 1wa基本在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 1wa字段高说明进程在等IO进程级IOpidstat -d 1看具体进程的读写速率实时IO排行iotop交互式查看哪个进程IO最高打开的文件与连接lsof -p PID看进程打开的fd和socket系统调用追踪strace -p PID -e traceread,write,fsync定位程序卡在哪个调用上磁盘调度器cat /sys/block/sda/queue/scheduler查看IO调度器确认是否需要调整6.3 画拓扑图和记录排障过程很多人忽略了一个非常实用的习惯给系统画一张网络拓扑图。不需要多专业用draw.io这类工具把客户端、负载均衡、业务服务器、数据库、外部依赖画清楚线上出问题时对着拓扑图走一遍链路能省下大量时间。我在排障时会随手把时间轴、命令、输出截图、结论记录到一份文档里这些记录就是以后遇到类似问题时的最宝贵参考。另外网络调试助手这类工具用来做TCP和UDP通信测试非常顺手适合验证本机和服务端之间的收发能力。做批量部署时同传软件在网络规划和分区规划合理的情况下能大幅提升效率但前提仍然是先把网络排查思路理顺否则同传过程中的卡顿和丢包也会被误判为软件问题。最后分享一点个人体会。干这行这么久我觉得排障最考验人的不是命令记得多熟而是心态和方法。遇到网络与IO问题先别急着怀疑“是不是代码写错了”或者“是不是要换机器”深吸一口气沿着链路一层层查下去每一步都用数据说话最终一定能找到那个真正的堵点。很多时候我们觉得某个问题诡异其实只是没有把网络和IO这两条线放到一起看而已。希望这一章的实战内容能让你下一次面对这类问题的时候心里更有底。