图 1:本文封面,每个工具守一层。
「输入 ping 回车后发生了什么」是道经典面试题,那道题把单个命令的协议栈旅程讲透了。真到线上排障,问题换了形状:用户报「服务打不开」,你手里有一整盒工具,先抄哪个、按什么顺序、每个工具的回答能信到哪一层?本文就讲这套分层排障方法论。
核心原则:每个工具只能回答某一层的问题。
排障效率的本质是控制排查成本——先用最便宜的工具把故障圈到某一层,再换重工具。五个工具各自的地盘:
ping:网络层通不通。发 ICMP ECHO_REQUEST 等回包,回答且仅回答「网络层是否可达、往返多久、丢不丢包」。ping 的 man 手册直接写了故障隔离的标准动作:先 ping 本机确认网卡活着,再 ping 越来越远的主机与网关。但它不回答「应用好不好」——ping 通而服务挂掉是常态,很多机器还干脆禁 ping。
**traceroute:断在哪一跳。**ping 不通时,它利用 TTL 逐跳递减、归零回 ICMP 超时的机制把路径逐跳摸出来,默认每跳三个探测包、上限 30 跳,某跳不回就打星号。它回答「哪一跳断了」,不回答「为什么断」——星号可能只是那台路由器限速不回 ICMP。
**nslookup / dig:名字解到哪。**通着但服务不可用时,先怀疑域名:解析出的 IP 对不对、缓存里的记录是不是旧的。用 dig @某权威服务器 域名 对比本地答案,不一致就是解析层的问题。它回答「名字解到哪」,不回答「服务活没活」。
**netstat / ss:本机连接什么状态。**解析对了再往下看传输层:ss -tan 按状态聚合,SYN_SENT 成片说明建不上,ESTABLISHED 只涨不落说明建了不用。netstat 手册已注明该工具废弃、推荐 ss。它回答「连接处于什么状态」,不回答「包里装的是什么」。
tcpdump:实际交互的全部细节。最重也最贵:按表达式过滤抓包,逐字段看握手、重传、RST、载荷,必要时 -w 落盘离线分析。它几乎能回答所有问题,但成本最高——数据量大、要会读、生产上有合规门槛——所以留到最后。
一次完整的分层排障,用模拟走一遍。
下面这段模拟注入「DNS 缓存了旧记录」的故障现场,按从轻到重的顺序走一遍五个工具,用 Node.js 22.15.0 直接运行(node netdiag.js):
'use strict';
// 分层排障模拟:注入「DNS 解析到旧 IP」故障,按 ping -> traceroute
// -> nslookup -> netstat -> tcpdump 的顺序走一遍,看每层工具给出什么答案。
// 故障现场:域名被解析到一台不存在服务的旧机器
const world = {
domain: 'api.example.com',
realIp: '93.184.216.34', // 权威记录里现在的正确地址
cachedIp: '192.0.2.8', // 本地缓存里的旧地址(故障根源)
hops: ['192.168.1.1', '10.20.0.1', '172.16.0.2', '192.0.2.8'], // 到旧机器全通
capture: 'SYN 发出 -> RST,ACK 立刻回来(端口无人监听)',
};
console.log(`用户报告:访问 ${world.domain} 报连接失败\n`);
// 第一层:ping——回答「网络层通不通」
console.log(`$ ping ${world.domain}`);
console.log(` 来自 ${world.cachedIp} 的回复:icmp_seq=1 ttl=56 time=8.2 ms`);
console.log(` 判读:网络层可达。但注意——ping 的是解析出来的 ${world.cachedIp},对不对还没验证\n`);
// 第二层:traceroute——回答「哪一跳断了」
console.log(`$ traceroute ${world.cachedIp}`);
world.hops.forEach((h, i) => console.log(` ${i + 1} ${h} 1.0 ms`));
console.log(' 判读:路径每一跳都通,问题不在路上\n');
// 第三层:nslookup/dig——回答「名字解到哪」
console.log(`$ nslookup ${world.domain}`);
console.log(` Server: 10.0.0.2\n Address: ${world.cachedIp}(本地缓存的旧记录)`);
console.log(`$ dig ${world.domain} @8.8.8.8`);
console.log(` ANSWER SECTION: ${world.domain}. A ${world.realIp}`);
console.log(` 判读:两个答案不一致!本地缓存 ${world.cachedIp} != 权威 ${world.realIp},定位到解析层\n`);
// 第四层:netstat/ss——回答「连接什么状态」
console.log(`$ ss -tan dst ${world.cachedIp}`);
console.log(' SYN-SENT 0 1 192.168.1.7:54321 192.0.2.8:443(连旧地址,对端立刻 RST)');
console.log(' 判读:连接根本建不起来,与「域名解错」互相印证\n');
// 第五层:tcpdump——最重,回答「实际交互细节」
console.log(`$ tcpdump -n host ${world.cachedIp} and port 443 -c 4`);
console.log(` ${world.capture}`);
console.log(' 判读:抓包确认是端口级拒绝,而非链路丢包——修复动作:清缓存/等 TTL 过期\n');
console.log('结论:工具越重越晚用;每一步都先问「这个工具能回答什么、不能回答什么」。');
实测输出:
用户报告:访问 api.example.com 报连接失败
$ ping api.example.com
来自 192.0.2.8 的回复:icmp_seq=1 ttl=56 time=8.2 ms
判读:网络层可达。但注意——ping 的是解析出来的 192.0.2.8,对不对还没验证
$ traceroute 192.0.2.8
1 192.168.1.1 1.0 ms
2 10.20.0.1 1.0 ms
3 172.16.0.2 1.0 ms
4 192.0.2.8 1.0 ms
判读:路径每一跳都通,问题不在路上
$ nslookup api.example.com
Server: 10.0.0.2
Address: 192.0.2.8(本地缓存的旧记录)
$ dig api.example.com @8.8.8.8
ANSWER SECTION: api.example.com. A 93.184.216.34
判读:两个答案不一致!本地缓存 192.0.2.8 != 权威 93.184.216.34,定位到解析层
$ ss -tan dst 192.0.2.8
SYN-SENT 0 1 192.168.1.7:54321 192.0.2.8:443(连旧地址,对端立刻 RST)
判读:连接根本建不起来,与「域名解错」互相印证
$ tcpdump -n host 192.0.2.8 and port 443 -c 4
SYN 发出 -> RST,ACK 立刻回来(端口无人监听)
判读:抓包确认是端口级拒绝,而非链路丢包——修复动作:清缓存/等 TTL 过期
结论:工具越重越晚用;每一步都先问「这个工具能回答什么、不能回答什么」。
这个案例最反直觉的一点:ping 全通、路由全通,服务却是坏的——因为「通」的对象从一开始就错了。前两层工具的「一切正常」恰恰是误导,直到 dig 对比出两个答案才定位:每一层的问题都会伪装成其他层正常。
图 2:排障顺序即成本控制。
**把方法论收成三句话。**第一,自底向上:链路层、网络层、解析层、传输层、应用层,层越低工具越便宜,先排除。第二,每个工具开口前先想清楚它能回答什么、不能回答什么——拿 ping 的正常去证明应用健康,就是典型的越层误用。第三,抓包留到最后:它是终极答案也是最高成本,前面四层能解决八成故障。跨机房调模型服务抖动时,这套顺序就是定位「哪一跳开始劣化」的标准动作。
你的排障习惯是顺手抓包党还是严格分层党?有没有过一次「抓了半小时包,结果发现是 DNS」的经历?欢迎交流。