# 第 19 章 DNS、CDN 与网络排障


> 前几章讲协议"应该怎么工作"，本章讲它"实际上哪里坏了"。DNS 是唯一能把整条链路拖慢数秒的旁路系统。

## 学习目标

读完本章，你应当能够：

1. 画出 DNS 递归查询与迭代查询的两条路径，说清每一层的缓存位置、TTL 归属与负缓存作用范围；
2. 判断一组 DNS 记录能否合法共存（CNAME 与 MX、CNAME 与根域名），解释"切换 IP 前为什么要调小 TTL"的量化依据；
3. 用 `dig +trace`、`+short`、`@server`、`+tcp`、`+noedns` 定位解析异常，说明 `nslookup` 在排障中的信息缺失；
4. 对一次"网站访问慢"给出分层命令序列，按 `curl -w` 的 `time_*` 差值定位到 DNS、建连、TLS、TTFB、传输中的哪一段；
5. 写出 `tcpdump` 的主机/端口/标志位过滤表达式，并判断握手是否完成、有无重传、`RST`、零窗口；
6. 辨析 `ping`/`traceroute`/`mtr`/`telnet`/`nc`/`ss` 各自的能力边界，在 ICMP 被禁时给出替代手段。

### 前置知识检查

| 你需要知道 | 在哪一节补齐 |
|---|---|
| UDP 语义、TCP 三次握手与状态名（`SYN_SENT`/`ESTABLISHED`/`TIME_WAIT`） | 见第 16 章 16.1、16.3 节；本章只用它们读抓包结果 |
| 重传、重复 ACK、零窗口的成因 | 见第 17 章 17.2、17.4 节；本章只讲"抓包时怎么看出来" |
| TLS 握手往返数、`SNI`、证书链 | 见第 20 章 20.5 节；本章只用 `time_appconnect` 度量它 |
| IP 分片、MTU、UDP 报文长度上限 | 见第 15 章 15.4 节；本章用来说明 DNS 的 TC 截断 |

## 19.1 DNS 的完整查询流程：递归与迭代是两条路径

面试里"讲一下 DNS 解析过程"最常见的错误回答，是把浏览器缓存、根服务器、权威服务器串成**一条线性链**。真实的 DNS 是**两段拼接**：客户端到本地 DNS 服务器（local DNS server，也常叫递归解析器 recursive resolver）是**递归**，本地 DNS 服务器到根/TLD/权威是**迭代**。两段的责任划分、缓存归属、故障表现都不同。

### 19.1.1 两段路径的全图

```
 客户端（浏览器/应用）                   本地 DNS 服务器          根/TLD/权威
 ┌────────────────────────┐            ┌──────────────┐      ┌──────────────┐
 │ ① 浏览器 DNS 缓存       │            │              │      │              │
 │ ② OS 解析器             │            │              │      │              │
 │    nsswitch.conf:      │            │              │      │              │
 │    hosts: files dns    │            │              │      │              │
 │    → /etc/hosts 命中即止│            │              │      │              │
 │ ③ OS DNS 缓存           │            │              │      │              │
 │ ④ 读 /etc/resolv.conf   │  递归查询   │              │      │              │
 │    nameserver 10.0.0.53─┼───────────▶│              │      │              │
 └────────────────────────┘   (RD=1)    │ ⑤ 查自身缓存  │      │              │
                                        │   未命中 ↓    │      │              │
                                        │ ⑥ 问根 ───────┼─────▶│ .  返回 TLD NS│
                                        │ ⑦ 问 TLD ─────┼─────▶│ com. 返回权威 │
                                        │ ⑧ 问权威 ─────┼─────▶│ 返回 A 记录   │
                                        │ ⑨ 按 TTL 缓存 │      │              │
                                        └───────┬───────┘      └──────────────┘
                                                │ 递归应答 (RA=1)
                                                ▼
                                         客户端拿到 A 记录
```

三个容易答错的点。第一，**`/etc/hosts` 优先于 DNS** 不是"永远优先"，而由 `nsswitch.conf` 里 `hosts:` 一行的顺序决定：写成 `hosts: dns files` 就反过来，写成 `hosts: files myhostname dns` 是多数发行版默认。面试问"改了 hosts 不生效"，第一个要看这一行，第二个是应用自己缓存了（JVM 的 `networkaddress.cache.ttl`、Go 的 `GODEBUG=netdns`）。第二，**本地 DNS 服务器自己也会缓存**，所以第 ⑥～⑧ 步通常根本不发生——这也是"改了权威记录但不生效"的第一大原因，不是你的 TTL 没到，是别人的缓存还没到期。第三，**递归与迭代的分界在本地 DNS 服务器**：客户端发的是 RD=1（recursion desired）请求，只关心最终答案；本地 DNS 发的是 RD=0 请求，每问一台就要自己接着问下一台。根服务器**从不做递归**，以避免被放大攻击利用。

> [!IMPORTANT]
> 答题模板固定为："客户端→本地 DNS 是递归，本地 DNS→根→TLD→权威是迭代"。只答"一层层问下去"会被追问"那谁负责帮你问完"，这个追问就是在区分你是否知道 RD 标志位与两段责任划分。

### 19.1.2 TTL 与负缓存：为什么"删除记录"不一定立刻失效

| 缓存位置 | 缓存什么 | 有效期由谁决定 | 排障时怎么绕过 |
|---|---|---|---|
| 浏览器 DNS 缓存 | 域名 → IP | 浏览器自定上限（常为 60 s 量级） | 清浏览器缓存、开隐身窗口 |
| OS 解析器缓存 | 域名 → IP | 记录的 TTL | `resolvectl flush-caches` |
| `/etc/hosts` | 静态映射 | 永不过期 | 不适用（它是覆盖源，不是缓存） |
| 本地 DNS 服务器 | 正/负应答 | 正应答按 TTL；负应答按 SOA 的 `minimum` | 换 `@8.8.8.8` 这类公共解析器对比 |
| 权威服务器 | 不缓存 | 它是数据源 | 不适用 |

