网络故障诊断方法论

本页目录

诊断的目标是用证据缩小故障边界,并验证修复是否改变了原始症状。协议分层提供检查地图,但没有必要每次从网线查到应用,也不能因为某次 ping 成功就把整个网络层永久排除。

“访问不了”至少要展开为:哪个客户端、哪个网络 namespace、哪个时间窗口、哪个域名和端口、哪个协议、哪一步错误。还应区分所有请求失败、部分后端失败、仅大响应失败、首次访问慢、长连接空闲后失败等条件。

诊断闭环:一次对照只改变可解释的条件 具体症状 谁、何时、访问哪里、怎样失败 可证伪假设 DNS / 路径 / 握手 / 应用阶段 最小对照 固定目标、协议与其他条件 保留结果 支持什么、尚未排除什么 恢复服务与证明根因是不同结果;每次变更都应有可观察指标和退出条件。

从应用症状选择最有区分力的对照

对照尽量保持一致能帮助区分
同客户端固定一个目标 IPURL 主机名、TLS 身份、路径和请求DNS 结果/节点差异与服务行为
同服务分别走 IPv4、IPv6请求、时段与客户端地址族、路由与后端差异
同路径小响应与大响应后端及业务处理尽可能一致尺寸、传输持续时间与缓冲问题
同一节点冷连接与复用连接API、负载与认证建连阶段与应用阶段
故障窗口两端采集五元组、时间和采集范围请求/响应在哪一段不可见

同时更换 DNS、代理、HTTP 版本和目标 IP,成功之后也难以知道哪项起作用。每个对照都应记录改变了什么,以及哪些条件仍然无法控制。

DNS:查到记录,不等于应用用了这份答案

# 指定解析器,保留状态码、TTL 与应答内容。
dig @192.0.2.53 app.example A
dig @192.0.2.53 app.example AAAA

# 由 dig 自己进行迭代查询,查看公共委派相关行为。
dig +trace app.example

+short 适合快速提取结果,却会隐藏不少失败上下文。+trace 不会展示企业递归器内部的每一步,也可能绕过分流 DNS;它需要从当前环境直接访问相应权威服务,失败不必然说明应用所用递归器也失败。BIND dig

应用可能使用系统解析、hosts、浏览器 DoH、自有缓存或代理端解析。先记录应用实际连接的远端 IP,再比较预期答案。NXDOMAIN、SERVFAIL、超时和返回了错误地址的诊断路径不同,不能统一归因“DNS 不稳定”。

若要测试某个 IP 对这个 HTTPS 域名的服务,用 --resolve 保留 URL 主机名对应的 SNI、证书验证和 HTTP 主机身份;直接把 URL 改成 IP 后再用 -k,已经改变了多个条件。

# 明确测试直连;目标地址、名称、路径按实际环境替换。
curl --noproxy '*' --resolve app.example:443:192.0.2.10 \
  --connect-timeout 5 --max-time 15 -sS -o /dev/null \
  -w 'ip=%{remote_ip} code=%{http_code} total=%{time_total}\n' \
  https://app.example/health

这里 --noproxy '*' 明确排除了代理变量的影响;若原问题就在代理路径,应另外保留代理设置做对照。固定 IP 也可能选中不同 CDN 节点,所以“固定 IP 正常”只支持继续检查解析与节点差异,并不能立即证明 DNS 协议故障。curl --resolve

连接:把 SYN 的去向和响应的回程分开

# 路由查询在当前 namespace 内进行,不发起远端探测。
ip route get 192.0.2.10
ip -6 route get 2001:db8::10

# 查看监听与连接状态,按实际端口过滤。
ss -ltnp 'sport = :443'
ss -tinp 'dport = :443'

路由结果受源地址、mark、策略规则、VRF 和 namespace 影响;当前 shell 的查询不一定复现容器或带特殊标记的业务流。服务监听在 127.0.0.1、某个指定地址、IPv6 地址或另一个 namespace,也会改变外部可达性。ip route

