2026/10/26 13:15:01

网络协议抓包分析实战:TCP握手、DNS解析与故障排查

网络协议抓包分析实战:TCP握手、DNS解析与故障排查 1. 把“网络协议”这四个字拆开看很多人对网络协议的第一印象是面试前夜翻开的那本厚书满页的字段图、状态机、时序箭头背完就忘忘了再背。我在带新人的时候发现一个共性网络协议真正难的不是记字段而是不知道这些字段在什么场景下会咬人。同一个 RST 报文可能是对端端口没开也可能是中间设备主动掐断还可能是应用层自己调用了关闭同一个超时可能出在域名解析、可能出在握手、可能出在首字节返回。你不把协议当成一套“可观测的证据链”就永远只能靠猜。这篇内容想做的事情很具体把“深入解析网络协议”这个听起来很大的题目压缩成一条可以动手跑的路线。从抓包环境怎么搭、观测点怎么选到一次 TCP 连接的逐字段拆解再到应用层从域名到首字节的完整链路最后落到参数计算和故障排查。你会看到具体的过滤表达式、具体的参数量级、具体的排查顺序而不是“三次握手是为了建立连接”这种正确的废话。适合谁看写过一些服务端代码但对底层一知半解的后端同学、天天被“接口超时”追着跑的运维和测试、准备系统补课但不想再背一遍教材的开发者。网络协议不是一门需要背的课它是一门需要看的课——看得见才谈得上改得动。1.1 协议本质是一份多方签字的通信合同我更愿意把协议理解成一份合同而不是一份说明书。合同的每一方都要签字签完各自留一份任何一方违约另一方有权终止交易。TCP 的握手就是这个签字过程双方各自声明自己的初始序号确认对方的接收和发送能力都在线然后才开始传数据。字段就是合同条款序号是“我说到第几句了”窗口是“我这儿还能接多少”校验和是“这段话有没有被路上改过”。你把这层映射建立起来后面所有分析都顺了。合同这个比喻能解释很多反直觉的现象。比如为什么 SYN 报文不携带数据却要消耗一个序号因为“我要开始说话了”这句话本身也是一句话也要占一个编号否则对方没法确认你到底说到哪。再比如为什么主动关闭方最后要进 TIME_WAIT 等一段时间因为合同终止后还得留个缓冲期万一对方没收到“我同意终止”的确认会重发一次终止请求这时候你还在场可以再答一遍你要是一拍屁股就走了对方就只能干等到超时。这些设计在教材里是“机制”在实际排障里就是“为什么我的端口被占着不放”的答案。还有一层常被忽略合同是有层级的。链路层管一根线到另一根线网络层管跨网段寻址传输层管端到端的可靠与复用应用层管业务语义。每一层只跟对端的同一层签合同中间设备只负责拆包看地址再转发不关心内容。这个约定让整条链路可以分段演进——你换了一台交换机上面的协议不用动你把 HTTP 换成 HTTP/2下面的 TCP 也不用动。理解这一点你在排查时就知道该在哪一层找证据而不是从物理线缆一路猜到业务代码。1.2 分层不是学术洁癖是排障时的定位坐标我见过太多人排查问题的顺序是反的先怀疑业务代码翻半天日志没发现异常再怀疑数据库最后才想起来看一眼连接状态。这套顺序的问题在于它没有坐标。分层模型最大的实用价值就是给你一组坐标让你能用极低的成本把问题范围缩掉一半以上。一次请求失败你先问三个问题域名解析通不通TCP 连没连上首字节有没有回来三个问题的答案组合起来基本就能锁定是哪一层出了问题。举个具体的判断逻辑。如果连域名都解析不出来那跟 TCP、HTTP 一点关系都没有问题在解析链路或本地缓存如果域名解析很快但连接阶段就卡住了说明包发出去了没有回应你得看是对方没监听、还是中间设备把 SYN 丢了如果连接建立成功但首字节迟迟不来说明服务端在应用层处理慢这时候去看栈回溯比看网络包有用得多。每一步用不同的证据源不要混着看这是我在实际工作里最受益的一个习惯。分层还有一个副作用就是它会诱导人过度抽象。有些人一上来就谈“四层负载”“七层转发”听着很唬人但真让他说清楚四层转发依据的是报文的哪几个字段就说不上来了。我的建议是抽象可以留着跟人沟通时用动手时必须落回字段。你说这是四层转发那它一定是根据 IP 和端口做的决策不看 HTTP 首部你说这是七层转发那它一定重组了应用层报文能读到 Host 或路径。能落到字段的判断才叫判断落不回去的就是话术。1.3 学习路径的取舍先跑通一条链路再回头补细节关于怎么学市面上有两种极端。一种是先把整本书啃完再动手结果啃到拥塞控制就卡住了因为没见过真实的窗口变化完全靠想象另一种是直接上手抓包看到一堆报文就懵了不知道从哪个开始看。我倾向的路径是中间那条先选一条最短的链路跑通并且看懂再以这条链路为骨架往上挂细节。最短的链路是什么我建议是一次纯 HTTP 的请求不加 TLS。域名解析、TCP 握手、HTTP 请求响应、四次挥手四段全齐报文总量小字段也少。你把这一次请求的每个报文都看明白再去看 HTTPS只是中间多了一段加密协商再去看 HTTP/2只是应用层从文本变成了二进制分帧再去看长连接和连接池只是把握手和挥手从每次请求里挪出去了。骨架搭好了后面全是挂件。我自己是这么走过来的也确实踩过弯路。最早我想一步到位直接抓一个 HTTPS 请求结果满屏都是加密载荷除了握手阶段什么都看不见白白浪费了一周。后来退回去抓 HTTP一天就看明白了再回头看 TLS 握手那些随机数、套件、扩展字段的意义一下就清楚了。所以我给新人的建议永远是别急着上难度先把一条链路看到能背下来。看到什么程度算够合上抓包文件你能凭记忆把这次请求的报文顺序和每个报文的关键字段复述一遍就够了。2. 观测环境抓包之前先想清楚看哪一层抓包这件事工具只占三成剩下七成是“在哪儿抓”和“抓什么”。我见过不少人一上来就装个图形化工具点开始刷出一屏报文然后就不知道干什么了。问题不在工具在于没有预设问题。你抓包之前心里得有一个待验证的假设比如“我怀疑对端没回 SYN-ACK”或者“我怀疑响应体大到触发了分片”。没有假设的抓包就是拍照有假设的抓包才是实验。另外一个常见误区是把抓包当成万能钥匙。加密流量抓下来应用层内容看不见容器环境里抓包可能抓到的是虚拟网卡的镜像而不是真实流量开启了硬件卸载的网卡抓到的包可能缺少校验和甚至顺序错乱。这些都是环境本身的特性不是工具坏了。所以这一节我想先把观测点的选择和准备工作讲透再谈过滤表达式顺序不能反。2.1 三个观测点的取舍与对比常见的观测点有三个请求发起方本机、中间转发节点、服务提供方本机。三个位置看到的景象完全不同选错了位置你会得出完全相反的结论。观测点能看到什么看不到什么适用场景发起方本机应用发出的原始报文、本地重传、连接状态、解析耗时报文在途中是否被改写或丢弃判断是本地问题还是远端问题中间转发节点双向流量、丢包与重传、转发延迟应用层真实意图加密时尤甚定位链路质量与转发策略服务方本机请求实际到达的样子、处理耗时、响应发出时间请求在路上经历了什么判断是不是服务端处理慢我一般的做法是两端同时抓然后用时间戳对齐。为什么要两端因为单点抓包永远有一个盲区你在发起方看到“我发了但没收到回应”你没法判断是对端没发还是对端发了但路上丢了。两端一对比答案立刻出来——如果服务端抓到了请求且发出了响应那问题就在回程如果服务端压根没抓到请求那问题在去程或者地址压根不对。这一个动作能省掉大量扯皮。还有一个细节值得说容器和虚拟化环境里抓包的位置可能不是你想象的那个位置。应用跑在容器里你在宿主机上抓物理网卡中间隔着网桥和虚拟网卡看到的可能是经过地址转换之后的报文源地址已经变了。这种时候抓包结论会误导你让你以为应用在用某个地址通信实际上那是转发层改写后的结果。稳妥的做法是先确认网络命名空间进到应用所在的命名空间里抓或者明确知道自己抓的是哪一段。2.2 过滤表达式的写法与常见误判过滤表达式是抓包效率和准确性的分水岭。不过滤的话一台忙一点的机器一分钟能刷出几十万条报文你根本没法看。写过滤表达式的原则是先用最粗的条件收窄再用最细的条件定位。抓取阶段的过滤在内核里生效的那种语法比较底层粒度粗典型写法是这样# 只保留与某台主机 443 端口的往来报文且每个报文只截前 96 字节 抓包工具 -i any -s 96 -w cap.pcap host 203.0.113.10 and tcp port 443 # 抓所有非本机网段的 80 端口流量用于排查异常的外发请求 抓包工具 -i eth0 -s 128 -w cap80.pcap tcp port 80 and not net 192.0.2.0/24-s 96这个截断长度是有讲究的。多数情况下你只需要看首部和一点点载荷96 字节足够覆盖以太网头、IP 头、TCP 头以及一部分选项抓全包会让文件体积涨十几倍回放和分析都变慢。真要还原载荷再调大这是按需取舍不是越全越好。分析阶段的过滤就精细多了可以按标志位、按序号、按相对时间。几个我高频使用的条件tcp.flags.syn 1 tcp.flags.ack 0只看握手第一个报文一眼看出谁在发起连接。tcp.analysis.retransmission只看重传判断链路是否在丢包。tcp.analysis.zero_window只看零窗口通告判断接收方是不是被应用层拖住了。tcp.flags.reset 1只看重置报文快速定位被拒绝的连接。误判的重灾区在“重传”这个判断上。工具标记为重传的报文不一定是真的丢了也可能是乱序到达被误判或者是抓包点在转发路径上同一个报文被镜像了两份。判断真假重传有个笨办法但很有效看重传报文的序号是否已经被对端确认过。如果对端已经确认了这个序号那说明是重复投递链路本身没问题如果对端一直没确认才是真丢。这个区别直接决定了你该去查链路还是查应用。另一个常见误判是时间戳。不同观测点抓的文件时间基准可能差几十毫秒甚至更多尤其是跨机器的时候。直接拿两边的绝对时间对比很容易得出“响应延迟 300 毫秒”的结论实际上可能只是时钟没对齐。我习惯先找同一个报文在两个文件里的时间戳算出差值当偏移量把一边平移过去再比对。这一步不做后面所有延迟分析都是错的。2.3 抓包文件之外的准备工作有几件事必须在按下开始之前做完否则抓出来的数据可能白抓。第一件是确认抓包权限和落盘位置。抓包需要较高权限但落盘目录的剩余空间同样关键。一个跑了半天的全量抓包文件几个 GB 是常事写满磁盘会连带影响同一台机器上的服务这个后果比抓不到包严重得多。我的做法是给抓包文件设一个大小上限和滚动份数比如单文件 100 MB、最多保留 5 个这样既能覆盖一个时间窗口又不会把盘撑爆。第二件是记下当时的环境参数。这一条被严重低估。你在三天后回看抓包文件如果没有记录当时的 MTU、RTT、并发连接数、服务端配置很多现象根本解释不了。我现在会在一开始就写一个简单的记录抓包起止时间、涉及的地址与端口、当时的典型响应耗时、有没有做过发布或扩容。这些小纸条比抓包文件本身还值钱。第三件是准备一个对照组。如果条件允许抓一次正常的请求作为对照再抓一次异常的。正常人可能觉得这是浪费时间实际上对照组能帮你排除掉九成的干扰因素——比如你发现两边都有同样的重传那重传就不是问题所在。我在排查一个偶发超时的时候正是靠对照组发现异常请求和正常请求的报文序列完全一致最终把怀疑从网络转向了服务端的线程池排队。3. 一次TCP连接的逐字段拆解到了核心部分。TCP 是绝大多数应用协议的承载把它彻底看明白剩下的基本是应用层语义问题。这一节我会把三次握手、数据传输、四次挥手串成一条线重点讲“这个字段为什么这样设计”而不是罗列位宽。3.1 三次握手每个字段都在回答一个问题先看首部里几个必须记住的字段。源端口和目的端口各 16 位这是复用与寻址的基础一台机器上万个连接靠这四元组区分。序号 32 位标记本报文数据在字节流中的位置。确认号 32 位表示“我期待你下一次从这一位开始发”。窗口 16 位表示接收方当前还能接收多少字节。标志位里最常用的是 SYN、ACK、FIN、RST 四个分别对应建连、确认、关闭、重置。选项字段则是各种扩展MSS、窗口扩大因子、选择确认、时间戳都在这里。三次握手的报文序列用相对序号写出来是这样次序方向标志序号确认号关键选项1客户端 → 服务端SYNseqx无MSS1460、窗口扩大因子、SACK 允许2服务端 → 客户端SYNACKseqyackx1MSS1460、窗口扩大因子3客户端 → 服务端ACKseqx1acky1时间戳等为什么要三次两次不够吗核心原因有两个。一是双方都要确认对方的收发能力第一个报文证明客户端能发第二个报文证明服务端能收能发第三个报文证明客户端能收。少一次总有一方的某个能力没被验证。二是防止历史连接的重复请求造成资源占用如果客户端一个迟到的旧 SYN 到达服务端两次握手的话服务端就直接建连等着了白占一份资源三次的话客户端发现确认号对不上会回一个 RST 把它掐掉。这个场景在真实的弱网环境里并不罕见。注意序列号的起点是随机化的不是从 0 开始。这不是为了好玩是为了防止报文被伪造和串号——如果起点可预测攻击者就能构造出看起来合法的报文插进流里。同样第一个 SYN 虽然不带数据但它消耗一个序号所以对端的确认号是 x1 而不是 x。这个“1”在抓包里是判断握手是否正常的第一个依据很多分析就是从“为什么确认号差了一位”开始的。握手阶段还有个实操中经常被忽略的点选项是在 SYN 里协商的。MSS、窗口扩大因子、SACK、时间戳这些能力必须在握手的前两个报文里交换第三个报文之后就不能再谈了。所以你如果在抓包里发现窗口很小、吞吐上不去第一件事是回头看握手报文里到底协商了什么。如果窗口扩大因子是 0那双方的窗口最大只能到 65535 字节物理上就限制了吞吐上限后面怎么调应用参数都没用。3.2 序列号与窗口可靠传输的两本账握手完成后进入数据传输。这一阶段所有现象基本都能用两本账解释序号账和窗口账。序号账管的是“有没有漏”。发送方给每个字节编号接收方按序确认。发送方发出数据后启动一个重传定时器超时没收到确认就重发。这个超时时间不是拍脑袋定的是算出来的先测往返时延的平滑值再加上时延波动幅度的四倍作为余量得到一个动态的重传超时。时延抖动大的链路超时时间会自动放宽所以你在弱网里会看到重传间隔越来越长这是设计行为不是故障。窗口账管的是“发多快”。这里有两个窗口容易混。一个是接收窗口由接收方在报文里通告表示“我这儿缓冲区还剩多少空间”这是怕你发太快把它撑爆。另一个是拥塞窗口由发送方自己维护表示“我估摸着网络能承受多少”这是怕你发太快把路撑爆。实际能发出去的量取两者较小值。这个设计解耦了两件事接收方关心自己的内存发送方关心网络的承载能力各管各的互不干扰。拥塞窗口怎么涨是理解吞吐曲线的关键。慢启动阶段每收到一批确认窗口就翻一倍指数增长所以曲线一开始很陡涨到某个阈值后转为拥塞避免每个往返只加一个最大报文段线性增长曲线就平了。一旦检测到丢包窗口立刻减半甚至回到起点曲线出现锯齿。你去看任何一条真实的传输速率曲线几乎都是锯齿状往上爬这不是质量问题这是协议在用丢包当拥塞信号。这里有个经典的计算能把窗口和吞吐的关系说透。假设往返时延 40 毫秒接收窗口 64 KB那么理论最大吞吐是64 × 1024 × 8 ÷ 0.04 ≈ 13.1 Mbps也就是说哪怕你的带宽是 1 Gbps只要窗口只有 64 KB、时延是 40 毫秒你能跑到的上限就是 13 Mbps 左右。这就是为什么长肥管道必须开窗口扩大因子——把窗口从 16 位扩展到 30 位才能让窗口随带宽时延积一起长大。看到“带宽很足但吞吐上不去”先算这个数比查任何配置都快。3.3 四次挥手与TIME_WAIT的真实代价关闭连接需要四次因为 TCP 是全双工的两个方向要分别关。主动关闭方发 FIN表示“我这边的数据发完了”对方回 ACK但可能还有数据要发所以不能立刻也发 FIN等对方数据发完再发一个 FIN主动方回 ACK连接才算完整关闭。这个中间状态叫半关闭很多时候应用层面的“连接还在但已经写不进去了”就是走到了这一步。真正让人头疼的是 TIME_WAIT。主动关闭方在发完最后一个 ACK 之后不能立刻释放连接要等一段时间常见实现里这个时间是 60 秒左右而且是内核里写死的常量改配置文件改不动。为什么必须等两个原因一是万一最后那个 ACK 丢了对方会重发 FIN你还在场可以再答一次二是让这条连接上的旧报文在网络中自然消亡避免它们串到之后复用的同四元组连接里去。代价是什么**每个 TIME_WAIT 状态的连接都占用一个本地端口而本地端口总共就六万多个。**如果一个服务每秒主动关闭几百个连接一分钟就能攒下上万个 TIME_WAIT端口就开始紧张了。这时候你会发现新建连接报错说地址已被占用但你去查监听端口明明是好的——因为占用的不是监听端口是临时端口。处理这个问题的思路按优先级排我建议这样优先改架构用连接复用把“每请求一连接”变成“长连接池”这是治本端口消耗直接下降一到两个数量级。让服务端主动关闭把 TIME_WAIT 的负担从客户端挪到服务端因为服务端端口是固定的不消耗临时端口。这个调整在多服务互调的场景里效果明显。谨慎调整内核参数比如允许复用处于 TIME_WAIT 的连接用于新建这个参数有前提条件不是万能药盲目打开在某些场景下会引入新问题。不要试图把等待时间改成 0这不是优化是把协议的容错机制拆了。注意看到大量 TIME_WAIT 不代表有故障它是正常的协议状态。只有当它挤占了可用端口、导致新建连接失败时才需要处理。判断标准不是数量而是“端口是否耗尽”。4. 应用层从域名到首字节的完整链路传输层看懂了往上是应用层语义往下是寻址。这一节按真实请求的时间顺序走一遍先解析域名再建连再发请求最后收响应。每一段都有一个典型的耗时指标也都有一类典型的故障。4.1 DNS解析的层级、缓存与超时域名解析的本质是把一个好记的名字换成地址。这个过程是分层递归的本地先查缓存缓存没有就问本地配置的解析服务解析服务如果也没有就从上往下逐级问直到拿到权威结果。每一级都有缓存每一级也都有超时。常见的记录类型要分清A 记录给地址AAAA 记录给另一种地址格式CNAME 是别名指向另一个名字MX 是邮件路由NS 是授权信息TXT 是文本备注。排查时最常遇到的是 CNAME 链路过长——一个名字指向另一个名字另一个又指向第三个每一跳都要多一次查询累计耗时可能从几毫秒涨到几百毫秒。这种问题在抓包里看得很清楚一串查询连着出去每个都要等回应。缓存时间是这条链路上最容易踩的坑。记录里带的存活时间决定了缓存多久设得太短每次请求都要重新查解析服务的压力上来了延迟也上来了设得太长改完配置要等很久才生效做灰度发布的时候特别难受。我的经验值是稳定的记录给小时级做发布切换的记录提前调短到分钟级切完观察一段时间再调回去。这个操作顺序别搞反先调短再改不是改完再调短。还有一个细节域名解析默认走无连接的传输方式报文有长度限制结果太大的时候会返回一个截断标志让客户端改用有连接的方式重查。所以你如果看到“解析偶尔特别慢”有可能是结果集太大触发了二次查询。这种情况在记录条数多、或者带了较多扩展信息的场景下会出现值得在解析耗时异常时留意一下。4.2 HTTP报文结构与容易忽略的首部HTTP 报文的结构很朴素起始行、首部、空行、正文。请求的起始行是方法、路径、版本响应的是版本、状态码、原因短语。就这么简单但坑全在首部里。几个必须理解的首部Host同一台机器上多个站点靠它区分。没有它服务端不知道该返回哪个站点的内容。Connection控制连接是否复用。早期的默认是关闭后来的默认是保持。这个默认值的变迁直接导致了大量“连接数为什么这么多”的困惑。Content-Length 与 Transfer-Encoding二选一不能同时用。后者用于长度未知时的分块传输每一块自带长度最后用一个空块结束。这两个同时出现是经典的安全问题源头解析器如果处理不一致就可能被利用。Cache-Control 与 ETag控制缓存与校验。前者说“能存多久”后者说“内容变了没有”。两者配合能把大量请求挡在服务端之外。Accept-Encoding声明支持的压缩方式。这个字段缺失会导致响应体积翻几倍在移动网络下影响非常明显。状态码的语义也有必要较真。2xx 是成功3xx 是重定向4xx 是客户端问题5xx 是服务端问题。但真实世界里会出现“200 里包着一个错误”的情况那是业务层自己封装的跟协议无关排查时别被误导。反过来有些中间设备会在不通知客户端的情况下改写状态码这时候抓包对比两端看到的响应就能发现问题出在中间。4.3 用一条命令把链路串起来排查请求慢最有效的一步是把总耗时拆成几段。命令行客户端通常都支持输出分段耗时我把这段脚本当作日常第一招curl -o /dev/null -s \ -w dns%{time_namelookup} tcp%{time_connect} tls%{time_appconnect} first%{time_starttransfer} total%{time_total}\n \ http://example.com/输出里几个时间点的关系是嵌套的理解它们就能直接定位指标含义相邻差值代表什么time_namelookup从开始到解析完成纯解析耗时time_connect从开始到建连完成减去上一项 握手耗时time_appconnect从开始到加密协商完成减去上一项 协商耗时time_starttransfer从开始到收到首字节减去上一项 服务端处理耗时time_total总耗时减去上一项 内容传输耗时这套拆解的价值在于它把“慢”这个模糊感受变成了五个可以分别优化的数字。如果解析占了 80%你去优化服务端代码毫无意义如果握手占了大部分说明网络往返次数太多该考虑复用连接如果首字节慢那才是应用层的问题如果首字节快但总时长很长那多半是响应体太大或者带宽受限。我拿这套方法定位过一个很隐蔽的问题某个接口在测试环境一直很快上线后偶尔慢到十几秒。分段一看解析和建连都正常首字节也正常慢在最后一段传输。顺着这个方向查下去发现响应体达到几百 KB而中间某一跳的设备对这条链路做了限速。如果没有分段这个结论根本推不出来。5. 参数计算与调优把协议知识落到配置上协议知识如果只停留在“能看懂抓包”价值有限。真正的分水岭是能不能把协议原理换算成配置参数。这一节讲三个最常调、也最容易调错的地方。5.1 带宽时延积与窗口的故事前面提过一次计算这里展开讲因为它是最划算的一个知识点。带宽时延积的意思是在一条链路上从你发出第一个字节到收到第一个确认这段时间里链路上能容纳多少数据。这个量等于带宽乘以往返时延。以 1 Gbps 带宽、30 毫秒时延为例1 Gbps 换算成字节是每秒 125 MB乘以 0.03 秒得到约 3.75 MB。也就是说要让这条链路跑满发送方手里必须有接近 3.75 MB 的“在途数据”。如果窗口只有 64 KB那你能跑到的吞吐就是64 KB ÷ 0.03 s ≈ 2.1 MB/s ≈ 17 Mbps不到标称带宽的百分之二。这就是为什么跨地域传输的文件拷贝经常慢得让人怀疑人生——不是带宽不够是窗口太小链路大部分时间是空的。对应的配置手段是开窗口扩大因子。它在握手时协商值的含义是窗口字段左移的位数最大 14 位也就是窗口最大可以到约 1 GB 量级。这里要注意协商是双方的取较小值。如果一端支持一端不支持最终还是回到 64 KB。所以你在抓包里看到窗口一直很小先确认握手报文里双方到底谈成了什么别急着改配置。还有一层是拥塞窗口的初始值。早期实现从很小的值开始慢启动短连接根本来不及长大就结束了白白浪费带宽。后来标准把初始窗口提到了 10 个最大报文段左右短连接的起步速度明显好转。这个改动看起来不起眼但在大量短连接的场景下累积效果非常可观。5.2 MSS、MTU与分片最大报文段和最大传输单元这两个概念经常被混着说。简单区分最大传输单元是链路层能承载的单帧上限以太网常见是 1500 字节最大报文段是 TCP 层能承载的单段上限等于前者减去两个首部的长度。算一下1500 − 20IP 首部− 20TCP 首部 1460 字节。这就是为什么大多数以太网环境的 MSS 是 1460。这个数字为什么重要因为一旦某个报文的长度超过路径上某一段的承载上限就要分片。分片本身不是问题问题是分片之后只要丢一片整个报文都要重传效率直接掉一半以上。更麻烦的是一种隐蔽故障某些链路的传输单元比两端协商的小但中间设备把“需要分片”的通知给丢了结果就是小包正常、大包卡死表现为“能连上、能发小请求、发大数据就超时”。排查这类问题的思路是逐步缩小报文长度试探。先用小请求确认链路通再逐渐加大请求体找到临界值。如果临界值稳定在某个数字附近基本可以确定是路径上某一段的上限比两端以为的要小。处理办法有两个方向一是把两端的最大报文段调小让它别超过路径上限二是让中间设备正确返回通知报文让发送方自己调整。前者治标快后者治本但要协调设备实际工作中通常是先治标。注意调整最大报文段是全局性配置会影响这台机器上的所有连接。改之前想清楚影响面别为了一个特殊链路把整台机器都拖下水。5.3 连接数与backlog服务端的连接数是另一个高频调参点。这里有个容易混淆的地方监听队列的长度和已建立连接的数量不是一回事。监听队列backlog指的是“三次握手已经完成、但应用还没来得及取走”的连接能排多长。这个值如果设小了高并发时新连接会被丢弃或者被拒绝客户端看到的现象是连接超时或直接被重置而服务端日志里可能什么都没有——因为请求根本没进到应用层。队列长度受两个地方限制应用在监听时传的值和系统级的全局上限取较小值。所以你在应用配置里把队列调到很大但系统级上限没动实际生效的还是那个小的。这是典型的“改了没效果”的场景排查时要把两层都看一眼。怎么判断是不是队列的问题一个比较直接的信号是已建立连接数不高但客户端频繁报告连接失败或超时且失败是间歇性的、跟流量峰值相关。如果同时能看到系统层面有“队列溢出”之类的计数在增长那基本就确定了。处理方式是同时抬高应用侧和系统侧的值并且观察溢出计数是否归零——计数不归零就说明还没调够。还有一点是临时端口范围。客户端发起连接时用的是临时端口如果范围窄高并发下很快就不够用了。这个范围可以调宽但也别调到跟保留端口冲突。调完之后同样要看“端口耗尽”类的计数用数据验证而不是凭感觉。6. 排查实录典型故障的定位顺序前面讲的都是“正常应该怎样”这一节讲“不正常怎么找”。我把这些年遇到的典型场景整理成一张表然后讲两次印象比较深的排查过程。6.1 故障速查表现象最可能的位置第一手证据处理方向解析耗时长解析链路或缓存分段耗时里的解析项查记录条数、别名链路、缓存时间连接阶段超时去程丢包或对端未监听抓包里只有 SYN 没有 SYN-ACK确认对端监听状态、查中间策略连接被重置对端拒绝或中间设备干预重置报文出现的时间点看重置是谁发的、在握手第几步首字节很慢应用层处理建连完成到首字节的间隔看服务端处理耗时、线程或连接池传输中途卡住路径上限或接收窗口为零零窗口通告、反复重传同一段查最大报文段、查接收方消费速度大量等待关闭状态主动关闭方端口耗尽状态统计里的数量与端口占用改连接复用、调整关闭方向间歇性失败队列溢出或临时端口不足系统计数器的增长趋势抬高队列、拓宽端口范围这张表的用法是先用分段耗时把范围缩到一段再用抓包在这一段里找证据最后用计数器确认判断。三步走完结论基本可靠。我特别不建议跳过第一步直接抓包因为抓包文件大、信息杂没有方向的话很容易陷进去。6.2 一次典型排障的完整推演说一个我印象比较深的场景。某服务上线后调用方反馈“偶发超时”比例大概千分之几重试基本能成功。这种问题最烦人因为复现不了。第一步我把调用方的耗时拆开看发现超时集中在连接阶段首字节正常。这一步就把应用层排除了。第二步两端同时抓包。对比之后发现一个有意思的现象调用方发了握手第一个报文服务端那边压根没收到但过了一小会儿调用方重发了一次这次服务端收到了连接正常建立。也就是说丢的是握手的第一包。第三步看丢包的规律。发现丢包集中在流量峰值时段而且不是固定某台机器是整批机器都有。这就排除了单机问题指向了共同的路径。第四步去查路径上的设备。发现某一跳在做连接数限制超过阈值就丢弃新连接的首包。这个行为很隐蔽因为它不返回任何拒绝信息只是静默丢弃表现就是超时加重试成功。定位之后处理就简单了一方面把连接改成复用减少新建连接的数量另一方面协调调整那台设备的限制阈值。两边一起动问题消失。回头看如果没有两端同时抓包这个结论根本不可能得出来——单看调用方你只会看到“发了没回应”永远不知道是丢了还是被拒了。这个过程里我觉得最有价值的一步其实是第一步用分段耗时把范围缩到连接阶段。因为如果没有这一步你很可能会去抓全量包然后在海量报文里迷失。排障的效率不取决于你抓了多少包取决于你多快能把范围缩小。6.3 我踩过的几个坑第一个坑是把正常现象当故障。最早看到大量等待关闭状态我第一反应是出问题了折腾了半天参数最后发现端口占用率才百分之几完全是正常范围。后来我给自己定了个规矩任何“异常”先找一个正常环境做对照对照不出来的不叫异常。第二个坑是在错误的观测点抓包。有一次在宿主机上抓包看到的源地址全是网关的我以为是应用配置错了查了半天配置文件。后来才想起来应用跑在容器里地址在出容器的时候被改写过了我抓的位置根本看不到原始地址。这件事教育我抓包之前先搞清楚自己站在哪。第三个坑是过度相信工具的自动分析。工具会把乱序标成重传会把重复投递标成异常会把正常的窗口调整标成性能问题。这些标记是提示不是结论。我现在的习惯是任何工具给的红色标记都要自己用序号和确认号验证一遍再下判断。第四个坑是改了参数不看计数器。有一阵子我特别喜欢调参数觉得调完就完事了。后来发现很多调整根本没生效因为被更上层的限制盖住了。现在我改任何参数都会先找到对应的计数器改完盯着看有没有变化。没有计数器验证的调优就是玄学。最后说一个我一直在用的方法给每类问题写一张排查卡片上面只写三件事——现象是什么、第一步看什么、判断标准是什么。卡片不写原理原理在脑子里卡片只负责在慌乱的时候给你一个起点。我攒了十几张从“接口超时”到“文件上传中断”都有。这东西比任何文档都管用因为它是从自己的坑里长出来的。每次踩新坑我就往卡片里加一条时间长了这张卡片本身就成了一套方法论。