**负缓存（negative caching）** 是面试的高区分度点：查询不存在的名字时，权威返回 `NXDOMAIN` 并附上 SOA 记录，本地 DNS 按 SOA 的 `minimum` 字段（RFC 2308 口径）把这个"不存在"缓存一段时间。后果是：刚上线的新域名若已被解析器查过一次，**必须等负缓存过期才能解析成功**，表现为"我这边能通、别人那边 `NXDOMAIN`"。因此上线顺序永远是**先建记录再切流量**。同理，TTL 设为 0 也不等于"不缓存"：多数解析器有秒级下限（RFC 1035 里 0 只表示"不要复用"），客户端缓存更不受你的 TTL 完全支配。

### 19.1.3 切换 IP 时必须提前调小 TTL：一个算例

TTL 决定缓存能活多久，所以变更生效的最坏时间 ≈ `当前 TTL`。切换 IP 的标准动作是两阶段：**提前至少一个旧 TTL 的时长**把 TTL 从 `86400`（1 天）改成 `60`，等旧的长 TTL 缓存全部过期；然后改记录值（A → 新 IP）并等 `60 s` 让新值扩散；稳定后再把 TTL 调回 `86400`，降低解析器压力与放大攻击面。

算例：`TTL = 86400` 时直接改 IP，最坏情况下**一部分客户端 24 小时内仍在访问旧 IP**，而你很可能已经下线了旧机器——这就是"灰度切换变成部分用户 404"。把 TTL 降到 60 s 后，最坏生效时间从 `86400 s` 压到 `60 s`，代价是**解析器查询量上升 1440 倍**，所以必须在切换完成后调回去。

> [!TIP]
> 面试话术："TTL 不是'什么时候生效'，而是'多久之内一定生效'。调小 TTL 不是为了加快生效，而是为了**给最坏情况设上界**。"这句话能直接压住"TTL 就是缓存时间"这种一句话题。

## 19.2 DNS 报文结构与记录类型

### 19.2.1 报文的五个区段

DNS 报文固定 12 字节头（Header），之后是四个可变长区段。面试常考标志位与"哪些区段会变长"：

```
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            ID (16 bit，把应答与查询配对)                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|QR| Opcode  |AA|TC|RD|RA|   Z    |         RCODE (4 bit)      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     QDCOUNT（问题数，通常 1）  |     ANCOUNT（回答数）          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     NSCOUNT（权威数）          |     ARCOUNT（附加数，EDNS0 在此）|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 区段顺序：Question → Answer → Authority → Additional（后三段可为空）
 域名压缩：重复出现的域名用指针 0xC0xx 指回报文前面，故域名不可任意长
```
| 标志位 | 含义 | 排障时说明什么 |
|---|---|---|
| `QR` | 0 查询 / 1 应答 | 抓包时区分方向；两者 `ID` 必须一致，否则说明混了并发查询 |
| `AA` | Authoritative Answer | 为 1 表示来自该域权威服务器；问公共解析器时通常为 0 |
| `TC` | Truncated | 为 1 表示应答被截断，客户端**必须改用 TCP 重查** |
| `RD` / `RA` | Recursion Desired / Available | RD=1 是客户端请求递归；RA=0 说明它是纯权威，不能当解析器用 |
| `RCODE` | 0 `NOERROR`、2 `SERVFAIL`、3 `NXDOMAIN`、5 `REFUSED` | `SERVFAIL` 常见于 DNSSEC 校验失败或权威不可达；`REFUSED` 是被策略拒绝 |

`TC` 与 EDNS0 的关系是高频追问：传统 DNS 的 UDP 应答上限 **512 字节**（RFC 1035），超了就置 `TC=1`；EDNS0（RFC 6891）通过一条**伪记录（OPT RR）**把 UDP 承载能力协商到更大（常见 1232～4096 字节），并同时承载 DNSSEC 的 `DO` 位。所以"`dig` 看到没截断"，往往是因为对方支持 EDNS0，而不是记录真的小。

### 19.2.2 记录类型与"为什么有些记录不能共存"

| 类型 | 语义 | 面试要说的点 |
|---|---|---|
| `A` / `AAAA` | 名字 → IPv4 / IPv6 | 一个名字可挂多条 A 形成轮询；双栈域名有问题先看 AAAA |
| `CNAME` | 别名 → 规范名 | 只允许指向另一个名字而非 IP；解析器会继续追到底 |
| `NS` | 该域由哪些服务器负责 | 委托（delegation）的载体 |
| `MX` | 邮件交换，带优先级 | 标准上**不能指向 CNAME** |
| `TXT` | 任意文本 | 现用于 SPF/DKIM/DMARC 与域名所有权校验 |
| `SOA` | 区的起始授权 | 含序列号与 `minimum`（负缓存时长） |
| `PTR` | IP → 名字（反向解析） | 在 `in-addr.arpa`/`ip6.arpa` 下；邮件被拒收常因缺 PTR |

**为什么 CNAME 不能与 MX 共存、不能用在根域名？** 根因是同一条记录集（RRset）里"名字到值"的映射必须唯一。DNS 是一条"问名字、拿值"的查询协议；当某个名字被声明为 `CNAME` 时，它**只剩下别名这一种语义**，RFC 1034 要求该名字下不得再存在其他类型的数据，否则解析器拿到 CNAME 后去追新名字，其他记录就成了永远取不到的孤儿。所以 `example.com CNAME target.com` 与 `example.com MX mail.example.com` 冲突。**根域名（zone apex）不能是 CNAME**，因为根域名**必须**承载 `NS` 与 `SOA`（区的委托点与起始授权）——一个名字既然已有 NS/SOA，就不可能"只剩 CNAME 语义"。这就是"裸域不能直接 CNAME 到 CDN"的规范来源，工程上用 **A/AAAA 指向 CDN 固定 IP**，或用厂商的 **ANAME/ALIAS 类记录**（服务端合成 A 记录）绕过。同理 `MX` 指向 CNAME 也违反 RFC 2181 的澄清，它既多一次解析，又给邮件投递路径增加失败面。追踪一条 CNAME 链则意味着解析器**逐跳再查**，每跳都可能是一次额外迭代：