双端可见事实优先缩小到尚不能排除
客户端采到 SYN,服务侧没采到去程或观察点之间过滤过窄、采集缺失、实际走另一节点
服务侧采到 SYN,没见 SYN-ACK服务侧处理和策略抓包点前后、防火墙、资源与监听状态
服务侧发了 SYN-ACK,客户端没采到回程或客户端前段非对称路由与采集缺失
收到 RST某端或中间设备主动拒绝/终止仅凭源 IP 不能证明一定是应用发出
三次握手完成当时该连接可交换小型握手报文后续 MTU、拥塞、TLS 和应用问题

ping 使用 ICMP,业务可能用 TCP、UDP、不同地址族甚至不同路径。ping 成功只能证明该次 ICMP 探测得到了响应;ping 被禁也不等于业务不可用。抓包细节见tcpdump 与 Wireshark。

TLS:正确区分传输握手与身份验证

openssl s_client -connect 192.0.2.10:443 -servername app.example \
  -verify_hostname app.example -verify_return_error -brief </dev/null

-servername 提供 SNI,-verify_hostname 检查期望主机名,两者不能互相替代。s_client 默认的诊断行为可能在证书验证错误后继续,使用 -verify_return_error 才明确把验证失败作为失败条件;私有 CA 需配置实际可信 CA 文件。OpenSSL s_client

TLS 建立成功也未验证 HTTP 路由、用户认证或后端依赖。若只有应用失败,应比较它的 SNI、ALPN、客户端证书、CA 集合和实际连接目标;不要只凭“命令行能握手”排除所有 TLS 配置差异。

把一次慢请求拆成可解释的时间

curl --noproxy '*' --connect-timeout 5 --max-time 15 -sS -o /dev/null \
  -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first=%{time_starttransfer} total=%{time_total}\n' \
  https://app.example/health

这些时间通常是从操作开始累计到相应阶段的时间,不是可以直接相加的五段耗时。下面假定新建直连 HTTPS、没有重定向、没有连接复用,并且请求体发送影响已包含在首字节前等待中。

同一请求的累计时间,需要相减才得到阶段耗时 20 ms DNS 40 ms TCP 80 ms TLS 200 ms 请求至首字节 60 ms 响应接收 0 60 140 340 400 ms 假设直连 HTTPS、无重定向、无连接复用;首字节前等待不能全部归为服务端 CPU。
from decimal import Decimal as D
cumulative = list(map(D, ['0', '0.020', '0.060', '0.140', '0.340', '0.400']))
stages = [b - a for a, b in zip(cumulative, cumulative[1:])]
assert stages == list(map(D, ['0.020', '0.040', '0.080', '0.200', '0.060']))
assert sum(stages) == D('0.400')
print('DNS 20、TCP 40、TLS 80、首字节前 200、接收 60 ms')

time_starttransfer - time_appconnect 包含请求发送、网络往返、服务排队及处理等,不能全记为服务端计算时间。代理、重定向、连接复用、多路复用和失败请求会改变字段的解释;不能把缺失或为零的阶段强行算成正常测量。curl write-out

如果 TLS 快、首字节慢,继续关联请求 ID、代理 upstream 时间、服务队列与依赖调用。若首字节快但下载慢,则比较响应大小、应用输出节奏、带宽、重传和接收背压。HTTP 503 说明已有 HTTP 响应,优先查看响应者与上游状态,不能仍只重复 ping。

五个计时值,为什么不能直接相加?

只增加TLS时间,appconnect、starttransfer、total三个累计值同时增加;它们不是三份独立的新耗时。时间条按相邻差展开后才可相加,得到真正的总时间。

正在呈现知识画面
五个计时值,为什么不能直接相加?

累计值包含之前的等待。阶段耗时来自相邻累计值相减;首字节前等待不等于服务器CPU时间。

MTU 黑洞:用一组相互支持的证据确认

