做运维和后台开发这些年,我遇到过太多“看着像玄学”的问题:接口偶发超时、页面偶发白屏、服务端 CPU 不高但吞吐上不去、容器里跑得好好的换台机器就卡。最后把包抓下来一看,八成不是代码写错,而是对链路上跑的协议理解有偏差——握手阶段就失败了、MTU 不匹配导致大包被丢、DNS 解析绕了远路、连接被中间设备悄悄重置、长连接被空闲超时悄悄收走。网络协议梳理这件事,说白了就是把“我这条请求从发出到回来,中间到底经历了哪些协议、每个协议在哪一层做了什么、哪一步可能出问题”画成一张自己能看懂的地图。它不是背 OSI 七层,也不是把 RFC 抄一遍,而是把协议按“我在排障时会用到”的顺序重新编排一遍。这篇内容适合三类人看:一是刚接手线上服务、抓包只会看“有没有 RST”的新同学;二是写了几年业务、想搞明白为什么压测数据上不去的中高级开发;三是需要给团队沉淀一份排障手册的技术负责人。下面我按自己的实际做法讲,从框架怎么搭、关键协议怎么拆、工具怎么用,到成果怎么落成文档,全部是可复现的操作。
1. 先想清楚:协议梳理到底要解决什么问题
1.1 三种最常见的困境,决定了梳理的切入点
我不止一次见过这样的场景:某同学排查一个“接口偶发 3 秒超时”,抓包看了半天说“TCP 没问题啊”,然后去查业务日志,查了两天没结果。后来我拿过包一看,前两次握手正常,第三次 ACK 之后客户端发了请求,服务端 3 秒后才回——中间那 3 秒里,服务端根本没有收到这个包,问题出在中间设备的会话表上。这个例子的价值不在于答案,而在于:如果没把“协议在哪一层、包在哪一段消失”这个视角建立起来,抓包文件在手里也是一堆乱码。
我总结下来,协议梳理要解决的困境基本是三种。第一种是定位断层:只知道应用层报错,不知道往下还有传输层、网络层、链路层,出问题时只能一层层试,试到最后靠重启。第二种是知识碎片化:TCP 三次握手背得滚瓜烂熟,但真问“为什么服务端上会有几万个 TIME_WAIT”“为什么 backlog 调大了还是拒绝连接”,答不上来,因为知识是点状的,连不成线。第三种是工具与协议脱节:会用 tcpdump 敲命令,但看到输出里Flags [S.]、win 29200、length 0不知道重点看哪个字段,工具只用来“证明我抓过包”,没用来“解释现象”。
所以我的梳理逻辑是反过来的:先列现象,再列协议,最后列工具。现象是“超时、连接拒绝、慢、丢包、解析失败、证书报错”,协议是能解释这些现象的那几个(TCP 状态机、DNS 解析流程、TLS 握手、HTTP 语义),工具是能把协议状态暴露出来的那几个。这样梳理出来的东西是有指向性的,不是教科书目录的复刻。我在团队里推这套方法时,第一件事就是让大家把最近三个月处理过的线上问题各写三条,然后一起归类——通常归完就发现,八成问题集中在不超过十个协议行为上,剩下的都是长尾。这个统计过程本身就是一次极其高效的梳理,比坐下来读文档有效得多。
1.2 梳理的边界:理什么、不理什么,得先划清
很多人一上手就想把所有协议都覆盖,结果做了个几百页的文档,团队里没人看。我建议一开始就把边界划死:只梳理“你的系统实际会碰到的协议路径”。举个具体的判断方法,把你负责的服务画成一条链路:用户浏览器 → CDN/接入层 → 负载均衡 → 应用服务 → 缓存 → 数据库。然后沿着这条链路问三个问题:每一跳之间用什么协议通信?这个协议的哪个行为会导致失败?失败了在哪儿能看到证据?
以一条典型的 HTTPS 请求为例,链路上至少涉及 DNS(UDP 53 或 DoH 的 TCP 443)、TCP(握手、传输)、TLS(证书、密钥协商)、HTTP(请求语义、连接复用),底下还有 IP 路由、ARP、以太网帧。这五六个协议就是你要精读的主线,其他的(比如组播、BGP、SNMP)除非你确实在做网络设备相关的工作,否则先放一边。我见过有人花两周研究 BGP 路由选路,结果自己服务的连接超时是因为容器网络的 conntrack 表满了——方向错了,功夫全白费。
边界划清之后,还有一个容易被忽略的点:要区分“协议规范行为”和“设备/中间件实现行为”。RFC 里写的是一回事,实际链路上跑的可能是另一回事。比如理论上 TCP 是端到端的,但你链路上有负载均衡,它可能做 TCP 代理(此时是两段 TCP)、可能做透传(一段 TCP)、也可能在四层做连接复用。这三种模式下同一个“连接超时”现象的根因完全不同。所以梳理的时候我习惯在每个协议旁边标注一句“本系统里这个协议被谁终结、被谁改写”,这一句往往比协议本身的细节更有用。
2. 搭骨架:用分层模型把协议排成一张可查的地图
2.1 从链路层到网络层:先认清“路”和“门牌号”
我不太喜欢从物理层开始讲,那对写业务代码的人没意义。我的起点是“一个包能发出去、能被认出来”。链路层负责的是同一段物理链路内的传递,最典型的细节是以太网帧头 14 字节(目的 MAC 6 + 源 MAC 6 + 类型 2),以及 MTU 的概念。MTU 默认 1500 字节,意思是这一帧能承载的 IP 包最大 1500 字节。这个数字后面会反复出现,因为它直接决定了 TCP 单段能塞多少数据。
网络层是 IP,负责跨网段的寻址和转发。这里必须搞清楚三件事:IP 头固定 20 字节(不含选项)、TTL 每经过一个路由减一、分片只在必要的时候发生。一个特别实用的小学:MSS = MTU - IP 头 - TCP 头 = 1500 - 20 - 20 = 1460 字节。这个 1460 就是你在抓包里经常看到的“单个 TCP 段携带的最大数据量”。为什么这个数值得记住?因为你一旦在链路上引入了隧道封装(比如某种 IP-in-IP 的封装会额外吃掉 20~60 字节),有效 MTU 就变小了,此时如果路径 MTU 发现(PMTUD)被中间设备阻断(ICMP 被过滤),就会出现一个非常经典的现象:小请求正常,大响应卡死。我遇到过好几次“登录接口正常、列表接口返回大数据时超时”,最后都是 MTU 问题。
ARP 也属于这个层面绕不开的东西,虽然它严格说是链路层和网络层之间的胶水。ARP 的作用是把 IP 地址解析成 MAC 地址,它的坑在于表项老化和免费 ARP 更新。在虚拟化和容器环境里,IP 迁移后旧 ARP 表项没更新,会导致流量发到错误的机器上,表现为“间歇性连不上、过一会儿自己好了”。这类问题的排查思路不是看协议细节,而是ip neigh看邻居表状态,配合抓包看有没有 ARP 请求无响应。
2.2 传输层:TCP 与 UDP 的取舍不是“可靠 vs 不可靠”这么简单
一说 TCP 和 UDP 的区别,大部分人背的是“TCP 可靠、UDP 不可靠”。这个说法对理解没帮助,我更愿意这样描述:TCP 替你管理了连接状态和发送节奏,代价是你必须接受它的节奏;UDP 把节奏交给你,代价是状态得自己维护。这个视角一换,很多技术选型就说得通了。
DNS 用 UDP 是因为一次查询一问一答,建连接的握手开销比查询本身还大;但如果响应超过 512 字节(现在有了 EDNS0 可以更大),就会切到 TCP 或者截断重试。音视频用 UDP 是因为实时性优先,丢一帧可以靠编码冗余补,但不能等重传。HTTP/3 基于 QUIC 走 UDP,是因为它把可靠性做在了用户态,可以按流(stream)粒度控制重传,避免 TCP 的队头阻塞。这些例子说明一件事:选型的核心变量是“你能容忍多大的延迟波动”和“你愿不愿意自己管状态”,而不是简单的好坏。
在梳理表格里,我一般会给每个协议标四个维度:是否有连接状态、是否保证有序、是否有重传、头部开销多大。这个表填完,遇到“为什么这个场景用这个协议”基本不用查资料了。下面这张表是我自己常用的简化版,实际写文档时会补上具体的头部字段和典型端口:
| 协议 | 连接状态 | 有序性 | 重传机制 | 头部开销 | 典型场景 |
|---|---|---|---|---|---|
| TCP | 有(三次握手/四次挥手) | 保证 | 有(超时+快速重传) | 20 字节起 | 网页、API、数据库 |
| UDP | 无 | 不保证 | 无 | 8 字节 | DNS、实时音视频、QUIC |
| ICMP | 无 | 不适用 | 无 | 8 字节起 | 探测、差错上报 |
| QUIC | 有(用户态) | 流内保证 | 有(按流) | 可变 | HTTP/3 |
2.3 应用层:别贪多,按“请求生命周期”串起来
应用层协议是最容易梳理成流水账的地方,因为数量太多。我的做法是不按协议名排,而是按一次请求的生命周期排:解析域名 → 建立安全通道 → 发送请求 → 接收响应 → 连接复用或关闭。每个阶段挂上对应协议要关注的点。
域名解析阶段看 DNS:记录类型(A/AAAA/CNAME/MX/TXT)、TTL 的影响、递归查询和迭代查询的区别、以及解析结果缓存分布在哪些层(浏览器、系统、本地缓存服务)。这里有个很实用的经验:TTL 设得太短会让权威服务器压力大,设得太长会让故障切换变慢,我在实际项目里一般给核心域名设 60~300 秒,切换时靠预热和灰度,而不是靠极短 TTL 硬扛。安全通道阶段看 TLS:版本、密码套件、证书链、SNI、会话复用(session ticket / PSK)。TLS 1.3 把完整握手压到 1-RTT,还支持 0-RTT 恢复,代价是 0-RTT 数据有重放风险,所以只适合幂等请求。
请求响应阶段看 HTTP:方法语义(GET 幂等、POST 不幂等)、状态码分类、头部字段(Cache-Control、Connection、Transfer-Encoding、Content-Length)、连接复用策略。HTTP/1.1 的持久连接加上管线化,理论上能省握手,但实际因为队头阻塞,浏览器基本不用管线化;HTTP/2 用二进制分帧和多路复用解决了应用层队头阻塞,但底层还是 TCP,丢包时依然全连接阻塞;HTTP/3 换到 QUIC 才真正把这个问题拆开。把这三个版本的演进理由讲清楚,比背每个版本的头部字段有用得多——因为它解释了为什么你换个协议版本,某些性能问题就消失了。
3. 抠细节:几个必须吃透的协议行为
3.1 三次握手与四次挥手:时序背后的真实含义
三次握手的过程人人会背:客户端发 SYN(带初始序列号 ISN),服务端回 SYN+ACK(自己的 ISN + 确认号),客户端回 ACK。但真正要理解的是为什么是三次而不是两次:因为双方都要确认“我的发送能力被你收到”和“你的发送能力被我收到”,两次只能完成一半。抓包里看握手,重点看三个字段:Flags [S]、seq(相对序列号)、win(窗口大小)。如果只看到 SYN 反复重发而没有 SYN+ACK,说明包到了对端但对端没响应,或者响应被拦了;如果看到 SYN 后立刻回 RST,说明端口没监听或者被安全策略拒绝。这两种情况的排查方向完全不同。
挥手是四次,因为 TCP 是全双工,两个方向要各自关闭。主动关闭方发 FIN,对方回 ACK,等自己数据发完再发 FIN,最后回 ACK。这里的关键角色是TIME_WAIT:主动关闭方在发完最后一个 ACK 后进入 TIME_WAIT,等待 2MSL(Linux 上通常实现为 60 秒)。为什么要等?一是保证最后的 ACK 能到达(如果对方没收到会重发 FIN,此时还能回 ACK),二是让这条连接上的旧报文在网络中自然消亡,避免干扰使用相同四元组的新连接。这个设计很优雅,但代价是:高并发的短连接服务端或客户端会堆积大量 TIME_WAIT,占用本地端口。
我实际遇到最多的误判是:看到服务端 TIME_WAIT 多就以为是服务端的问题。其实 TIME_WAIT 出现在主动关闭方。如果是短连接,通常是客户端主动关,那 TIME_WAIT 就该在客户端;如果服务端上有大量 TIME_WAIT,说明是服务端在主动关连接,这时要去看是不是 keepalive 配置过短、或者应用层逻辑里每次都新建连接。判断清楚这一点,能省掉一半无效排查。
3.2 连接队列与状态机:为什么 backlog 调大了还是拒绝连接
服务端处理新连接的流程里有两个队列:半连接队列(SYN queue)和全连接队列(accept queue)。客户端 SYN 到达后,服务端回 SYN+ACK,这个连接进入半连接队列;收到客户端 ACK 后,连接移入全连接队列,等待应用调用 accept 取走。全连接队列的长度由listen(fd, backlog)的 backlog 和系统参数net.core.somaxconn共同决定,取两者较小值。
这就解释了一个非常常见的现象:压测时出现间歇性连接失败,但服务端 CPU 和内存都很闲。原因往往是应用 accept 的速度跟不上,全连接队列满了,内核按tcp_abort_on_overflow的配置决定是丢弃 ACK(让客户端重试)还是直接 RST。如果是前者,客户端表现为超时;如果是后者,表现为连接被重置。这两种表现经常被误认为是网络抖动。
实操上我一般这样看:用ss -lnt看Send-Q列(对监听 socket 来说,这个值就是当前全连接队列的最大长度限制),用ss -s看整体统计,用netstat -s | grep -i listen看队列溢出计数(times the listen queue of a socket overflowed和SYNs to LISTEN sockets dropped)。这两个计数只要在涨,就说明队列不够或 accept 太慢,方向很明确。参数上,net.core.somaxconn和应用的 backlog 都要调,只调一个没用。另外net.ipv4.tcp_max_syn_backlog管的是半连接队列,在遭受大量半开连接时才有意义,正常情况下它不是瓶颈。
3.3 窗口、重传与拥塞控制:吞吐上不去的算术题
很多人压测时会发现:带宽明明够、CPU 也够,但单连接吞吐就是上不去。这时候要算一个东西——带宽时延积(BDP):BDP = 带宽 × 往返时延。举例,链路带宽 100 Mbps,RTT 40 ms,BDP = 100 × 10^6 × 0.04 / 8 = 500 KB。意思是“在途未确认的数据最多能有 500 KB,链路才不空闲”。如果 TCP 窗口只有 64 KB,那单连接吞吐上限就是 64 KB / 0.04 s = 1.6 MB/s ≈ 12.8 Mbps,无论带宽多大。这就是为什么跨地域传输大文件时,单连接慢是多连接快的好几倍。
窗口的问题还有一层:TCP 头里的窗口字段只有 16 位,最大 65535 字节,不够用,所以有了窗口缩放因子(window scale),在握手时协商,之后的窗口值要左移这个因子。抓包里如果看到win 29200, options [..., wscale 7],实际窗口就是 29200 × 2^7。这个细节在分析“窗口明明很小为什么不涨”时很关键——你可能看的是未缩放的值。
重传机制分两种:超时重传(RTO)和快速重传。RTO 是基于 RTT 采样动态计算的,Linux 有最小 RTO 下限(通常 200 ms),所以遇到丢包时,最坏情况要等几百毫秒才能发现。快速重传靠三个重复 ACK 触发,不用等超时,但如果丢的是最后一个包,就没有后续 ACK 了,只能靠 RTO。抓包里判断重传,用显示过滤器tcp.analysis.retransmission最直接,但要注意区分“真丢包”和“乱序导致的伪重传”——后者在有多路径或者网卡多队列时很常见,看到重传就断定网络丢包是新手最常犯的错。
拥塞控制这块,我建议至少搞清楚慢启动和拥塞避免的分界。初始拥塞窗口现在常见是 10 个 MSS(约 14.6 KB),每收到一个 ACK 窗口加一,呈指数增长,直到达到慢启动阈值或者检测到丢包。丢包后的处理策略因算法而异:CUBIC 用三次函数增长,BBR 则基于带宽和 RTT 建模,不把丢包当拥塞信号。这也是为什么同样一条有轻微丢包的链路,换拥塞控制算法后吞吐可能差好几倍。实际调优时,跨地域长肥管道(高带宽高时延)场景,BBR 往往比 CUBIC 表现好,但要注意它对公平性的影响,别在共享链路上无脑开。
4. 上手实操:把协议从文档里“抓”出来
4.1 工具选型:够用就好,别堆一堆
我常用的工具就四类,各司其职。第一类是抓包:tcpdump 用于服务器上无图形界面的场景,Wireshark 用于本地深度分析。第二类是连接状态查看:ss已经基本取代 netstat,速度快、信息全,ss -tni能直接看到每个连接的拥塞窗口、RTT、重传次数,非常实用。第三类是请求层工具:curl 用来验证应用层行为,curl -v能看到完整的握手和头部交换,curl -w可以输出耗时分解,比看日志快得多。第四类是路径探测:ping、traceroute、mtr,用来判断问题在哪一跳。
选型上我的原则是:服务器上不装重型工具,本地分析用图形工具。生产机器上只放 tcpdump 和 ss,抓完包拉回本地用 Wireshark 慢慢看。原因很实际——生产机上多装一个带图形依赖的包,就多一份维护成本和潜在风险。另外提醒一句,抓包本身有开销,全量抓包在高流量机器上可能吃掉可观的 CPU 和磁盘,一定要加过滤条件并限制文件大小。
抓包命令我常用的几个,参数解释写在注释里:
# 抓指定网卡上 80 端口的前 1000 个包,-nn 不做域名和端口名解析(避免额外 DNS 查询干扰) # -s 0 表示抓完整包不截断,-w 直接写文件避免终端刷屏 sudo tcpdump -i eth0 -nn -s 0 -c 1000 -w http.pcap 'tcp port 80' # 只看 SYN 包,用来快速判断有没有握手请求到达 sudo tcpdump -i eth0 -nn 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0' # 抓 ICMP,排查 MTU 和不可达问题 sudo tcpdump -i eth0 -nn -v 'icmp' # 抓 DNS,看解析请求和响应 sudo tcpdump -i eth0 -nn -s 0 'udp port 53'那个-s 0我特别要强调。早期版本 tcpdump 默认只抓 68 字节,你会拿到一堆“截断的包”,分析 TLS 证书或者大响应时完全没法看,白白浪费一次抓包窗口期。养成习惯,除非明确只要头部,否则一律-s 0。
4.2 一次完整的抓包分析流程:以“接口偶发超时”为例
我按固定套路走,五步,基本能覆盖大部分情况。
第一步,明确现象和时间点。记下出错请求的准确时间(精确到秒)、源 IP、目的 IP、目的端口。没有这几个信息,抓出来的包你根本对不上业务。我一般让应用在报错日志里打印本地时间、远端地址和请求 ID,有了请求 ID 还能和链路追踪对上。
第二步,在两端同时抓。只在客户端抓是不够的——你只能看到“我发出去了、没收到回”,但不知道包到没到服务端。两端同时抓,对比同一个序列号的包在两边的时间戳,就能算出单程时延和是否丢包。这一步是很多人的盲区,其实成本很低。
第三步,用显示过滤器逐层看。在 Wireshark 里先看 TCP 流:tcp.stream eq N锁定一条流,然后看tcp.flags.reset == 1有没有重置,看tcp.analysis.retransmission有没有重传,看tcp.analysis.zero_window有没有零窗口。零窗口意味着接收方缓冲区满了,发送方被迫停发——这时候问题在接收端的应用读取速度,不在网络。
第四步,对齐时间线。把 TCP 流的时间戳和应用的请求日志对齐,看那 3 秒到底花在哪:是握手慢、是请求发出去了等服务端处理、还是服务端回了但客户端没及时读。Wireshark 的“统计 → 对话”能看到每条流的持续时间,配合tcp.time_delta字段能快速定位长间隔。
第五步,交叉验证。抓包的结论一定要能被另一个证据佐证。比如怀疑是中间设备重置连接,就同时看服务端有没有收到 RST、看设备侧的会话日志;怀疑是 DNS 慢,就用dig +trace和抓包结果对比。只有一条证据链的结论,我一般不敢直接写进故障报告。
4.3 应用层协议怎么“看”:DNS、TLS、HTTP 各自的观察点
DNS 相对简单,抓包里看请求的 QNAME 和响应里的 A 记录,重点看响应时间和 TTL。如果解析耗时超过 100 ms,大概率是走到了远端权威服务器,说明本地缓存没命中;如果出现大量重试,要看是不是 UDP 包丢了或者被限速。这里有个经验:容器环境下 DNS 经常是隐性瓶颈,因为每个新 Pod 都要解析一次,而本地缓存服务如果没部署,请求会打到上游。我在梳理时会把“DNS 解析耗时”单独列成一个监控指标,比事后抓包有效得多。
TLS 的观察点有三个。一是握手轮次:ClientHello→ServerHello→Certificate→ClientKeyExchange(TLS 1.2)或者直接Finished(TLS 1.3 更简洁)。二是证书链:看Certificate包里返回了几张证书、有没有缺中间证书。缺中间证书是个经典问题——某些客户端能靠缓存补上,某些不能,表现为“部分用户报证书错误”。三是协商结果:看选的密码套件和协议版本,如果发现降级到了很老的版本,说明有一端配置有问题或者中间有设备在干预。
HTTP 的观察点在头部字段。Connection: keep-alive和close决定了连接是否复用;Transfer-Encoding: chunked意味着没有 Content-Length,响应长度靠分块标记;Cache-Control决定了缓存行为。我遇到过一个问题:某接口偶发返回不完整,最后发现是同时设置了Content-Length和Transfer-Encoding: chunked,不同中间设备的处理不一致,有的按前者截断。这类问题不看报文根本想不到,因为代码里两个头都不是手写的,是框架和网关各加了一个。
5. 落地:把梳理结果变成团队能用的东西
5.1 文档结构:三张表比三十页文字管用
梳理做完如果只存在自己脑子里,价值会随时间快速衰减。我的做法是固定输出三张表。
第一张是协议与端口对照表,列:协议、默认端口、本系统实际端口、谁终结(客户端/网关/服务端)、是否加密。这张表的价值在于出问题时能快速定位“这个端口上跑的是什么、中间经过谁”。尤其是微服务架构下端口一堆,新人接手时没有这张表根本无从下手。
| 协议 | 默认端口 | 本系统端口 | 终结位置 | 加密 |
|---|---|---|---|---|
| DNS | 53 | 53 | 本地缓存服务 | 否(内网) |
| HTTP | 80 | 8080 | 接入网关 | 否(内网回源) |
| HTTPS | 443 | 443 | 接入网关 | 是 |
| MySQL 协议 | 3306 | 3306 | 数据库代理 | 是(TLS) |
| Redis 协议 | 6379 | 6379 | 应用直连 | 视部署而定 |
第二张是分层排障路径表,列:现象、优先检查层、具体命令、常见根因。这张表是真正会被反复用到的。比如“连接被拒绝”优先看传输层和应用层,“请求超时但服务端无日志”优先看网络层和链路层。
| 现象 | 优先检查层 | 常用命令 | 常见根因 |
|---|---|---|---|
| 连接被拒绝 | 传输层/应用层 | ss -lnt、curl -v | 端口未监听、队列满、安全策略拒绝 |
| 请求超时且服务端无日志 | 网络层/链路层 | 两端抓包、mtr | 丢包、MTU 不匹配、会话表满 |
| 小请求正常大响应失败 | 网络层 | ping -M do -s、看 ICMP | PMTUD 被阻断、分片被丢 |
| 偶发 DNS 解析慢 | 应用层 | dig、看 TTL | 缓存未命中、上游限速 |
| 证书报错 | 应用层 | openssl s_client | 证书链缺失、SNI 不匹配、过期 |
第三张是关键参数清单,列:参数名、当前值、调整依据。像somaxconn、tcp_max_syn_backlog、ip_local_port_range、tcp_tw_reuse这些,写清楚为什么是这个值,比抄一份“最佳实践”有用。因为不同业务模型的合理值差别很大,短连接密集的服务和长连接为主的服务,端口范围和 TIME_WAIT 策略完全不一样。
5.2 常见误判:几个我自己踩过的坑
第一个坑是把 TIME_WAIT 当故障。前面说过,它是正常状态,只要数量在可接受范围、端口没耗尽,就不用管。真正要关注的是端口耗尽:可用端口范围默认约 2.8 万个,如果每秒新建连接数乘以 TIME_WAIT 时长超过这个数,就会出问题。算一下:60 秒 TIME_WAIT,每秒 500 个新建连接,就是 3 万个,刚好超。这时候的正确做法是扩大端口范围、启用tcp_tw_reuse(只对出向连接有效,且需要时间戳支持),或者干脆改成连接池复用。千万不要去开那些已经被废弃的激进回收参数,它在 NAT 环境下会导致连接错乱,问题比 TIME_WAIT 严重得多。
第二个坑是看到重传就认定网络丢包。前面提过伪重传,还有一个更隐蔽的情况:抓包点选错了。如果你在容器的虚拟网卡上抓,看到的是进入容器前的包,而容器内的应用可能因为缓冲区问题丢包,此时抓包文件里根本没有丢包痕迹。所以抓包点的选择本身要写进流程:优先在离应用最近的位置抓,同时在对端抓,两相对比。
第三个坑是用 ping 通不通来判断服务是否可用。ping 走的是 ICMP,很多环境里 ICMP 被限制,ping 不通不代表 TCP 不通;反过来 ping 通也不代表端口开着。这个道理简单,但实际排查时还是经常有人拿 ping 结果下结论。正确的验证顺序是:先ss确认监听,再curl或nc确认端口可达,最后才是应用层验证。
6. 常见问题速查与实操心得
6.1 问题速查:按症状对号入座
下面这张表是我这些年攒下来的,遇到问题时先扫一遍,能快速缩小范围。
| 症状 | 可能原因 | 验证方法 | 处理方向 |
|---|---|---|---|
| 握手阶段反复 SYN 重发 | 对端未响应、安全策略丢弃 | 两端抓包对比 | 检查监听与策略 |
| 握手后立刻 RST | 端口未监听、队列溢出、应用主动关闭 | ss -lnt、看溢出计数 | 调大队列、查应用逻辑 |
| 传输中偶发卡顿 200ms+ | 丢包触发 RTO、伪重传 | tcp.analysis.retransmission | 查链路质量、看是否乱序 |
| 窗口长期很小不增长 | 接收端应用读取慢、缓冲区小 | tcp.analysis.zero_window | 优化应用消费速度、调缓冲区 |
| 大响应超时小响应正常 | MTU 不匹配、PMTUD 阻断 | ping -M do -s 1472 | 调 MTU、放行 ICMP |
| 连接数上不去 | 端口耗尽、文件描述符限制 | ss -s、ulimit -n | 扩端口范围、调 fd 上限 |
| 部分客户端证书报错 | 证书链不完整、SNI 未配置 | openssl s_client -showcerts | 补全中间证书、配 SNI |
关于 MTU 那个验证方法,详细说一下:ping -M do -s 1472表示禁止分片、发送 1472 字节数据。1472 + 8(ICMP 头)+ 20(IP 头)= 1500,刚好是标准 MTU。如果这个能通而更大的不通,说明路径 MTU 就是 1500;如果 1472 就不通,逐次减小区间试探,找到实际可用的最大值。这个方法在排查隧道封装场景时特别有效,比盲猜靠谱。
6.2 实操心得:几条不上文档的经验
第一,抓包一定要先想好过滤条件再抓。生产环境的流量往往是几个 G,全量抓十分钟就是几十 G 的文件,分析时打开都费劲。我习惯先tcpdump -nn -c 10抓十个包看看流量长什么样,确认端口和协议,再补上精确过滤条件正式抓。这个习惯让我省过好几次磁盘告警。
第二,时间同步是排障的前提。多台机器抓包对比时,如果时钟差几秒,整个分析就废了。梳理文档里我会明确写“所有节点必须开启时间同步,偏差控制在毫秒级”,这不是废话,是真实吃过亏——曾经因为两台机器差了三秒,把问题定位到了完全错误的方向上。
第三,优先级永远是“先恢复、再定位”。协议梳理的价值在于让你在定位时更快,而不是让你在故障时先做学术研究。线上出问题,第一动作是止损(扩容、回滚、切流量),把包抓下来留证据,等恢复了再慢慢分析。我见过有人在故障时现场读 RFC,这属于本末倒置。
第四,给每个协议细节标注“这个知识能解决什么问题”。梳理文档里我最看重的一栏就是这个。比如“掌握窗口缩放”对应“解释跨地域传输为何慢”,“掌握 TIME_WAIT”对应“判断端口是否耗尽”。有了这一栏,文档才不是知识堆砌,而是问题索引。新人拿到文档,反查问题比正序阅读有效得多。
第五,定期用真实故障回填。梳理不是一次性工作,每次处理完一个线上问题,就把新的现象、新的命令、新的根因补进那张速查表。半年下来,这张表就是团队最有价值的资产之一。我现在带的团队里,这张表已经积累了几十条,新人上手排障的速度比纯看文档快很多,因为它是从真实故障里长出来的。