```
 www.example.com          CNAME  example.cdn-provider.net   ← 第 1 跳（权威返回）
 example.cdn-provider.net CNAME  edge-42.cdn.net            ← 第 2 跳
 edge-42.cdn.net          A      203.0.113.42               ← 第 3 跳才拿到 IP
```

每跳都增加延迟（未缓存即为一次 RTT），并放大失败面：任一跳的权威不可达，整个解析就是 `SERVFAIL`。这是 CDN 接入时建议减少 CNAME 层数的原因。

## 19.3 DNS 负载均衡与健康检查

| 策略 | 做法 | 适用与代价 |
|---|---|---|
| 轮询（round robin） | 同一名字返回多条 A，解析器轮换取用 | 实现最简；粒度是"客户端"而非"请求"，无法按负载分配 |
| 地理就近（geo-steering） | 按解析器 IP（EDNS Client Subnet 可带客户端网段）选最近机房 | 依赖解析器位置准确；公共解析器集中化会让"就近"偏到大城市 |
| 加权（weighted） | 给记录配权重，按比例返回 | 常用于灰度；权重只在**解析时刻**生效，不反映真实负载 |

分工上，DNS 做的是**最外层、最粗粒度**的一次分流：决定流量进哪个机房/集群入口。进入机房后，LVS 一类四层负载均衡按连接分流，Nginx/Envoy 再按请求分流与路由（并发模型见第 7 章 7.5 节）。三层职责不可互换：DNS 看不到连接，四层看不到 URL。

> [!CAUTION]
> 不要答"用 DNS 做负载均衡就可以了"。DNS 有两个**结构上无法消除**的缺陷：(1) **摘除不及时**——客户端与中间解析器按 TTL 缓存结果，一台机器挂了，在 TTL 过期前仍有流量打到它；(2) **感知不到服务端真实负载**——返回哪条记录与你的 CPU、队列深度无关，DNS 协议里根本没有回传负载的字段。

工程上的补救是**分层兜底**：DNS 负责机房级选择（TTL 设小一些，如 60 s），健康检查在 DNS 侧只做服务器级存活探测（失败则摘除该 A 记录），精细负载交给机房内的四层/七层，再由应用层做熔断降级（见第 35 章）。能把"DNS 只能做粗粒度、且摘除受 TTL 约束"说清楚，就已超过只背"轮询/加权/就近"的答法。

## 19.4 `dig` 的实战用法与 `nslookup` 的差距

`dig`（domain information groper）的价值在于它**把报文的每个区段都打出来**，包括 `flags`、`QUERY`/`ANSWER`/`AUTHORITY` 段与耗时。写排障命令时，注释要写"为什么这么查"：

```bash
# 只看答案：脚本里取 IP 用，避免再去解析 "ANSWER SECTION" 这类人类可读文本
dig +short www.example.com

# 走完整迭代链路：跳过本地解析器缓存，复现"根→TLD→权威"每一步，
# 用来区分"权威配置错了"与"本地解析器缓存了旧值"
dig +trace www.example.com

# 换服务器对比：定位本地 DNS 被污染、缓存旧值或配置了内网 view 的问题
dig @8.8.8.8 www.example.com
dig @ns1.example.com www.example.com +norecurse   # 直问权威，绕开所有缓存

# 验证 TC 截断与 EDNS0 协商：人为压到 512 字节逼出 TC=1，再看 TCP 重查是否成功
dig +bufsize=512 www.example.com
dig +tcp www.example.com
dig +noedns www.example.com      # 关闭 EDNS0，回退到 512 字节口径

# 反查与其他类型：PTR 缺失是邮件被拒收的高频原因
dig -x 203.0.113.42 +short
dig MX example.com +short
dig NS example.com +short        # 委托链要逐级向上问才能确认完整
```

**`dig` 与 `nslookup` 的差距**在于信息量：`nslookup` 是历史工具，输出经过"友好格式化"，默认**不显示 `flags`、TTL、`AUTHORITY` 段**，也不区分答案来自缓存还是权威，只能告诉你"解析出来了/没解析出来"。而排障需要的是**为什么**：

| 需求 | `dig` | `nslookup` |
|---|---|---|
| 看 `AA`/`TC`/`RA` 标志位 | 头部直接显示 | 不显示 |
| 看 TTL（判断缓存新旧） | `ANSWER` 段每条都有 | 不显示 |
| 看 `AUTHORITY`/`ADDITIONAL` 段 | 完整输出 | 不输出 |
| 复现迭代链路 | `+trace` 内建 | 无 |
| 指定端口、EDNS0、超时控制 | `-p`、`+bufsize`、`+time` | 能力缺失 |

> [!NOTE]
> 面试官常追问"为什么 `nslookup` 不推荐"。标准答案不是"它旧"，而是**它隐藏了判断缓存与截断所必需的信息**：同样是"解析成功"，`dig` 能告诉你答案来自哪台服务器、TTL 还剩几秒、是否走过 TCP，`nslookup` 只能给你一个 IP。

## 19.5 CDN：回源、命中率与调度

设 `命中率 = 边缘直接响应的请求数 / 总请求数`，其补数是**回源率**。回源请求要额外走一次"边缘 → 源站"的 RTT 并占用源站容量，所以回源率高的 CDN 等于给源站加了一层绕路。

```
                    ┌──────────── 边缘节点 ────────────┐
 用户 ─▶ DNS 调度 ─▶│ 缓存查询 → 命中 → 直接返回        │
        (或 Anycast)│        └ 未命中 ──┐               │
                    └───────────────────┼───────────────┘
                                        │ 回源（长连接 + 协议优化）
                                        ▼
                                   ┌─────────┐
                                   │  源站   │
                                   └─────────┘
```

**命中率低的常见原因**（面试高频，按出现频率排序）：一是 `Cache-Control: no-cache/no-store/private` 写错，边缘只能遵守源站指令；二是 URL 带随机参数（`?t=时间戳`、`?rand=`），每次都是新 key，等于关掉缓存；三是 `Vary` 维度过多（`Vary: User-Agent` 会把每个 UA 变成独立 key）或按 `Cookie` 全量区分；四是缓存键设计过细，把完整 query string 都算进 key 而业务只需要其中一两个参数；五是 TTL 过短，或源站 `max-age` 与边缘默认 TTL 冲突时取了小值；六是内容本来就动态（个性化接口），此时应走动态加速而不是硬缓存；七是发布后大规模刷新使缓存未回温，或请求合并没有开启导致同一文件被并发回源多次。

