关于 debugging-network-issues
这是一套面向网络与流式传输故障排查的循证方法,适用于需要跨层调查的人员。它强调用实验检验假设,而非从单条日志或表面症状直接下结论。
主要功能
- 01以证据为先的诊断
- 02可证伪假设检验
- 03逐层隔离实验
- 04修复前进行反向审查
- 05检查探测路径与规模
- 06先查仪器再询问用户
适用场景
- 01调查间歇性网络故障
- 02区分上传超时与处理延迟
- 03测量异常缓慢的传输
- 04追踪多跳系统中的故障
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ daymade/ claude-code-skills --skill debugging-network-issues安装后在 Claude Code 中输入/debugging-network-issues确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/case-cloudflare-524-upload.mdcase-iot-router-migration-wpa3.mdcase-proxy-tun-cname-override.mdcase-sse-rst-130s.mdcase-throughput-collapse-no-errors.mdcognitive-traps.mdcounter-review-pattern.mddevice-throughput-fieldwork.mdhttp-response-completeness.mdinstrumentation-patterns.mdlayered-isolation-experiment.mdpacket-capture-recipes.mdscripts/layered-isolation-probe.shmock-idle-upstream.py详细介绍
调试网络问题
一种基于证据的调查方法,适用于明显原因可能并非真因的事件。该方法源自一个真实的生产环境案例,调查耗时 5 小时(参见 references/case-sse-rst-130s.md);在这个案例中,层层叠加的假设浪费了数小时,而一次 10 分钟的分层实验本可以解决问题。
当用户报告网络、流式传输或协议症状,而调查者忍不住想根据一行日志或一个间接数据点下结论时,请使用此技能。此技能的作用是让调查者放慢这种冲动。
先分诊——这是否属于已知领域?
在采用以下通用方法之前,先检查症状是否指向本仓库中已有专用技能的技术栈。这些技能包含本技能刻意不作泛化处理的领域专属“症状→原因→修复”对照表——先从那里开始;如果发现根因另有所在,再回到这里采用方法论。
| 如果症状是…… | 从这里开始 |
|---|---|
macOS 上 Tailscale 与代理/VPN 冲突(Shadowrocket / Clash / Surge):tailscale ping 正常,但 SSH/curl/git 失败,出现 Connection closed by 198.18.x.x、TUN DNS 劫持,或 getaddrinfo 解析器停顿约 60 秒 | tunnel-doctor |
Cloudflare 配置:ERR_TOO_MANY_REDIRECTS、SSL 模式不匹配、橙色云朵代理后方的 DNS / 代理状态问题 | cloudflare-troubleshooting |
| Windows App / AVD / W365 RDP 连接质量:使用 WebSocket 而不是 UDP Shortpath、RTT 高、STUN/TURN 干扰 | windows-remote-desktop-connection-doctor |
客户端代理 / VPN / TUN 路由错误:某个特定网站报错 ERR_CONNECTION_CLOSED 或 SSL_ERROR_SYSCALL,其他网站正常,DNS 返回伪造/TUN IP,添加 PROXY 规则也无济于事 | 本技能——先阅读 references/case-proxy-tun-cname-override.md |
TLS 证书验证错误(UNKNOWN_CERTIFICATE_VERIFICATION_ERROR、证书对应的网站不正确),或所有直接路由/国内域名同时出现握手中途 EOF,而代理域名正常——且任何代理运行状况监控仍报告正常 | tunnel-doctor(TUN DIRECT 分流脑裂步骤) |
| 代理可访问,OS/Tailscale 路由正常,但批量传输缓慢,切换当前出口/节点后测得的速率会变化 | tunnel-doctor——代理节点 / 出口 / 链路容量分支 |
| 没有任何报错——只是很慢。 每个请求都返回 200,延迟和健康检查看起来正常,但传输速度极慢、批量任务超出预估时间,或收到的文件被截断 | 本技能——直接跳到 步骤 0.7;错误驱动的步骤无从入手 |
| 一组智能家居 / IoT 设备同时离线——通常发生在更换路由器/AP 或更改 Wi-Fi 设置数周后;集线器“就在那儿”,但所有子设备都失联,此外还有一些直连 Wi-Fi 设备 | 本技能——先阅读 references/case-iot-router-migration-wpa3.md |
如果以上都不符合——或者你试过某项领域技能,但证据指向别处——请继续阅读下文。这套方法适用于任何多层系统。
本技能特别说明:如果症状是 Cloudflare 524/522,且涉及较大的
POST请求正文(例如,/<openrouter-path>的Content-Length> 1 MB),故障往往是上传到源站所需时间超过 Cloudflare 的源站读取超时,而不是后端响应缓慢。请先使用下方的上传与处理时间检查清单,再判断是否为后端停滞。
核心原则
1. 证据优先于假设
如果你无法指出具体的证据材料——日志行、pcap 帧、探测输出、指标样本——那你就是在猜测,而不是诊断。在断言“X 是原因”之前,要求自己说出直接证据。如果目前还没有证据,请先添加检测手段(参见 references/instrumentation-patterns.md)或采集证据(参见 references/packet-capture-recipes.md),然后再继续。
2. 证伪优先于证实
N 个独立来源“证实”一个假设,并不能使它成为事实。只要一条证伪性观察,就足以排除它。根据假设采取行动之前,请回答:
“什么观察会让我放弃这个假设?”
如果答案是“没有”或“我想不出来”,那么这个假设就不可证伪,不能用来推动调查。如果答案具体明确,就先去寻找这种观察,再决定是否采取行动。
3. 分层隔离
多跳系统(客户端 → CDN → LB → 反向代理 → 应用 → 上游)的问题常常出现在各层之间的接缝处。当某个症状可能由多个层导致时,不要推测问题出在哪一层;要进行测试。通过经过验证的路径对比同一逻辑请求,明确指出每个发生变化的跳点,并且只将差异归因于已隔离的变化。对于间歇性故障,在将成功的变化视为因果证据之前,应有界地恢复到原始条件。关于路径设计和 A/B/A 结果解读,请阅读 references/layered-isolation-experiment.md。
同样的技术也能隔离容量问题,而不只是正确性问题。 当症状表现为速率而非错误时,应改变协议栈而不是跳点:通过两条共享网络路径、但不共享任何应用代码的通道,测量同一方向的速率。这是避免为一个从未成为瓶颈的应用反复调优,成本最低的办法。不要把结论简化成“结果一致就说明是路径问题”——这仅在两个通道都慢时成立;两个通道都很快,说明探测未能复现症状,不能证明任何事情。关于如何解读四种结果,请参见步骤 0.7。
4. 提交前进行反向审查
在确定根因或发布修复之前,应让独立审查者质疑结论,而不是为其背书。Agent 善于发现单个调查者未曾想到的风险,但不擅长权衡这些风险。每项发现影响行动之前,都要应用四问筛选法(参见 references/counter-review-pattern.md)。
5. 绿色健康检查只能证明它探测的路径正常
监控只会监测其作者认为需要探测的路径。多平面系统——具有 DIRECT 平面和代理平面的 TUN 代理、具有数据平面和控制平面的服务、具有后端 API 和网页登录页面的技术栈——会逐个平面发生故障,而只探测另一平面的监控程序会在整个故障期间一直显示绿色。在这一原则背后的事件中,代理健康守护进程每 5 分钟通过代理探测一个海外端点,并连续 2 小时以上记录“健康”,而直连平面早已完全宕机。
在接受“监控显示它是健康的”作为证据之前,请先问:这项检查实际经过的是哪条路径? 它显示绿色,只能作为该路径正常的证据。列出系统实际转发或提供服务的各个平面,并直接探测发生故障的平面——能发现故障的检查通常只需运行一次 curl。
检查也只能证明其测量精度足够覆盖的量级。 路径覆盖是一个维度;规模是另一个维度,也是掩盖吞吐量骤降的那个维度。存活探针返回几百字节,因此其耗时主要由握手和往返时间决定——在案例研究中,它一整天都在 40 ms 内响应,在链路实际容量发生 100 倍变化时仍毫无变化;它并没有撒谎:它确实完成了它能测量的事情。ping/RTT 也是如此:它测量的是小数据包的往返时间,对链路容量毫无说明。
实用规则是:只有当传输时间占据总耗时的主导时,探针才能测量链路。 因此,应根据你需要的答案来设定探针规模,而不是为了方便——如果探针远不到一秒就返回,那么耗时几乎全花在了准备工作上,你测到的也就是准备时间。也可以完全避开规模问题,改为按时间而非字节数来预算(持续传输 N 秒,再用接收的数据量除以 N);Step 0.7 中的命令正是这样做的,因此即使链路已经慢得寸步难行,运行成本也很低。把问题扩展为:是哪条路径,量级又有多大?
6. 先用尽所有检测手段,再向用户提问——只升级处理那些只有用户才能提供的信息
用户口述的观察(“Wi-Fi 会断几秒”“有些应用报告连接错误”)是 线索,不是要反问用户的问题。将其中每个未解子问题都视为可观测事项,并在提问前先查找检测手段:监控/探测记录(如果存在健康守护进程,其日志会记录它所探测路径的故障时间表——需从两个方向解读:绿色记录绝不能推翻用户的观察(原则 5),红色记录也不能说明它从未探测过的路径)、系统日志(内核链路/断开关联事件可确定“Wi-Fi 是否实际断开”;各进程的错误激增可确定“哪些应用发生故障”,但仅限于会在此处记录日志的应用),以及同一网络上的控制通道(通过不同介质连接的第二台主机——一台未使用代理的有线设备、热点对比,甚至路由器自己的状态页;如果不存在控制通道,就明确说明,而不是把问题当作已得到解答)。如果查找检测手段后发现某个子问题没有可用手段,下一步应是第 4 步——部署检测手段并等待问题再次出现——而不是“让用户回忆”。
调查过程中,只有以下三类问题可以合理地交由用户处理:
- 决策权限——生产环境变更、重启、支出,以及任何超出你授权范围的事项。
- 检测手段无法捕获的物理操作和第一人称观察——拔掉设备、读取标签上的密码、确认你无法获取遥测数据的设备上的 Wi-Fi 图标是否消失。
- 你不具备的凭据或访问权限——仅印在设备标签上的管理密码。
观察记录于 2026-09-10:一项家庭网络调查在报告结尾向用户提出了四个问题。“哪些应用报告了错误”——其诊断信息,即各平面的故障时间表,已记录在健康守护进程的日志中(应用名称列表本身只在用户的屏幕上,而且没有提供时间表所缺少的任何层级信息);“Wi-Fi 是否实际断开”则已由 12 小时内没有任何断开关联事件的内核日志确认;但这两个问题仍然都问了用户。第四个问题——“已退役的路由器是否仍在广播”——是合理的反例:ARP 缺失无法证明任何事情(陷阱 14:探测只能证明它运行所在的 L2 域),平台又对 Wi-Fi 扫描结果进行了脱敏,因此这个问题正确地交由用户进行一次 5 秒钟的物理检查。
对于每个交由用户回答的问题,都要在报告中用一句话说明为什么没有任何检测手段能回答它,以及它属于上述三类中的哪一类。如果你写不出这句话,说明问题尚未查尽——回到检测手段,不要提问。
工作流
将此清单复制到调查记录中,并逐项勾选:
Investigation Progress:
- [ ] Step 0: Scope the symptom (exact error, exact times, who, who-not, what changed)
- [ ] Step 0.5: Verify the premise — does direct evidence show the symptom is actually happening?
- [ ] Step 0.6: **For large POST bodies: distinguish upload-timeout from processing-timeout** (see recipe below)
- [ ] Step 0.7: **If nothing errors and it is just slow: measure a rate, then two-channel it** (see recipe below)
- [ ] Step 1: Gather direct evidence at every hop before hypothesizing
- [ ] Step 2: Frame ≥3 hypotheses; for each, name (a) what falsifies it, (b) which layer boundary the intervention would target
- [ ] Step 3: Design a decisive experiment (for network: layered isolation)
- [ ] Step 4: Add instrumentation if evidence gaps block direct observation
- [ ] Step 5: Execute, record actual vs predicted
- [ ] Step 6: Counter-review before acting
- [ ] Step 7: Fix + re-run the same experiment to verify
- [ ] Step 8: Document wrong turns as teaching material
步骤 0:确定范围
范围明确与否,决定了一次调查是耗时 20 分钟还是 5 小时。在查看任何内容之前,先提取:
- 准确的错误字符串(复制粘贴,不要改述)。
socket closed不等同于ECONNRESET,也不等同于HTTP/2 RST_STREAM INTERNAL_ERROR (err 2)。 - 准确的时间戳(带时区的 ISO-8601 格式,而不是“昨晚”)
- 可复现性(每次都会发生/间歇性发生/仅影响特定用户)
- 哪些人受影响,哪些人不受影响(差异化观察有助于缩小搜索范围)
- 近期发生了哪些变化(部署、配置、上游依赖、客户端版本)
区分症状和诊断。“慢”不是症状。“请求耗时 130.898 秒,然后返回 HTTP/2 INTERNAL_ERROR”才是。
但不要让这句话把真实事故排除在外。它要求的是量化,而非错误代码——而有一整类故障可以用速率量化,且完全没有错误代码:每个请求都返回 200,没有连接重置,但系统依然无法使用。只要把“慢”写成特定载荷大小下的每秒字节数(“8.4 MB 用时 95 秒 = 0.09 MB/s,全程 HTTP 200”),它就成为了症状。得到这个数字后,步骤 0.7 就有了明确的实验来验证。超出范围的是未经测量的抱怨——“用户说感觉很慢”——这是步骤 0.5 要处理的问题,不是这里要处理的。
步骤 0.5:核实前提
在投入完整调查之前,先确认报告的症状确实正在发生,而不是仅从下游影响或用户不满推断出来。一次低成本的直接观察,胜过花数小时调查一个并不存在的问题。
请问:“有什么直接证据能证明这个症状确实存在?”
- 如果用户报告“在 130 秒时超时”:这是来自带时间戳的日志、浏览器网络面板,还是凭记忆描述的?
- 如果用户报告“连接被重置”:他们看到了数据包,还是从重试次数激增推断出来的?
- 如果用户报告“有些人失败,有些人没问题”:是否在受控测试中复现过,还是只是传闻?
可接受的依据:
- 带时间戳和错误字符串的日志行
- 显示故障的浏览器 DevTools 网络面板截图
- 可按需复现症状的命令
- 显示特定错误计数上升的指标图表
不足以作为依据:
- “用户说感觉很慢”
- “警报触发了,但我没有检查实际发生了什么故障”
- “上周有人提到过……”
如果前提未通过核实,应该采取的措施是观察,而不是调查。补上缺失的遥测,在完成监测的情况下等待下次发生,拿到真实数据后再回来。不要因为“反正已经开始了”,就屈服于沉没成本的冲动,继续调查。
步骤 0.6:大请求体的上传超时与处理超时
对于由 CDN 代理的、请求体较大的 POST/PUT 端点,最常见的误判是把问题归咎于后端响应慢,而真实原因是上传请求体所需时间超过了 CDN/代理的源站超时时间。
当症状是在 Content-Length > ~500 KB 的请求上出现 524/522/504 时,请应用以下子检查清单:
- 找到边缘/反向代理访问日志(Caddy、nginx、Envoy、Cloudflare Logpush)。
- 比较
bytes_read(或等效字段)与Content-Length:bytes_read == Content-Length且status为错误 → 很可能是后端/处理问题。bytes_read < Content-Length且连接在超时窗口附近关闭 → 上传问题。
- 检查
duration/request_time的含义:- Caddy
duration= 从读取第一个字节到响应结束的总耗时。 - nginx
$request_time= 同上。 - / 应用的
request_time= 完整接收请求体后,后端花在处理上的时间。 - 如果代理
duration≈ 超时时间,而上游request_time很短或根本没有日志记录,那么瓶颈就在请求体上传。
- Caddy
- 查找
status=0(Caddy)或-(nginx):status=0表示代理从未写入 HTTP 响应,通常是因为下游/客户端一侧先关闭了连接。
- 与上游日志进行关联:
- 如果请求 ID / ray ID / trace ID 没有出现在上游(、、应用)日志中,那么请求就没有完成上传。
上传超时 524 的示例特征:
{
"status": 0,
"duration": 125.0,
"bytes_read": 4111422,
"request": {
"headers": { "Content-Length": ["6042141"] }
}
}
解释:代理保持连接 125 s,读取了 6 MB 请求体中的 4.1 MB,随后 Cloudflare 关闭连接并返回 524。
处理超时的示例特征:
{
"status": 504,
"duration": 120.1,
"bytes_read": 6042141,
"request": { "headers": { "Content-Length": ["6042141"] } }
}
解释:请求体已完整上传,但后端未能在代理超时前响应 → 后端/处理问题。
步骤 0.7:吞吐量骤降——没有报错,但一切都很慢时
上面的方案(0.6)针对的是超时——一种可以通过搜索找到的错误代码。本节针对的是完全没有错误的情况:每个请求都成功,每项健康检查都正常,但字节到达速率远低于预期,系统因此无法使用。这种故障最可能被误诊数小时,因为整个由错误驱动的工具箱都无从下手。
为什么常见信号看起来都正常——在链路性能下降 100 倍的同时,观察到了以下所有情况:
| 信号 | 读数 | 为什么看起来仍然正常 |
|---|---|---|
| HTTP 状态 | 全部为 200 | 慢不算错误;响应最终会完成,只是晚了一些 |
| 往返延迟 | 12 ms | RTT 衡量的是小数据包的往返时间;它不是容量指标 |
| 存活/健康端点 | 0.04 s | 其响应大小接近握手数据包,因此耗时全部是 RTT,没有传输时间。如果探测在链路成为限制因素之前就已完成,就无法测量链路——参见原则 5 |
| 传输状态字段 | active; direct <endpoint> | 报告的是路径类型,不是路径容量——参见陷阱 16 |
因此,首先要停止查看状态,转而测量速率。
对于设备或有线/Wi-Fi 对比,在探测或更改关联之前,先加载设备吞吐量实地测试;在其中确定控制/数据路径、对比方式和恢复窗口。
决定性实验:两个独立通道、相同方向、相同时间预算。
这是将原则 3 的分层隔离应用于容量,而非正确性。选择两个通过相同网络路径访问同一主机、但不共享任何应用代码的通道——关键在于,它们一致的结果不可能由任一通道的内部因素造成。 下面两条命令都是按时间预算运行,而非按数据量预算运行:它们会持续传输固定秒数,并报告期间收到的数据量。无论链路状况多差,这都能让探测成本保持不变——案例研究中,固定的 8 MB 负载在健康链路上耗时不到 1 秒,在退化链路上则耗时约 90 秒;如果退化程度更深,等待时间还会无限增长,而这恰恰是你最承受不起的时候。这也是两个数值可以比较的原因:用时相同、方向相同,且都在接收端测量。
BUDGET=20
# Channel A — the service under suspicion. Point it at the largest object you can name.
# --max-time aborts mid-transfer; -w still prints, and curl exits 28. That is a
# successful measurement, not a failure — read the rate, ignore the exit code here.
curl -s -o /dev/null --max-time "$BUDGET" \
-w 'A: %{size_download} B in %{time_total}s = %{speed_download} B/s\n' \
"http://<host>:<port>/<a-large-object>"
# Channel B — a completely different stack to the same host. `cat /dev/zero` streams
# until cut off, so the byte count is decided by the link, not by the payload size.
B=$(timeout "$BUDGET" ssh <host> 'cat /dev/zero' 2>/dev/null | wc -c)
echo "B: $B B in ${BUDGET}s = $((B / BUDGET)) B/s"
不要用
dd自己输出的摘要行替代通道 B 的数值。dd if=/dev/zero …报告的是它写入自身标准输出的速度;对于通过管道进行的 SSH 传输,数据会被管道和 SSH 的通道窗口吸收——它测量的是本地写入速度,而不是网络传输速度,也不会计入连接建立时间。在案例研究的健康链路上测得,原始 SSH 端到端传输了 33.6 MB,速度为 44 MB/s,而同一次传输中dd自报的速度是 54.9 MB/s。发送端的速率只是网络传输速率的上限,绝不是对它的测量。像上面那样,从接收端为整个调用计时。
预计两个通道的绝对数值会不一致——这不是问题。 在同一条健康链路上,原始 SSH 测得约 44 MB/s,而通过媒体服务获取数据的速度为 11–16 MB/s,因为通道 A 除了网络传输,还要承担服务自身的读取和编码工作。因此,下面的判定规则关注的是数量级,而不是数值相等:你要测试的是两个通道是否处于同一档位,而不是它们是否报告相同的数值。在健康链路上相差 3 倍,而在退化链路上都降至 0.1 MB/s 的两个通道,已经给出了你需要的全部信息。
阅读表格前,请确认两个探测确实都已运行。 这是陷阱 17 在你自己的测试方案中的体现,也是最容易让你在这里得出自信却错误结论的方式:从未传输任何数据的探测会产生较低或为零的速率,表格会据此判定为“慢”,并直接指向“链路”——但这个结论是基于一次根本没有发生的测量得出的。两项检查都很简单:
- 通道 A:
size_download是否接近对象的实际大小?DNS 失败(curl 退出码 6)、连接被拒绝(退出码 7)或 404 错误页面都会很快返回,且数据量很小——这甚至可能被读作速度很快,从另一个方向让你落入错误的行。只有退出码 28(--max-time在传输过程中触发)才表示“测量有效,只是对象大于预算”。 - 通道 B:
$B是否合理地大于零?该命令中的2>/dev/null会隐藏 SSH 自身的错误,因此无法通过身份验证的主机也会静默地给出B=0。如果数值看起来不对,请去掉2>/dev/null再运行一次。
将结果视为四种情况的测试——四种结果都有可能出现,其中三种会告诉你停止手头正在做的事:
| 通道 A | 通道 B | 结论 |
|---|---|---|
| 慢 | 慢 | 链路问题。 两套互不相关的技术栈不可能因为互不相关的原因而以相同速率变慢。停止调整应用——应用内部的任何操作都无济于事。转到下文的“链路发生了什么变化”。 |
| 慢 | 快 | 应用/服务问题。 链路显然能传输数据,A 却不行。现在你有了一个可用于二分排查的正常对照。 |
| 快 | 慢 | 暂时不能得出结论——异常的是你的对照。 可能是 SSH 特有的开销(弱 CPU 上的加密算法、对 SSH 流量进行限速的某个跃点),也可能是 B 走了不同的链路。不要断定“服务没问题”;换用第三套技术栈替代 B,然后重新运行。 |
| 快 | 快 | 探测未能复现症状。 不要宣布事件已解决。实际工作负载在某个尚未复现的维度上有所不同——方向(上传还是下载)、并发数、对象大小或一天中的时间。更改其中一项后重新测量,再决定是否相信这个正常结果。 |
| “速率相同”有多接近? 在此处,误差在 约 20% 以内就算速率相同——两个通道在封装、加密和每个对象的开销上都有差异,因此不要求也不指望完全相等。判断看的是数量级:两个栈分别测得 0.09 和 0.11 MB/s,算一致;0.1 和 12 MB/s 则不算。如果差距介于约 20% 和 2 倍之间,就先视为无法定论,并扩大预算或对象大小,而不是贸然选边。 |
没有 SSH 时如何配对通道 B。 要求仅仅是同一主机、同一方向、没有共用应用代码——SSH 用起来方便,但并无特殊之处。以下方式都可行:主机上的另一个无关服务(指标端点、对象存储、静态文件服务器)、容器运行时传输(从该主机执行 docker cp)、能在两端运行原始吞吐量工具时使用该工具(iperf3 -c),或使用主机自己的软件包/构件镜像。不算的是同一服务的第二个端点——它与要排除嫌疑的代码共用,因此结果一致也证明不了什么。
接着找出路径发生了什么变化。 一旦确认问题出在路径上,变量通常是拓扑,而非硬件:如今与昨天相比,流量实际经过了哪条路由。对于网状 VPN(Tailscale、Nebula、ZeroTier)和拆分隧道代理,同一个逻辑地址可能通过 LAN 直连、WAN 直连或中继提供服务——容量可能相差一个数量级,而且状态字段或连接地址不会发生变化。请让传输层报告它实际使用的路径(tailscale ping <host> 会输出端点,以及回复是否经由中继)。
如果没有可供比较的历史正常测量值——首次发生故障时通常如此——仍然可以继续排查,因为“情况是否发生了变化?”这个问题有个成本更低的替代方案:从其他位置测量同一路径。 在不同网络上的第二个客户端,或主机通过环回测量自身,都可以帮助界定问题,而无须任何历史数据。然后把今天测得的数值记录为此前缺失的基线;第二次遇到此故障时,排查成本会比第一次低得多,但前提是有人把数值记下来。
最后,审查错误假设已经造成的影响。 链路降级不只是让一切变慢——它还会悄然使你基于快速路径校准的所有超时失效,而这些超时会产生看似成功的损坏构件。在宣布故障处理完毕前,重新验证降级期间传输的所有内容;参见陷阱 18 和步骤 7。
在改变语义前,先重放原始工作负载。 路径或节点变更恢复了有代表性的传输速率后,使用原有的完整性要求重新运行最初失败的确切操作:克隆故障就完整克隆 Git,上传故障就传输完整正文,流故障就运行原始流式协议。小型健康检查请求、ls-remote 或部分/浅克隆,只能证明它们自身较小的路径可用。如果原始工作负载如今能在现有截止时间内完成,这一证据就说明没有必要采用部分克隆或放宽超时等应用层变通方案。如果仍然失败,就保留该结果,只有到那时才调查工作负载语义。这遵循 RFC 6349 对持续 TCP 吞吐量与 RTT/存活状态的区分:验证必须传输足够的数据,使传输容量的影响超过连接建立成本。
步骤 1:在每个环节收集直接证据
在调用不熟悉的设备或本地客户端 HTTP/RPC 端点收集证据之前,先阅读设备吞吐量实地调查;先完成离线探查并分类影响,再单独执行调用。
提出假设之前,先收集:
- 请求路径上每个环节的服务端日志
- 客户端日志(浏览器开发者工具 HAR、CLI 调试日志、SDK 跟踪)
- 事件时间窗口内的指标(RPS、延迟、错误率、连接数、CPU/内存)
- 可用时的分布式跟踪
- 症状出现在网络传输层时的抓包(参见 references/packet-capture-recipes.md)
如果其中任何一项缺失且与问题相关,先补齐缺口,再做猜测。添加 TRACE_* 环境变量标志并重启容器,也胜过花一小时叠加假设。references/instrumentation-patterns.md 中的检测模式风险低、由环境变量控制,并且可以安全地永久部署到生产环境。
通过反向代理访问日志区分上传与处理阶段
Caddy 和 nginx 日志是证伪“后端速度慢”这一判断的最简单方法。重点关注以下三个字段:
| 字段 | Caddy JSON 键 | nginx 变量 | 含义 |
|---|---|---|---|
| 总耗时 | duration | $request_time | 从客户端收到第一个字节到向客户端发送最后一个字节(或连接关闭)的时间 |
| 已接收的正文大小 | bytes_read | $request_length (大致值) | 代理实际从客户端读取的字节数 |
| 声明的正文大小 | request.headers.Content-Length | $content_length | 客户端声明将发送的字节数 |
| 响应状态 | status | $status | 0 / - 表示代理从未写入响应 |
关键模式:
bytes_read < Content-Length且duration ≈ timeout→ 上传超时。bytes_read == Content-Length且status为 5xx → 处理超时。status == 0且bytes_read < Content-Length→ 客户端/CDN 在上传完成前关闭了连接。
跨技术栈跟踪单个请求
对于 技术栈(Cloudflare → Caddy → → → ),标准跟踪流程如下:
- Cloudflare:从客户端错误信息或 Cloudflare Logpush 中获取
Cf-Ray和时间戳。 - Caddy:运行
docker logs <gateway-container> | grep <Cf-Ray>→ 提取X-Request-Id(Caddyuuid),并确认bytes_read、duration、status。 - :检查
docker logs <provider-gateway-service>,查找Client request error: aborted或请求/响应日志。 - :运行
grep <X-Request-Id or timestamp> /data/<upstream-capture-service>/log/access.log→ 确认请求是否到达 ,以及上游处理耗时。 - :检查
docker logs <new-api-container>,查找计费/渠道错误。
如果在步骤 3–5 中始终找不到请求 ID,说明故障发生在边缘层或正文上传期间。
按客户端 IP 聚合以发现模式
单个 524 可能只是偶发情况;如果 524 集中出现在一个 IP 和一个路径上,就是明显的线索。运行如下聚合命令:
# Caddy JSON example: count failures by IP and body size for an endpoint
python3 -c "
import sys, json
from collections import Counter, defaultdict
stats = defaultdict(lambda: {'total': 0, 'fail': 0, 'slow': 0, 'max_cl': 0})
for line in sys.stdin:
d = json.loads(line)
req = d.get('request', {})
if req.get('uri', '').startswith('/<openrouter-path>'):
ip = req.get('headers', {}).get('Cf-Connecting-Ip', [''])[0]
cl = int(req.get('headers', {}).get('Content-Length', ['0'])[0] or 0)
dur = d.get('duration', 0)
status = d.get('status', 0)
s = stats[ip]
s['total'] += 1
s['max_cl'] = max(s['max_cl'], cl)
if status == 0:
s['fail'] += 1
elif status == 200 and dur > 60:
s['slow'] += 1
for ip, s in sorted(stats.items(), key=lambda x: -x[1]['fail']):
print(f\"{ip}: total={s['total']} fail={s['fail']} slow={s['slow']} max_cl={s['max_cl']}\")
" < caddy-access-log.jsonl
如果某个 IP 的失败次数占主导,且其 max_cl 较大,应先调查上传带宽/路径,再检查后端。
第 2 步:提出带有证伪条件和威胁模型边界的假设
列出三个或更多合理的原因。对每个原因,写三句话:
- 什么情况可以证实它?(容易验证,而且常常会产生误导)
- 什么情况可以证伪它?(证伪条件——这才是关键)
- 干预会针对哪个层边界?(威胁模型问题——迫使你明确修复适用于哪个位置)
第三个问题可以避免一种常见的反模式:提出的修复作用在了错误的链路跳点上。例如,针对_上游_空闲超时问题,如果“保活”修复是向下游客户端写入字节,那就毫无用处——干预针对的边界与问题所在的边界不同。提前指出边界,可以在开始编码前发现这种不匹配。
如果你无法明确说出证伪条件,这个假设就不可证伪。请标记它,但不要据此采取行动。如果你无法说明某项拟议修复针对哪个边界,就说明你还不清楚这项修复实际会做什么。
第 3 步:进行决定性实验
对于网络层问题,使用分层隔离,并验证路径之间的差异。以 Client → CDN → LB → Origin 为例:
| 路径 | 路由 | 与完整路径的差异 |
|---|---|---|
| A | 经由 CDN 和 LB 的完整路径 | 基线 |
| B | 使用 --resolve 指向已验证的源站 IP | 移除 CDN、LB 及其连接路由 |
| C | 服务器环回 | 也移除外部客户端到源站的路由 |
如果只有 A 失败,就将差异定位到已移除的 CDN/LB 路径;添加一条保留 LB 的路径,以区分这两个层。如果 A 和 B 失败但 C 通过,就调查 C 额外移除了什么。请阅读 references/layered-isolation-experiment.md,了解完整的结果解读、针对间歇性症状的有界 A/B/A 检查,以及带有受控空闲间隔的模拟空闲上游。
对于非网络领域:
- 性能:每次只改变一个变量的受控基准测试
- 正确性缺陷:能够复现问题的失败测试用例
- 间歇性问题:采样跟踪,并有界地恢复原始条件;如果对比结果无法再次出现,就保留不确定性
第 4 步:按需添加检测
如果决定性实验需要当前无法观察到的信息,就添加相应检测——不要跳过这一步。标准模式是由环境变量控制的检测,它应当:
- 默认关闭(稳定运行时零开销)
- 通过一个环境变量启用,无需更改代码
- 写入便于 grep 的日志标签(
[SSE-CHUNK] ts=... req=... bytes=...) - 永久部署到生产环境——以便在未来的故障中复用
请参阅 references/instrumentation-patterns.md,了解本次事件中用于诊断 上游静默 125 秒问题的确切模板。
第 5 步:执行并记录
完整记录并运行一次实验:命令、环境、输入、观测到的输出、墙上时钟时间戳。与第 2 步中的预测进行比较。如果实际结果符合预测,说明假设经过了校准。如果不符合,说明假设是错误的——不要用临时编造的辅助假设来挽救它(“哦,不过也许 X 也会造成干扰……”)。返回第 2 步,从头提出新假设。
第 6 步:交叉审查
在确定根本原因或发布修复之前,请邀请独立审查者质疑结论。向他们提供相同的证据,要求他们证伪,而不是确认。对他们提出的每项发现,应用以下四个问题进行筛选:
- 概率——这种情况真的会发生吗?
- 成本——修复与忽略的成本分别是什么?
- 真实场景——这适用于用户的实际业务场景吗?
- 验证——我能否以较低成本证实或证伪它?
将每项发现归类为:真实问题/部分正确/不太可能/有害。不要把 Agent 的原始输出直接粘贴给用户;先进行筛选。请参阅 references/counter-review-pattern.md。
第 7 步:修复并验证
应用修复。重新运行第 3 步中相同的决定性实验。使用能够稳定复现症状的相同设置,确认该症状不再出现。如果修复后无法再复现修复前的状态,就无法证明修复有效——在宣布成功之前,先弄清楚为何复现条件丢失。
第 8 步:记录走过的弯路
调查过程中走过的弯路比正确答案更有价值。撰写事件报告,记录以下内容:
- 症状及直接证据
- 尝试过的每个假设,以及如何证伪
- 决定性实验的设计及结果
- 修复及验证
- 新增的监控或检测手段
未来的调查人员——包括未来的自己——会阅读这份报告,以避免陷入同样的认知陷阱。
常见认知陷阱
-
间接证据汇聚。 五条间接线索都指向同一个方向,感觉就像证据确凿。但事实并非如此。如果直接探测成本很低,就直接运行探测。
-
字段语义混淆。
duration=5.95s可能表示总耗时(某个工具)、处理程序执行阶段的耗时(另一个工具),也可能表示 TTFB(第三个工具)。引用数值字段前,务必根据文档或代码核实其语义。 -
单一原因偏见。 多层系统故障往往源于多层缺陷的组合。修复直接原因的同时,也要记录放大因素,以便加固下一层防御。
-
名称假设。 标记为
spot-instance的资源不一定真的是竞价实例。要通过 API 核实属性,而不是依赖元数据名称。 -
探测自我验证。 通过故障连接运行诊断来测试该故障连接,得出的结果无法解读。务必使用独立探测进行交叉验证。
-
假设补救循环。 当证据与假设矛盾时,人们很容易加上限定条件(“是的,但只在情况 X 下”)。不要这样做。如果第一个证伪条件触发,就放弃该假设。
-
未经核实的前提。 调查一个从未被直接观察到的症状——它只是从用户的不满、警报标题或下游影响推断而来。先核实(Step 0.5)。不要调查传闻。
-
威胁模型不匹配。 提出的修复针对错误层级——试图通过在下游写入字节来解决上游问题,或者调整一个从未触发超时的跃点上的超时设置。明确每个假设针对的边界(Step 2),就能暴露这种问题。
-
反向路径/方向不对称。 A→B 正常 ≠ B→A 正常。对节点进行外部探测,只能证明该节点的回程/入站方向正常;网络路径和拥塞具有方向性。在宣布某个跃点正常之前,应从用户所在一侧,沿着用户流量经过的相同方向进行测量(从受影响的源端运行 TCP 模式的
mtr/nexttrace)。 -
边缘超时伪装成上游客户端中止。 Cloudflare 返回的 524 可能导致源站代理(Caddy/nginx)将上游连接记录为“客户端中止”(
status=0,Client request error: aborted)。源站处的中止确实发生了,但其_原因_是 CDN 边缘先超时。将中止归因于客户端之前,务必对照边缘错误代码、边缘时间戳和源站日志。参见 Step 0.6 中关于上传与处理的操作流程。 -
假设代理规则列表顶部的规则优先于 CNAME 匹配。 会解析 CNAME 的代理客户端,可能会针对解析后的 CNAME 链应用规则,而不只是针对原始主机名。
DOMAIN-SUFFIX,<cname-suffix>,DIRECT规则可能会覆盖显式的DOMAIN,<target>,PROXY规则。检查配置,并通过代理测试主机名路径与 IP 路径,以此核实。 -
代理节点的 DNS = 客户端的 DNS。 代理节点解析主机名的结果可能与客户端不同。客户端侧的 DoH 查询可能返回可用 IP,而代理节点却返回被屏蔽或不可路由的 IP。使用
curl -x proxy -H 'Host: host' -I https://<working-ip>进行测试,以区分 DNS 问题与可达性问题。 -
指纹 ≠ 身份。 服务横幅、端口特征或 MAC OUI 都是容易被仿冒的线索,而非设备身份的确凿证明。某个 5000 端口的响应方带有
Server: AirTunes/…标头,且没有_raopmDNS 广播,乍看像是 DIY Linux AirPlay 接收器(shairport-sync)——实际上它是 macOS AirPlay 接收器(ControlCenter 监听 5000/7000 端口),而新版OpenSSH_10.x横幅才是识别线索。同一事件中,一个 Realtek OUI MAC 让人以为“和 NAS 属于同一家厂商”,但实际上它来自连接到 Mac 的 USB 以太网适配器。在断定设备是什么之前,要检查能表明其自身身份的证据:SSH 主机密钥是否与~/.ssh/known_hosts中的记录匹配(决定性证据——一台主机对应一个密钥)、mDNS 主机名解析,以及 AirPlay/infoplist(自报的名称/型号/osBuildVersion)。应将横幅和 OUI 视为待证伪的假设,绝不能当作结论。 -
在一个网段不可达 ≠ 已失效。 探测结果只能证明探测所运行的 L2 域上的情况。更换路由器后,旧路由器在以太网上不响应 ARP,看起来“已失效”,但它的 Wi-Fi AP 仍在广播并提供 DHCP,因此使用已保存凭据的设备会悄悄接入一个没有 WAN 的网络。宣布设备消失前,应从目标设备自身的视角确认其不可达(例如在设备本身运行
ipconfig getifaddr en1,检查其他接口);任何拓扑变更后,还应关闭已退役设备的电源——仍在提供 DHCP 的“失效”路由器会像陷阱一样不断吸引设备接入。 -
拓扑变更会使手动 IP 设备成为孤岛。 DHCP 客户端会自动跟随新网络;手动/静态 IP 设备则会保留旧网关和 DNS,变成无声的孤岛——没有任何设备能连接到它,它也无法连接任何设备。macOS 上有一种情况更难察觉:“Manually Using DHCP Router Configuration”(手动 IP + 从旧 DHCP 获取的路由器配置):地址看起来是刻意设置的,但网关已过时。每次更改路由器/子网后,在宣布迁移完成前都要全面排查:检查旧子网上的 ARP 条目、解析到旧子网地址的 mDNS 名称,以及所有已知静态 IP 设备(服务器、NAS、打印机)的网关和 DNS 是否已重新核实。
-
把路径类型字段误当成路径容量字段。 Mesh-VPN 和代理状态行报告的是如何连接到某个对端——
direct还是relay,以及端点地址。它回答的是“是否打通了 NAT?”,绝不是“速度有多快?”。两者之间的差异可能很大:在这个陷阱背后的事故中,状态行在 0.09–0.11 MB/s 和 11–16 MB/s 两种情况下都显示为active; direct <endpoint>——同一字段、同一个词,速度却相差两个数量级——因为较慢时走的是跨 WAN 的直连路径,较快时走的是跨 LAN 的直连路径。RTT 也不是更好的预测指标:较慢的状态 RTT 看起来更低,为 12 ms;较快的状态则是 4 ms。两者测量的都是真实指标,但都不能衡量容量。测量速率(步骤 0.7),并把状态字段视为拓扑信息。 -
把自己的探测请求造成的问题归咎于远端主机。 在断定“服务器很慢”之前,先确认发出的请求确实是想发出的请求。格式错误的标识符往往比有效标识符走得更慢的路径——查找未命中会触发完整扫描,然后返回错误——因此,错误的探测会制造出你正在寻找的那种“另一端不堪重负”的迹象。真实案例:一个 Windows 风格的对象键(
…\archive\…)经过一个会把\a当作 BEL 字节处理的 shell 插值后,键被破坏,无法命中任何索引,服务扫描了约 21 秒,然后返回了 17 字节的404。如果将其解读为“花 21 秒返回空结果——主机过载”,调查方向险些因此完全偏离。关键线索是负载大小:传输 17 字节耗时 21 秒不是带宽问题的迹象,而是服务端扫描的迹象;而扫描一个不存在的对象,问题通常出在你的键,而不是对方的磁盘。改用不经过 shell 的方式重建探测请求(使用带参数向量的脚本),然后重新测量,再判断问题归属。 -
错误的容量假设会悄无声息地破坏产物,却仍能通过成功检查。 吞吐量下降不会只表现为“变慢”——它会使所有依据正常路径校准的超时都失效,而被你自己的超时终止的传输会留下截断文件,而不是报错。你是否能发现,完全取决于成功检查测量的是什么——而一个看起来合理的大小下限也救不了你。这个陷阱背后的事故中,获取循环检查的是
HTTP 200 && bytes > 1000,听起来像真正的完整性检查,但在 35 次下载中,全部都通过了检查,其中 14 个文件却遭到了截断。健康速率下宽裕的 180 秒上限,在降速后变得根本不可能完成;被终止的传输仍带有200,因为状态行会在响应正文停止传输很久之前到达;它们也仍超过了 1000 字节,因为与任何你可能想到的阈值相比,一个被截断的数 MB 文件仍然很大。任何字节数下限都无法区分“完整”和“大部分完整”——这是文件格式的属性,而不是文件大小的属性。有两种可靠的修复方法:检查传输的退出状态,而不只是响应状态(HTTP 200 描述的是响应开头;只有退出代码才能说明响应是否结束),以及根据产物自身的格式进行验证——带有终止标记(%%EOF、IEND、结束帧)的容器可以证明自身是否完整。这个检查也能用于事后审计影响范围:带有终止标记的文件与解析器接受的文件完全一致,均为 21 个,从而把“有些下载可能有问题”变成了一份准确清单。JSON 解析也不是传输完整性检查。 有界 Python HTTP 读取即使在声明的正文尚未完整时,也可能返回有效 JSON。在将解析成功视为接收成功之前,请阅读有界响应检查;它保留大小限制,并涵盖完整的定长、分块和连接关闭定界响应。
-
通配地址监听器不一定能通过每个网络接口访问。
lsof -nP -iTCP -sTCP:LISTEN会为绑定到通配地址的套接字打印*:59807。这只是关于 BSD 套接字层的事实,并不能回答“这个端口能否从互联网访问”。在 macOS 上,基于 Network.framework 构建的服务会绑定通配地址,然后为每个接口打开一个收件箱,因此同一个*既可能对应通过物理网卡提供服务的进程,也可能对应只在 Apple 点对点无线电上响应的服务。在导致这个陷阱的事件中,一台刚获得公共 IPv6 地址的设备接受了暴露面审计;审计仅凭*:port就将四个端口判定为面向互联网。统一日志却对其中三个端口给出了不同结论:rapportd的监听器记录中,所有interface:字段都指向awdl0,没有一个指向en0。AWDL 是 Apple 的点对点无线电,只承载fe80::链路本地地址,并且要求另一台设备处于无线电覆盖范围内——因此,这些端口从未能从链路以外的任何地方访问到。同一台设备的同一份日志中,sharingd则通过en0和五条隧道分发服务,这才是真正通配地址监听器的样子。同一审计也漏掉了两个确实在每个接口上监听的端口,因为审计人员读到看起来熟悉的进程名称时就停了下来。反制方法:从框架为自身记录的日志中读取接口信息(操作方法以及两个log show注意事项见 references/instrumentation-patterns.md),然后用地址类别交叉核验,因为地址类别独立于日志所说的一切——全局可路由地址只会出现在真正的网络接口上,因此,若观察到服务在该接口上响应,就说明它确实暴露在外;若只在fe80::上看到服务,它就从未离开链路。lsof列出的是套接字,而不是对可达性的判定。这是原则 6 的前提:只有当你清楚某种仪器能回答哪一层的问题时,使用仪器而非询问用户才算改进。 -
在最新的客户端和最近的物理证据上验证网络变更。 两者都不是正确的验证对象。“克隆 SSID + 密码”的 AP 替换看起来会在数小时内顺利完成,因为较新的客户端——手机、新音箱——会无缝漫游,而这些设备恰恰从未面临风险。故障群体是无法解析新 AP 默认设置的旧款 IoT 硅片(WPA2/WPA3 过渡模式、仅支持 ax 的 2.4 GHz);故障会延迟出现——每次 DHCP 续租或取消身份验证都会造成一次,持续数天到数周——因此,替换工作早已被宣布完成,受影响设备才陆续出现。相同的视角错误也会出现在物理层面:一个“就在那儿、通着电”的集线器,只能证明有电。集线器本身也是 Wi-Fi 客户端,只有从网络侧才能判断它是否已加入网络(查看它的 ARP 表中的 OUI、mDNS 记录)。反制方法:更换 AP 时,确保四项一致——SSID、密码、安全模式、频段/PHY 兼容性;并用最旧的客户端而非最新客户端进行验证;如果一整组设备同时失效,先检查集线器的关联状态,再动任何设备,因为硬件故障不会以集线器为中心成簇出现。完整案例和设备清点方法见 references/case-iot-router-migration-wpa3.md。
references/cognitive-traps.md 收录了陷阱 1–12 的扩展说明(恢复循环警告、字段语义示例)。陷阱 13–20 则记录在其来源所在的位置:13–15 在 LAN/拓扑资料中,16–18 在 references/case-throughput-collapse-no-errors.md 中,19 在上文内联说明,其命令操作方法见 references/instrumentation-patterns.md,20 也在上文内联说明,其案例见 references/case-iot-router-migration-wpa3.md。
客户端代理 / VPN / TUN 路由错误
如果症状仅出现在特定客户端(某台设备上的浏览器无法访问,其他设备或网络正常,或者关闭代理 / VPN 后故障消失),那么代理客户端本身就是一个网络跳点。应将其视为网络跳点来排查。
快速差分检查清单:
- DNS:操作系统解析到哪个 IP?如果是虚拟 / TUN IP(例如
198.18.x.x),说明代理客户端正在拦截 DNS。 - 路由:
route -n get <ip>会显示数据包从哪个接口发出。虚拟 IP 经由utun5路由是 TUN 模式下的正常情况;如果真实 IP 只经由 TUN 路由,而物理接口无法访问它,则本地直连不可用。 - 代理端口:本地代理是否正在监听?
lsof -P -i TCP:<port>可以确认。分别在启用和停用代理的情况下进行测试。 - 经代理访问主机名与 IP:
curl -x http://127.0.0.1:<port> -I https://<host>- 自行解析主机名(DoH),然后运行
curl -x http://127.0.0.1:<port> -k -H 'Host: <host>' -I https://<ip>如果第二条命令成功而第一条失败,说明代理节点的 DNS 返回了与客户端 DoH 查询不同的错误 IP。
- 物理接口连通性:临时强制真实 IP 经由
en0(或当前使用的物理接口)发出。如果此路径失败而 TUN 路径正常,则本地网络无法访问目标;必须使用代理 / TUN。 - 规则 / CNAME 交互:检查代理配置中是否存在匹配目标 CNAME 后缀的规则。如果客户端会根据解析出的 CNAME 进行规则匹配,那么
DOMAIN-SUFFIX,<cname-suffix>,DIRECT规则可能会覆盖显式的DOMAIN,<host>,PROXY规则。
如果以上检查都表明代理客户端解析到了错误的 CNAME,或依赖错误的代理节点 DNS,请参阅 references/case-proxy-tun-cname-override.md 中的修复模式。
反模式——应明确避免的做法
- 尚未找到证伪依据就着手修复。“可能是 X,我来重启 / 调整 / 升级一下。”这会把学习机会变成无法防止问题复发的神秘修复。
- **全盘接受 Agent 反向审查的发现。**Agent 往往会过度报告风险。采取行动前先筛选(参见上文的四问筛选法)。
- **绕过 IaC 进行临时生产环境修改。**如果排查需要更改生产环境,应先修改事实来源,再应用更改——否则“修复”会在下次部署时消失,配置漂移也会掩盖真实状态。
- **仅凭单一观察就断定根因。**应先尝试证伪。
- **没有重新运行失败实验就写“现在应该可以了”。**务必重新验证。
案例研究
以下典型案例展示了该方法在不同故障模式下的应用:
-
references/case-sse-rst-130s.md — 一场持续 5 小时的调查中,助手反复得出错误结论。正确答案是:Cloudflare 边缘 HTTP/2 流空闲超时为 126 秒,而 在生成 的 tool_use 期间未发送 SSE ping,使问题进一步加剧。一个子 Agent 设计了一个包含 3 条路径的分层隔离实验,并使用模拟的空闲上游后,10 分钟内就找到了答案。
-
references/case-cloudflare-524-upload.md — 对
<api-domain>/<openrouter-path>的请求遇到 Cloudflare 524:一个约 6 MB 的 POST 请求正文,从美国客户端上传到 源站所花的时间,超过了 Cloudflare 默认的源站读取超时时间。通过比较bytes_read(4.1 MB)和Content-Length(6.0 MB),并确认请求从未到达<upstream-capture-service>或<new-api-container>,找到了关键线索。本案例是上述上传与处理排查方法以及“边缘超时伪装成客户端中止”陷阱的来源。 -
references/case-proxy-tun-cname-override.md — 一个客户端侧
<proxy-client>TUN 案例:尽管配置顶部有显式 PROXY 规则,<auth-domain>仍报错ERR_CONNECTION_CLOSED。根因是DOMAIN-SUFFIX,<cname-suffix>,DIRECT规则匹配了目标的 CNAME 链,同时代理节点自身的 DNS 返回的 IP 与客户端 DoH 查询得到的不同。修复方法使用[Host]映射和use-local-host-item-for-proxy。 -
references/case-throughput-collapse-no-errors.md — 这里唯一一个完全没有错误字符串的案例:整个过程中 HTTP 200、RTT 为 12 ms、健康检查端点耗时 0.04 s,但链路速度只有 0.09–0.11 MB/s。调查花了 3 小时,先后得出“验证过程很慢”→“主机的上行带宽肯定只有约 1 Mbps”→“问题出在中继”这些看似合理却都未经验证的结论。两个共享同一路径但不涉及应用代码的通道(一个 HTTP 服务和普通 SSH)测得的速度相差约 20%,只用一条命令就确定了问题所在:瓶颈在路径,而不是服务。本案例是步骤 0.7 和陷阱 16–18 的来源,其中包括:基于错误速率推导出的 180 s 超时,如何截断了 35 个下载中的 14 个;这些下载都通过了
200 && bytes > 1000检查。 -
references/case-iot-router-migration-wpa3.md — 这里唯一一个关于客户端侧关联的案例:“无缝”克隆 SSID 并更换路由器三周后,一组以集线器为中心的 IoT 设备离线了——而集线器本身根本没有接入网络。整个证伪过程都通过一台笔记本电脑完成(mDNS 盘点排除了 AP 隔离,ARP+OUI 盘点发现集线器的 OUI 不在网络中,信标参数则显示当前使用 WPA2/WPA3 混合模式,而旧 AP 仅支持 WPA2)。决定性的测量来自一次干预:将安全模式切回仅 WPA2 后,12 台设备在几分钟内恢复联网。本案例是路由器更换配置对齐清单、IoT 被动盘点探测方法和陷阱 20 的来源。
在将此技能应用于陌生的问题领域之前,请先阅读这些案例;错误转向的过程才是其中的教学重点。
参考文件
- references/layered-isolation-experiment.md — 3 路径技术、模拟上游模板、结果矩阵
- references/instrumentation-patterns.md — 由环境变量控制的 TRACE_*、便于 grep 的日志标签、部署检查清单
- 如需数据包级证据或排查尚未解决的隧道丢包问题,请阅读 references/packet-capture-recipes.md。
- references/counter-review-pattern.md — 4 Agent 团队构成、4 问筛选法、集成工作流
- references/cognitive-traps.md — 扩展示例、救援循环警告
- references/case-sse-rst-130s.md — 典型案例研究,包含错误转向时间线
- references/case-cloudflare-524-upload.md — 区分上传超时与处理超时的方案
- references/case-proxy-tun-cname-override.md — 客户端代理/TUN CNAME 规则覆盖与修复模式
- references/case-throughput-collapse-no-errors.md — 各项信号均正常但吞吐量下降;双通道容量隔离;截断产物的后续影响
- references/case-iot-router-migration-wpa3.md — 更换路由器数周后 IoT 设备组发生故障;WPA3 过渡模式与旧版芯片的兼容性;先从集线器入手的分解法与被动普查探测
脚本
- scripts/mock-idle-upstream.py — SSE 服务端,发送一个帧后空闲 N 秒。在分层隔离实验中将其用作上游,以便精确控制空闲间隔。
- scripts/layered-isolation-probe.sh — 执行 3 路径 A/B/C 对比并输出诊断矩阵。