典型假设是:路径某处容不下较大的报文,报文被丢弃,而发送端又没有正确获知或适应更小 PMTU。握手和小请求能通、大数据持续停滞,是线索而非独占症状;应用缓冲、策略按内容过滤和接收窗口停顿也可能相似。

# IPv4,无 IP options 时:1472 B payload + 8 B ICMP + 20 B IP = 1500 B。
ping -4 -c 3 -M do -s 1472 192.0.2.10
ping -4 -c 3 -M do -s 1200 192.0.2.10

大 ping 失败可能来自本机 PMTU 检查、对端 ICMP 策略、路径或回程,不能单次确认黑洞。先区分本机 message too long、收到 ICMP 错误和完全超时,再观察实际业务的大报文、重传与 ICMP Packet Too Big/Fragmentation Needed 是否相关。ping 手册

ipv4_probe = 1472 + 8 + 20
ipv6_probe_without_extensions = 1452 + 8 + 40
assert ipv4_probe == ipv6_probe_without_extensions == 1500
path_mtu = 1400
assert path_mtu - 20 - 20 == 1360  # IPv4 TCP 基础头下的数据上界。
assert path_mtu - 40 - 20 == 1340  # IPv6 无扩展头。
print('探测 payload、IP 包长和 TCP 数据预算分开计算')

这些预算还没有计 TCP options 或额外封装。修复应针对真实路径 MTU、必要 ICMP 反馈或适用的 PLPMTUD 机制;MSS clamping 只影响 TCP 协商的相关方向,不能修复任意 UDP/QUIC,也不自动作用于已经建立的连接。因此不应把一条全局防火墙修改当成诊断步骤。DPLPMTUD

间歇性问题:看时间相关性,而不是累计值大不大

mtr 通过重复探测汇总响应,能帮助发现持续或间歇模式,但中间路由器对探测响应的限速不等于转发业务包也丢失。第 4 跳显示 60% 无响应、第 5 跳和目标正常,通常不支持“第 4 跳丢了 60% 业务流量”的结论。

# 适用时使用与业务更接近的 TCP 目标端口探测,仍非真实业务请求。
mtr --report --report-cycles 100 --tcp --port 443 app.example

每跳响应还有自己的返回路径;ECMP、探测字段与协议差异也会影响观察。后续跳和目标持续出现相近退化,才更值得联同端到端业务证据检查该区段,也仍需留意回程。mtr 手册

数据使用方式常见误读
NIC error/drop故障窗口前后做差,并查驱动字段语义将所有 drop 都当成同一种线路损坏
qdisc backlog/drop关联负载、时延和具体队列策略看见 drop 就断言 bufferbloat
conntrack count/max结合分配失败、丢弃计数及日志仅 count 较高就确认表满
TCP 重传/RTT同方向、同时间窗口和连接范围比较累计重传总量直接等同丢包率
CPU/softirq/应用队列比较是否与吞吐或尾延迟同步变化只看整机平均 CPU 很低

Linux TCP 的 ss 队列字段还要区分状态:LISTEN 的 Recv-Q/Send-Q 表示已完成待 accept 队列及其上限;已建立连接的接收队列表示尚未被应用读取的数据,发送队列则涉及已写入但尚未确认的序号空间,包含尚未发送的数据,不能一律叫“线上未 ACK 数据”。这些口径可从 Linux tcp_diag_get_info() 对照。ss 手册

把结论写成可复查的因果链

一份有用的记录可以是:“10:02–10:08,节点 A 的 IPv6 新连接在 TLS 前超时,IPv4 对照正常;服务端未见对应 IPv6 SYN,客户端接口可见重发;当前证据将边界缩到两采集点之间,但尚未排除中间镜像漏采。”这比“网络抖动,已重启”更便于下一步协作。

修复后重做原始失败条件,比较错误率、时间分布和资源副作用。一次请求成功只能证明当时成功;间歇性故障需要足够覆盖原故障条件的观察窗口。若临时措施恢复了服务但根因未确定,应将恢复事实和根因假设分别记录。