调度有两条路线。**DNS 调度**按解析器位置/运营商返回不同边缘 IP，粒度是客户端级且受 TTL 约束，切换与摘除都慢。**Anycast** 让同一 IP 在全球多节点公告，由 BGP 选最近入口，逐包生效、切换快，但路由抖动会影响长连接，且故障节点若未撤公告会持续收流量。两者常叠加：Anycast 做入口就近，DNS 做机房级容量调度。**预热（preheat）与刷新（purge）** 方向相反，面试常混：预热是**主动把内容推到边缘**（上线前拉一遍热门文件，避免上线瞬间全量回源打垮源站）；刷新是**主动让边缘副本失效**（发布新版本后强制回源）。刷新本质上牺牲命中率换一致性，因此发布后短时间内回源率会尖峰上升。至于**动态加速**，对不可缓存的请求，CDN 的价值从"缓存"转为"路径优化"：边缘与源站之间保持**长连接复用**（省掉每次 TCP + TLS 握手，见第 20 章 20.5 节）、使用更优的传输协议（如基于 UDP 的 QUIC，见第 15 章）、选更优的回源路径。这时该看**回源时延与回源成功率**，而不是命中率。

## 19.6 一次"网站访问慢"的分层排查法（主线案例）

排障第一原则是**只改变一个变量**。先用一条命令把一次 HTTP 请求切成若干时间段，再决定往哪一层深挖：

```bash
# 用 curl -w 把一次请求切成若干时间段；关掉连接复用，否则"第二次很快"
# 可能只是连接被复用了，反而掩盖了真正的握手问题
curl -w '\n time_namelookup:%{time_namelookup} time_connect:%{time_connect}\n
 time_appconnect:%{time_appconnect} time_pretransfer:%{time_pretransfer}\n
 time_starttransfer:%{time_starttransfer} time_redirect:%{time_redirect}\n
 time_total:%{time_total} http_code:%{http_code} remote_ip:%{remote_ip}\n
 num_connects:%{num_connects}\n' -o /dev/null -s https://www.example.com/
```
| 时间段（差值） | 含义 | 异常阈值（同城口径） | 异常时的根因清单 |
|---|---|---|---|
| `time_namelookup` | 从发起到 DNS 解析完成 | > 100 ms | `resolv.conf` 指向的解析器不可达或跨网；CNAME 链过长；负缓存/权威抖动；`nsswitch.conf` 里 `dns` 排在 `files` 前多走一次 |
| `time_connect − time_namelookup` | TCP 三次握手耗时 | > 3 × RTT 或 > 50 ms | 服务端 `SYN` 队列满（`tcp_max_syn_backlog`/`somaxconn`）；中间设备丢包；跨地域（先量 RTT）；`SYN` 重传 |
| `time_appconnect − time_connect` | TLS 握手耗时 | > 2 × RTT 或 > 100 ms | TLS 1.2 的 2-RTT 握手（对比见第 20 章 20.5 节）；证书链过大、未启用会话复用、OCSP 未装订；CPU 算力不足 |
| `time_pretransfer − time_appconnect` | 握手完成到即将发请求 | 接近 0 | 明显偏大说明在等代理或重定向链，或客户端自己在忙 |
| `time_starttransfer − time_pretransfer` | 发出请求到收到首字节，即 TTFB | > 200 ms | 应用层慢（下游依赖、慢 SQL、锁）；服务端排队（后端并发已满）；CDN 回源（边缘未命中）；**不是网络问题** |
| `time_total − time_starttransfer` | 收到首字节到传完 | 与响应体大小/带宽比对 | 带宽瓶颈；丢包导致拥塞窗口上不去；响应体未压缩；最后一公里差 |

按图索骥的命令序列，不要一上来就抓包：

```bash
# 1 DNS 耗时与来源：换解析器对比，若公共解析器快则问题在内网解析器
dig +stats www.example.com | sed -n '/Query time/p;/SERVER/p'
dig @8.8.8.8 +short www.example.com

# 2 建连：nc -vz 只判断端口，telnet 能再看应用是否有应答；-w 防止排障命令自己挂住
nc -vz -w 3 www.example.com 443

# 3 多次采样看抖动：单次数字会被偶发抖动骗，P99 才是用户感知
for i in $(seq 1 10); do
  curl -o /dev/null -s -w '%{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}\n' \
       https://www.example.com/
done

# 4 对比不同源：换 IP 明显变快说明问题在特定入口而不在应用
curl -o /dev/null -s -w '%{time_starttransfer}\n' --resolve www.example.com:443:203.0.113.42 \
     https://www.example.com/

# 5 TTFB 异常时用 Host 头直连源站：直连快而走 CDN 慢，问题在 CDN 调度或回源路径
curl -o /dev/null -s -w '%{time_starttransfer}\n' -H 'Host: www.example.com' https://203.0.113.9/
```
> [!IMPORTANT]
> `time_starttransfer` 偏大而前三段正常，**是应用层问题不是网络问题**。这条判断能在面试中直接止损：很多人的错误回答是把 TTFB 高也归为"网络慢"，然后一路去查路由和丢包，方向从一开始就错了。

### 19.6.1 `curl -w` 全部时间指标表

