诊断的目标是用证据缩小故障边界,并验证修复是否改变了原始症状。协议分层提供检查地图,但没有必要每次从网线查到应用,也不能因为某次 ping 成功就把整个网络层永久排除。
“访问不了”至少要展开为:哪个客户端、哪个网络 namespace、哪个时间窗口、哪个域名和端口、哪个协议、哪一步错误。还应区分所有请求失败、部分后端失败、仅大响应失败、首次访问慢、长连接空闲后失败等条件。
从应用症状选择最有区分力的对照
| 对照 | 尽量保持一致 | 能帮助区分 |
| 同客户端固定一个目标 IP | URL 主机名、TLS 身份、路径和请求 | DNS 结果/节点差异与服务行为 |
| 同服务分别走 IPv4、IPv6 | 请求、时段与客户端 | 地址族、路由与后端差异 |
| 同路径小响应与大响应 | 后端及业务处理尽可能一致 | 尺寸、传输持续时间与缓冲问题 |
| 同一节点冷连接与复用连接 | API、负载与认证 | 建连阶段与应用阶段 |
| 故障窗口两端采集 | 五元组、时间和采集范围 | 请求/响应在哪一段不可见 |
同时更换 DNS、代理、HTTP 版本和目标 IP,成功之后也难以知道哪项起作用。每个对照都应记录改变了什么,以及哪些条件仍然无法控制。
DNS:查到记录,不等于应用用了这份答案
dig @192.0.2.53 app.example A
dig @192.0.2.53 app.example AAAA
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 的去向和响应的回程分开
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、没有重定向、没有连接复用,并且请求体发送影响已包含在首字节前等待中。
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时间。
{"id":"curl-cumulative-waterfall","title":"五个计时值,为什么不能直接相加?","summary":"累计值包含之前的等待。阶段耗时来自相邻累计值相减;首字节前等待不等于服务器CPU时间。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"dns\" data-i18n=\"dns\"></label><output for=\"dns\" id=\"dns-value\"></output><input id=\"dns\" type=\"range\" min=\"0\" max=\"400\" value=\"20\" step=\"10\"></div><div class=\"control\"><label for=\"tcp\" data-i18n=\"tcp\"></label><output for=\"tcp\" id=\"tcp-value\"></output><input id=\"tcp\" type=\"range\" min=\"0\" max=\"400\" value=\"40\" step=\"10\"></div><div class=\"control\"><label for=\"tls\" data-i18n=\"tls\"></label><output for=\"tls\" id=\"tls-value\"></output><input id=\"tls\" type=\"range\" min=\"0\" max=\"400\" value=\"80\" step=\"10\"></div><div class=\"control\"><label for=\"wait\" data-i18n=\"wait\"></label><output for=\"wait\" id=\"wait-value\"></output><input id=\"wait\" type=\"range\" min=\"0\" max=\"400\" value=\"200\" step=\"10\"></div><div class=\"control\"><label for=\"receive\" data-i18n=\"receive\"></label><output for=\"receive\" id=\"receive-value\"></output><input id=\"receive\" type=\"range\" min=\"0\" max=\"400\" value=\"60\" step=\"10\"></div><div id=\"waterfall\" class=\"intervals\"></div><section class=\"panel\"><h3 data-i18n=\"values\"></h3><div id=\"values\"></div></section><div class=\"metrics\"><div><span data-i18n=\"total\"></span><strong id=\"total\"></strong></div><div><span data-i18n=\"sum\"></span><strong id=\"sum\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#values .tr{grid-template-columns:1.7fr .65fr .65fr}#values .tr>*{min-height:3.6em}.metrics strong{font-size:19px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function draw(){let end=0,sum=0;const names=['namelookup','connect','appconnect','starttransfer','total'],rows=[[t('tableField'),t('tableTotal'),t('tableStage')]],bars=[];['dns','tcp','tls','wait','receive'].forEach((id,i)=>{const n=+$('#'+id).value,start=end;end+=n;sum+=end;set(id+'-value',n);rows.push([names[i],end,n]);bars.push({start,end,label:id+' '+n+' ms',cls:i%2?'gold':''})});intervals('waterfall',bars,Math.max(1,end));table('values',rows);set('total',end);set('sum',sum);$('#total').dataset.value=end;}document.querySelectorAll('input').forEach(e=>e.oninput=draw);draw();","audio":false,"strings":{"dns":"DNS(ms)","heading":"五个计时值,为什么不能直接相加?","intro":"调节一个阶段耗时,观察它之后的累计里程碑一起移动。","receive":"后续接收(ms)","reset":"重置","scope":"新建直连HTTPS,无代理/重定向/复用,所有阶段成功。单位ms,顺序DNS→TCP→TLS→请求到首字节→接收,不作为其他curl场景通用公式。","scopeLabel":"模型范围与约定","sum":"错误地累加五个里程碑","tableField":"计时字段","tableStage":"阶段 / ms","tableTotal":"累计 / ms","tcp":"TCP(ms)","tls":"TLS(ms)","total":"真实总耗时(ms)","values":"累计值与相邻差","wait":"请求到首字节(ms)"}}
MTU 黑洞:用一组相互支持的证据确认
典型假设是:路径某处容不下较大的报文,报文被丢弃,而发送端又没有正确获知或适应更小 PMTU。握手和小请求能通、大数据持续停滞,是线索而非独占症状;应用缓冲、策略按内容过滤和接收窗口停顿也可能相似。
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 assert path_mtu - 40 - 20 == 1340 print('探测 payload、IP 包长和 TCP 数据预算分开计算')
这些预算还没有计 TCP options 或额外封装。修复应针对真实路径 MTU、必要 ICMP 反馈或适用的 PLPMTUD 机制;MSS clamping 只影响 TCP 协商的相关方向,不能修复任意 UDP/QUIC,也不自动作用于已经建立的连接。因此不应把一条全局防火墙修改当成诊断步骤。DPLPMTUD
间歇性问题:看时间相关性,而不是累计值大不大
mtr 通过重复探测汇总响应,能帮助发现持续或间歇模式,但中间路由器对探测响应的限速不等于转发业务包也丢失。第 4 跳显示 60% 无响应、第 5 跳和目标正常,通常不支持“第 4 跳丢了 60% 业务流量”的结论。
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,客户端接口可见重发;当前证据将边界缩到两采集点之间,但尚未排除中间镜像漏采。”这比“网络抖动,已重启”更便于下一步协作。
修复后重做原始失败条件,比较错误率、时间分布和资源副作用。一次请求成功只能证明当时成功;间歇性故障需要足够覆盖原故障条件的观察窗口。若临时措施恢复了服务但根因未确定,应将恢复事实和根因假设分别记录。