| 变量 | 起点 → 终点 | 与前一指标的差值含义 | 典型阈值（同城口径） |
|---|---|---|---|
| `time_namelookup` | 命令起始 → DNS 解析完成 | 纯 DNS 耗时 | < 50 ms；> 100 ms 异常 |
| `time_connect` | 命令起始 → TCP 建连完成 | `−namelookup` = 握手 RTT | ≈ 1 RTT；同城 0.1～0.5 ms 量级 |
| `time_appconnect` | 命令起始 → TLS 等握手完成 | `−connect` = TLS 握手 | TLS 1.3 ≈ 1 RTT，TLS 1.2 ≈ 2 RTT |
| `time_pretransfer` | 命令起始 → 即将发出请求 | `−appconnect` ≈ 0 | 明显大于 0 说明有代理/重定向/客户端阻塞 |
| `time_starttransfer` | 命令起始 → 收到首字节 | `−pretransfer` = 服务端处理 + 首包回程 = TTFB | < 200 ms；> 500 ms 需查应用 |
| `time_redirect` | 所有重定向（含 DNS/建连/握手）累计 | 多次跳转的累计耗时 | 存在重定向时非 0，跳数越多越不可控 |
| `time_total` | 命令起始 → 全部完成 | `−starttransfer` = 内容传输 | 与 `size_download / 带宽` 相符即可 |

两个易踩的坑：这些 `time_*` 是**相对命令起点的累计量而非分段量**，必须做减法；`time_appconnect` 在非 TLS 请求（如 `http://`）下为 0，此时 `time_pretransfer − time_connect` 也接近 0，不要错误解读为"TLS 没耗时"。

## 19.7 抓包与协议分析：`tcpdump` 看什么

```bash
# 抓到文件而不是刷屏：抓包本身很贵，先落盘再离线分析（-w 不解析，减少排障动作的干扰）
tcpdump -i eth0 -s 0 -w /tmp/cap.pcap 'host 203.0.113.42 and tcp port 443'

# 只抓握手三包；只抓 SYN 且非 ACK 是判断"谁发起连接"的标准写法
tcpdump -i any -nn 'tcp port 443 and tcp[tcpflags] & (tcp-syn|tcp-ack) != 0'
tcpdump -i any -nn 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'

# 只看 RST/FIN：定位"谁先断的"；RST 往往比超时更能说明问题
tcpdump -i any -nn 'tcp[tcpflags] & (tcp-fin|tcp-rst) != 0'

# 按网段与方向过滤，避免把整个网卡的流量都拉进来
tcpdump -i any -nn 'net 10.0.0.0/8 and src host 10.0.1.5'

# 抓 ICMP：验证"ping 不通"到底是没回包还是被策略丢弃
tcpdump -i any -nn 'icmp'
```

`tcp[tcpflags]` 的位含义固定：`tcp-fin`=0x01、`tcp-syn`=0x02、`tcp-rst`=0x04、`tcp-push`=0x08、`tcp-ack`=0x10、`tcp-urg`=0x20。

| 观察对象 | 正常样子 | 异常含义 |
|---|---|---|
| 三次握手 | `SYN → SYN,ACK → ACK` 三包齐全，间隔 ≈ 1 RTT | 只有 `SYN` 无应答：未监听、`SYN` 队列满、中间设备丢包；出现 `SYN` 重传即丢包证据 |
| 重传 | 偶发，间隔近似指数退避（`RTO` 计算见第 17 章 17.2 节） | 大量重传 = 链路丢包，或拥塞窗口被压到很小 |
| 重复 ACK | 偶尔出现 | 连续三个重复 ACK 提示丢包并触发快速重传 |
| `RST` | 正常关闭流程里少见 | 应用进程退出、连接被中间设备重置、端口未监听；"谁发 RST 谁有问题"要结合方向判断 |
| 零窗口 | 不应长期出现 | 接收缓冲区被打满 → 应用读取太慢（不是网络问题） |
| TLS 握手 | `ClientHello` 到 `Finished` 的往返数固定 | 往返数偏多说明未复用会话或证书链过长（见第 20 章 20.5 节） |
| 传输段大小 | 稳定在 MSS 附近（常见 1460） | 长期小于 MSS 说明路径 MTU 受限、拥塞窗口小或应用写得零碎 |

> [!WARNING]
> **"抓包看到慢"不等于"服务端慢"。** 抓包点决定了你能看到的因果：在客户端抓，你看到的是"字节到达客户端慢"，它可能来自服务端、可能来自中间链路、也可能来自客户端自己读取慢（零窗口）。要定位责任方，必须在**两端同时抓**并比对时间戳，或至少先确认接收窗口未被打满。

Wireshark 里两个必用功能：**`Follow → TCP Stream`** 把一条连接的双向载荷按序拼成可读流，用来确认应用层到底发了什么（响应体是否被压缩、是否少了某个头部），注意它按连接过滤，多连接时要先在包列表里选中一条会话；**`Statistics → TCP Stream Graphs`** 的 Throughput 图给出实际速率随时间的变化，Time-Sequence (Stevens) 图给出序列号随时间上升的斜率——斜率变平就是停等或阻塞，锯齿状回落通常是丢包/重传导致窗口收缩。这两张图是把"抓包"变成"证据"的关键。

## 19.8 连通性工具的能力边界

面试高频辨析题几乎都围绕"某个工具说通/不通，能不能证明服务可用"。核心原则：**每个工具只探测它自己那一层的语义，跨层推断就是错的。**

| 工具 | 实际探测的东西 | 能力边界（关键） | 替代手段 |
|---|---|---|---|
| `ping` | ICMP `echo request/reply` 是否往返 | 显示"通"**不保证端口可达**；被防火墙丢 ICMP 时显示"不通"但服务完全可用 | `nc -vz`/`curl` 直接测端口；`tcpdump icmp` 确认丢在哪一侧 |
| `traceroute` | 逐跳增大 TTL 收集 `ICMP Time Exceeded` | 依赖 ICMP 回包，屏蔽就显示 `*`；**不反映实际转发路径**（多路径/负载均衡导致每跳不同） | `mtr` 连续统计；`traceroute -T -p 443` 用 TCP 探测绕过 ICMP 屏蔽 |
| `mtr` | 持续采样的逐跳丢包与时延 | 同样受 ICMP 策略影响；**中间跳丢包不代表端到端丢包** | 关注**最后一跳**的丢包与抖动，而非中间跳 |
| `telnet` / `nc -vz` | TCP 建连是否成功（`nc -z` 不发数据） | 只证明三次握手成功，**不证明应用能正确响应**；UDP 模式结论不可靠（无握手） | `curl -v` 看应用层响应；`openssl s_client` 验 TLS |
| `ss` | **本机**套接字状态与队列 | 只能看本机；不产生流量，故不验证对端 | 配合 `ss -lntp`、`ss -tn state established` 看积压 |
| `curl -w` | 端到端各阶段耗时 | 只给时间，不给原因 | 与抓包、服务端日志对齐定位 |

**ICMP 被禁时的替代手段**（把"ping 不通"继续往下推）：一是 TCP 建连探测，`nc -vz -w 3 host port`，或 `traceroute -T -p 443 host` 用 TCP SYN 探路；二是应用层探测，`curl -v --max-time 5` 看是否拿到状态码，`openssl s_client -connect host:443 -servername host` 验证证书链；三是抓包确认责任方，`tcpdump -nn 'icmp or tcp port 443'` 区分"没发包"、"发了没回"、"回了被丢"；四是绕开本机网络栈干扰，`mtr --tcp --port 443 host`、多网卡用 `-a`/`-I` 指定源地址对比。

> [!CAUTION]
> 把 `traceroute` 输出里的 `*` 当成"这一跳坏了"是最常见的错误回答。`*` 只是"这一跳没有回 ICMP 超时报文"，很多骨干设备就是这么配的；真正有意义的是**从某一跳开始丢包率持续到最后一跳**。面试官追问"那你怎么判断"时，正确答案是"看 `mtr` 的最后一跳与端到端丢包，而不是中间跳"。

## 本章小结

| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| 递归与迭代的分界 | 客户端→本地 DNS 是递归（RD=1），本地 DNS→根→TLD→权威是迭代（RD=0），根服务器从不做递归 | 把整条链说成"一层层递归问下去" |
| 缓存优先级 | `/etc/hosts` 与 DNS 的先后由 `nsswitch.conf` 的 `hosts:` 行决定 | 认为 hosts 永远第一，忽略应用自身的 DNS 缓存 |
| 负缓存 | `NXDOMAIN` 会按 SOA 的 `minimum` 被缓存，"新域名解析不出来"不一定是配置错 | 把 `NXDOMAIN` 一律当成"记录没配" |
| TTL 的作用 | TTL 是"最坏生效时间的上界"；切换前调小、切换后调回 | 直接改 IP 而 TTL 仍是 86400，部分用户长期打到旧机 |
| CNAME 的限制 | 名字一旦是 CNAME 就不能承载其他类型；zone apex 必须有 NS/SOA | 说"根域名 CNAME 只是不推荐"，把规范约束说成风格问题 |
| DNS 负载均衡的缺陷 | 摘除受 TTL 约束、无法感知服务端真实负载，只能做机房级粗粒度分流 | 答"用 DNS 做负载均衡就够了" |
| `TC` 与 EDNS0 | UDP 传统上限 512 字节，超限置 `TC=1` 必须改 TCP 重查；EDNS0 扩大承载并承载 `DO` 位 | 把 `TC=1` 解释成"服务器过载" |
| 慢的分层定位 | 用 `curl -w` 的差值把问题切到 DNS/建连/TLS/TTFB/传输五段，再进对应工具 | 一上来就抓包；把 TTFB 高归因为"网络慢" |
| 抓包的局限 | 单点抓包只证明"字节到达该点的快慢"，不能直接判定责任方 | 由"抓包看到慢"直接得出"服务端慢" |
| 工具边界 | `ping` 通不代表端口通，`traceroute` 的 `*` 不代表该跳故障，`telnet` 通不代表应用正常 | 用 ICMP 结论推断应用可用性 |

## 自测题

**Q1**（推导）某域名的 A 记录 TTL 为 86400，你计划在明天 10:00 把服务从旧机房切到新机房。请给出完整的切换步骤与每一步的等待时长依据，并说明不做这一步会发生什么。

<details><summary>答案与解析</summary>

标准答案是**两阶段 TTL 收敛**：(1) 立即把 TTL 从 `86400` 改为 `60`，然后**等待至少 86400 s（24 h）**，让所有仍缓存旧的长 TTL 的解析器过期，等待时长的依据是"旧 TTL 是上界"，不能缩短；(2) 在 10:00 把 A 记录指向新机房，等约 `60 s` 让新值扩散，观察新机房入口流量上升；(3) 确认旧机房流量归零后下线旧机器，再把 TTL 调回 `86400`。

如果不做第 (1) 步就直接改：最坏情况下，那些在改之前刚查过记录的解析器会继续用旧 IP 长达 24 小时，表现为"一部分用户访问到已下线的旧机房"，比例随缓存分布抖动，极难复现。

**错在哪**：常见错误是答"把 TTL 设成 0 就能立刻生效"——TTL=0 只表示"不要复用"，多数解析器有秒级下限，客户端缓存（浏览器、JVM、Go 的 DNS 缓存）更不受你的 TTL 支配。另一个错误是只答"提前调小 TTL"而说不出**要等多久**，面试官追问"提前多久"就是在验证你是否知道 TTL 是上界而非周期。**追问方向**：调小 TTL 的代价是什么（解析器查询量上升、权威侧压力变化）。

</details>

**Q2**（辨析）同事说："`example.com` 的裸域我直接加一条 CNAME 指向 CDN 就行了。"请指出这条方案的问题，并给出两种可落地的替代。

<details><summary>答案与解析</summary>

问题：`example.com` 是区的顶点（zone apex），按 RFC 1034/1035 它**必须**承载该区的 `NS` 与 `SOA`；而一旦某个名字被声明为 `CNAME`，别名语义就独占该名字，不允许存在其他类型的数据。二者冲突，所以裸域不能是 CNAME。同理可解释"CNAME 不能与 MX 共存"：`example.com` 若既是 CNAME 又挂 MX，解析器追到别名后，原名字上的 MX 就永久取不到。

两种替代：(1) 用 **A/AAAA 记录**直接指向 CDN 提供的固定入口 IP，代价是 IP 变更需跟随；(2) 使用厂商提供的**类 CNAME 顶点别名记录**（叫法各异，如 ANAME/ALIAS），由解析服务端在应答时把目标解析成 A 记录再返回，从而不违反规范地实现"裸域走 CDN"。

**错在哪**：把规范约束当成"厂商限制"或"不推荐"，回答里说不出"zone apex 必须有 NS/SOA"这个根因。**追问方向**：`MX` 指向 CNAME 为什么也被禁止？正确答案是规范要求 MX 的目标不应是 CNAME，它引入额外解析跳数与失败面；进一步还会问你 CNAME 链每一跳对 RTT 与 `SERVFAIL` 风险的影响。

</details>

**Q3**（计算）一次 `curl -w` 输出为 `time_namelookup=0.004`、`time_connect=0.052`、`time_appconnect=0.158`、`time_pretransfer=0.159`、`time_starttransfer=1.310`、`time_total=1.340`（秒，`--resolve` 直连某机房，`http_code=200`，响应体 12 KB）。请把总耗时拆成五段，指出问题在哪一层，并给出下一步两个验证动作。

<details><summary>答案与解析</summary>

分段（全部做减法）：

```
 DNS：      0.004 s                     → 4 ms，正常
 TCP 建连： 0.052 − 0.004 = 0.048 s     → 48 ms，偏高（同城 RTT 应为 0.1~0.5 ms 量级）
 TLS 握手： 0.158 − 0.052 = 0.106 s     → 106 ms，偏高
 应用等待： 0.159 − 0.158 ≈ 0.001 s     → 可忽略
 TTFB：     1.310 − 0.159 = 1.151 s     → 1.15 s，异常，占总耗时 86%
 内容传输： 1.340 − 1.310 = 0.030 s     → 12 KB / 30 ms ≈ 400 KB/s，偏低但不主导
```

结论：瓶颈在 **TTFB（服务端处理时间）**；建连与 TLS 偏高提示链路 RTT 偏大（可能跨地域或路径绕行），但不是主因。

下一步验证：(1) 用 `-H 'Host: ...'` 直连**源站**（绕过 CDN）对比 `time_starttransfer`，若直连快则问题在 CDN 回源或调度；(2) 在服务端对齐同一时刻的应用日志/链路追踪，判断是下游依赖、慢查询还是排队，同时用 `mtr --tcp --port 443` 确认 48 ms 的建连是否来自跨地域路由。

**错在哪**：最常见的是"总耗时 1.34 s，网络慢"，把 TTFB 也算进网络；另一个是把 `time_appconnect` 直接当 TLS 耗时（它是相对命令起点的累计量，必须减去 `time_connect`）。**追问方向**：如果 `time_total − time_starttransfer` 很大而 TTFB 正常，你会怀疑什么？答案是带宽瓶颈、丢包导致拥塞窗口受限、响应体未压缩，或最后一公里质量差。

</details>

**Q4**（判断）"我用 `ping www.example.com` 能通，说明 443 端口是开的、网站是好的。"请判断并说明，同时给出这个结论会漏掉的三类故障。

<details><summary>答案与解析</summary>

判断：**错**。`ping` 只证明 ICMP `echo` 有去有回，即"该主机在 IP 层可达"，完全不涉及 TCP 端口、监听状态与应用健康。反向也不成立：`ping` 不通而服务完全可用是**常见**情形，因为很多安全组/防火墙只放行所需的 TCP 端口而显式丢弃 ICMP。

会漏掉的三类故障：(1) **端口未监听或被策略拒绝**——`ping` 通但 443 无监听，`SYN` 得到 `RST`；或防火墙 `DROP`，表现为 `SYN` 重传后超时；(2) **应用层异常**——端口开着但返回 502/503 或响应体错误；(3) **TLS 层异常**——证书过期、SNI 与证书不匹配、证书链缺中间证书，此时 TCP 通而 HTTPS 请求失败。

正确做法是分层验证：`nc -vz -w 3 host 443` 验证端口 → `curl -v --max-time 5 https://host/` 验证应用与 TLS → `openssl s_client -connect host:443 -servername host` 单独验证证书链。若 `ping` 不通而业务正常，先确认 ICMP 是否被策略屏蔽（`tcpdump -nn icmp` 看是否根本没回包），不要急着改网络。

**追问方向**：`telnet host 443` 成功说明了什么？——只说明 TCP 三次握手完成（见第 16 章 16.1 节），不说明应用能正确应答，也不说明 TLS 能协商成功。

</details>

**Q5**（设计）线上告警"某地域用户访问慢"，该地域用户走 CDN。请设计一套从 DNS 到 CDN 到源站的排查顺序，写明每一步的命令、要看的指标，以及"看到什么就跳到下一步"的判据。

<details><summary>答案与解析</summary>

按"由外向内、由粗到细"排序，每步都有明确跳转判据：

```
 步骤 1  DNS 调度是否正确（先确认用户被调度到哪个机房）
         dig +short www.example.com          # 从该地域解析器视角取结果
         dig @<该地域解析器> www.example.com  # 对比是否被调度到远端节点
         判据：返回 IP 不属于就近节点 → 调度问题（查 geo 库 / EDNS Client Subnet）；否则进步骤 2
 步骤 2  CDN 边缘时延与命中率（区分"边缘慢"还是"回源慢"）
         curl -w '<time_* 模板>' -o /dev/null -s https://www.example.com/
         控制台看该节点命中率与回源率
         判据：命中率高而 TTFB 高 → 边缘或最后一公里；命中率骤降 → 进步骤 3
 步骤 3  回源路径（边缘到源站）
         边缘侧看回源时延与成功率；curl -H 'Host: ...' 直连源站对比
         判据：直连源站快而走 CDN 慢 → 回源路径差或长连接复用失效（每次回源重建 TCP+TLS）
 步骤 4  源站与应用（TTFB 归因）
         服务端看应用耗时、下游依赖、慢查询、线程池队列
         判据：源站自身 TTFB 正常 → 回到链路；源站 TTFB 高 → 应用问题，与网络无关
 步骤 5  路径质量与丢包（仅当步骤 1~4 都正常）
         mtr --tcp --port 443 -c 100 <边缘IP>     # 只看最后一跳的丢包与抖动
         tcpdump -i any -nn -w cap.pcap 'host <边缘IP> and tcp port 443'
         判据：末跳丢包/抖动持续 → 链路质量问题；否则两端抓包比对时间戳
```

设计要点（也是加分点）：**不要从步骤 5 开始**，抓包成本最高且最难归因，只在时间指标已把范围收窄后才用；所有时间结论都要**多次采样**（单次会被抖动骗）；并且必须**跨地域对比**——"某地域慢"本身意味着需要一个对照组的数字，否则无法区分"该地域的网络问题"与"全网都慢只是这个地域先被报出来"。

**错在哪**：把"用户慢"直接当成"CDN 慢"或"源站慢"，跳过 DNS 调度这一步，而调度错误（用户被指到远端节点）恰恰是"某地域慢"最常见的原因。**追问方向**：如果该地域用户全被调度到同一个远端节点，你先怀疑什么？优先怀疑解析器位置失真（用户使用集中化的公共解析器，geo 判断基于解析器而非用户）与 EDNS Client Subnet 未启用或被剥离。

</details>

## 面试高频问题与陷阱

| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应本章小节 |
|---|---|---|---|
| 请完整讲一下 DNS 的解析过程，递归和迭代分别发生在哪一段？ | 是否把两段责任划分讲清 | 错误回答：把浏览器缓存、根服务器、权威服务器串成一条"递归链"，说"根服务器帮我递归查到结果"——根服务器从不做递归；加分：点出 RD/RA 标志位与本地 DNS 服务器是分界点 | 19.1.1 |
| 为什么 CNAME 记录不能和 MX 记录共存？为什么根域名不能用 CNAME？ | 对 RRset 语义与区顶点约束的理解 | 错误回答："因为厂商不支持"或"只是不推荐"——说不出"名字一旦是 CNAME 就不能承载其他类型"以及"zone apex 必须有 NS/SOA"；加分：给出 ANAME/ALIAS 这类工程替代 | 19.2.2 |
| 我们准备把服务切到新机房，为什么上线前要先把 TTL 调小？ | 是否理解 TTL 是"最坏生效上界" | 错误回答："TTL 是缓存时间，调小能快点生效"，但说不出要**提前多久**（必须是旧 TTL 的时长）与调小 TTL 的代价（解析器查询量上升）；加分：给出 86400 → 60 的算例 | 19.1.3 |
| 用 DNS 做负载均衡有什么问题？ | 是否知道 DNS 的固有缺陷 | 错误回答："轮询/加权/就近就够了"——说不出摘除受 TTL 约束、DNS 协议里没有回传服务端负载的字段；加分：说明 DNS 只做机房级粗粒度分流，精细负载交给四层/七层 | 19.3 |
| `ping` 通是不是说明服务就是好的？`traceroute` 里出现 `*` 是不是说明那一跳坏了？ | 工具能力边界，两个问题常连着问 | 错误回答：用 ICMP 结论推断端口/应用可用；把 `*` 当故障跳。加分：说明 ICMP 常被策略丢弃、`*` 只表示该跳不回超时报文，真正有意义的是末跳丢包 | 19.8 |
| 用户反馈访问慢，你怎么排查？ | 是否有分层方法论而非乱试 | 错误回答："先抓包看看"或"重启一下"——没有把问题先切到 DNS/建连/TLS/TTFB/传输五段；加分：给出 `curl -w` 差值定位法与每段根因清单，并强调多次采样 | 19.6 |
| 抓包看到响应很慢，能说明是服务端慢吗？ | 因果与观测点 | 错误回答："抓包慢就是服务端慢"——忽略了抓包点、中间链路与客户端接收窗口；加分：说明必须在两端同时抓并比对时间戳，或先确认接收窗口未为零 | 19.7 |
| `time_appconnect` 是 0，是不是说明 TLS 握手没有耗时？ | 指标口径 | 错误回答："是，TLS 很快"——实际 `time_*` 是累计量，且非 TLS 请求该值为 0；加分：说明所有 `time_*` 都要做减法，并区分 TLS 1.2 与 1.3 的往返数 | 19.6.1 |
| CDN 命中率突然下降，你会从哪几个方向查？ | 对缓存键与源站指令的理解 | 错误回答："多加点机器"或"把 TTL 调长"——说不出 URL 随机参数、`Vary`/`Cookie` 维度、`Cache-Control` 指令、请求合并；加分：区分"缓存策略失效"与"内容本就动态"（该走动态加速） | 19.5 |
| 为什么 `dig` 比 `nslookup` 更适合排障？ | 工具输出的信息量 | 错误回答："`dig` 更新更现代"——说不出 `nslookup` 不显示 `flags`/TTL/`AUTHORITY` 段，因此无法判断答案来自缓存还是权威；加分：提到 `+trace`、`+tcp`、`+bufsize` 才能复现截断与迭代链路 | 19.4 |

## 延伸阅读

- RFC 1034《Domain names - concepts and facilities》：递归/迭代、委托与 zone 概念的第一手定义，19.1 节的路径划分以它为准。
- RFC 1035《Domain names - implementation and specification》：报文格式、512 字节 UDP 上限与各记录类型的字段定义。
- RFC 2181《Clarifications to the DNS Specification》：CNAME 与 MX/NS 目标关系的澄清，19.2.2 节的规范依据。
- RFC 2308《Negative Caching of DNS Queries》：负缓存机制与 SOA `minimum` 的口径。
- RFC 6891《Extension Mechanisms for DNS (EDNS(0))》：EDNS0 伪记录、UDP 承载能力协商与 `DO` 位。
- RFC 8499《DNS Terminology》：术语标准化，写文档与答辩时统一用词。
- `man 1 dig`、`man 8 tcpdump`、`man 5 nsswitch.conf`、`man 5 resolv.conf`：本章命令行为与配置文件优先级的权威口径。
- Wireshark 官方文档中 `Statistics → TCP Stream Graphs` 一节：吞吐图与 Stevens 时间序列图的读法。


---

> 作者: [破戒僧](https://onlyblacktea.cn)  
> URL: https://onlyblacktea.cn/book/backend/ch19/  

