网络-11 DNS
DNS(Domain Name System)把域名翻译成 IP 地址。它是每一次网络访问的第一步——DNS 慢了,后面所有优化都白费。
1. 域名分级

域名从右往左逐级变大,这个顺序和解析顺序一致:
www.example.com.
│ │ │ └── 根域(root),平时省略不写
│ │ └───── 顶级域 TLD(Top-Level Domain)
│ └───────────── 二级域,你注册的那部分
└───────────────── 主机名/三级域
- 根域:全球 13 组根域名服务器(
a.root-servers.net~m.root-servers.net)。注意是 13 组而非 13 台——通过任播(Anycast)技术,实际有上千台物理服务器分布全球,同一个 IP 会路由到最近的那台。 - 顶级域:
.com、.org、.net等通用顶级域,以及.cn、.jp等国家顶级域。 - 权威域名服务器(Authoritative):真正持有该域名解析记录的服务器,是解析链的终点。
FQDN(完全限定域名) 指以点结尾的完整写法 www.example.com. ——末尾那个点代表根域。这个点看起来无关紧要,但在 K8s 里它直接关系到解析性能,本文最后会讲。
2. 记录类型
| 类型 | 作用 | 示例 |
|---|---|---|
| A | 域名 → IPv4 地址 | example.com → 93.184.216.34 |
| AAAA | 域名 → IPv6 地址 | example.com → 2606:2800:220:1:: |
| CNAME | 域名 → 另一个域名(别名) | www → example.com |
| NS | 指定该域由哪些权威服务器负责 | example.com → ns1.dns.com |
| MX | 邮件服务器,带优先级 | example.com → mail.example.com |
| TXT | 任意文本,用于域名所有权验证、SPF 反垃圾邮件 | v=spf1 include:... |
| PTR | IP → 域名(反向解析) | 邮件服务器用它验证发送方 |
| SOA | 该域的起始授权记录,含管理员邮箱、序列号、各种 TTL | — |
| SRV | 服务发现,指定服务的主机 + 端口 | K8s 内部大量使用 |
CNAME 有两个必须知道的限制:
- 根域名不能用 CNAME。
example.com本身必须是 A 记录,因为根域必须同时有 SOA 和 NS 记录,而 CNAME 不允许与其他记录共存。想让根域名指向 CDN,只能用云厂商的私有扩展(AWS 的 ALIAS、阿里云的隐性 URL 等)。 - CNAME 会增加一次解析。
www → cdn.provider.com → A 记录,链条越长首次解析越慢。
CDN 的接入基本都靠 CNAME:你把 www 指向 CDN 厂商的域名,CDN 再根据用户地理位置返回最近的边缘节点 IP。
3. 递归查询与迭代查询

这两个概念最容易混淆,关键在于谁在替谁跑腿:
- 递归查询:客户端问本地 DNS,本地 DNS 必须给出最终答案。客户端只发一次问、收一次答,中间过程与它无关。
- 迭代查询:被问的服务器只告诉你下一步该问谁,不代你去问。本地 DNS 需要自己一级级追下去。
实际的组合是固定的:
客户端 ──递归──> 本地 DNS ──迭代──> 根域 ──> 顶级域 ──> 权威域
(帮我查到底) (我只告诉你下一步问谁)
为什么这样分工? 如果所有客户端都对根服务器做递归查询,根服务器要替全世界跑腿,瞬间被压垮。改成迭代后,根服务器只需回一句"去问 .com 的服务器"就完事,压力极小。而本地 DNS 数量有限且有缓存,承担递归的开销是划算的。
完整的解析链(假设全部缓存未命中):
① 浏览器缓存 → 未命中
② 操作系统缓存 → 未命中
③ hosts 文件 → 未命中
④ 本地 DNS 缓存 → 未命中,开始迭代
⑤ 根域服务器 → "去问 .com 的 TLD 服务器"
⑥ .com TLD 服务器 → "去问 ns1.example.com"
⑦ 权威服务器 → "93.184.216.34" ← 终点
⑧ 本地 DNS 缓存结果并返回给客户端
八步里前四步都是缓存。实际生产中绝大多数查询在第 1~4 步就返回了,完整走完八步是少数情况。
4. 缓存与 TTL
每条 DNS 记录都带 TTL(Time To Live),单位秒,表示这条记录可以被缓存多久。
TTL 的取值是个权衡:
| TTL | 优点 | 缺点 |
|---|---|---|
| 短(60~300s) | 改 DNS 后生效快,适合灰度、故障切换 | 查询量大,解析延迟高 |
| 长(86400s) | 缓存命中率高,解析快 | 改了 DNS 要等很久才全网生效 |
实践经验:平时设长 TTL(如 1 小时),计划变更前提前几小时把 TTL 调短(如 60s),变更完成、观察稳定后再调回去。这样既保证日常性能,又让变更可控。
为什么"改了 DNS 还是访问旧 IP"? 因为链路上每一层都有缓存,任何一层没过期都会返回旧值。而且现实更糟——很多客户端和中间层不严格遵守 TTL:浏览器有自己的缓存策略,某些运营商 DNS 会强行延长 TTL 以降低自己的查询量。所以 DNS 变更永远不要假设它能在 TTL 时间内全网生效,切换 IP 时必须让新旧两个 IP 同时可用一段时间。
手动清缓存:
# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Linux (systemd-resolved)
sudo resolvectl flush-caches
# Windows
ipconfig /flushdns
排查工具首选 dig:
dig example.com # 查 A 记录
dig example.com MX # 查指定类型
dig +trace example.com # 完整展示迭代过程,排障最有用
dig +short example.com # 只输出结果
dig @8.8.8.8 example.com # 指定 DNS 服务器,用于对比排查
nslookup 也能用,但 dig 的输出更完整,+trace 尤其适合定位"哪一级解析出了问题"。
5. UDP 还是 TCP
DNS 默认用 UDP 53 端口,这是它快的关键——见 网络-06 UDP:一问一答两个包搞定,不用握手。
但 DNS 也会用 TCP,两种情况:
- 响应超过 512 字节。传统 DNS 限制 UDP 响应体最大 512 字节,超了就在响应里置 TC(Truncated)标志位,客户端收到后改用 TCP 重发查询。
- 主从同步(AXFR/IXFR)。区域文件传输数据量大且要求可靠,直接走 TCP。
这也意味着防火墙只放开 UDP 53 是错的——遇到大响应会因 TCP 53 被封而解析失败。这类故障的特征很典型:小域名解析正常,某些返回记录多的域名(比如带一堆 A 记录的 CDN 域名)稳定失败。
现代做法是 EDNS0(RFC 6891),它允许客户端声明"我能接收更大的 UDP 包"(通常 4096 字节),从而避免大部分 TCP 回退。DNSSEC 的签名数据很大,基本依赖 EDNS0 才能走 UDP。
6. 加密 DNS
传统 DNS 是明文的,这带来两个问题:
- 可被窃听:链路上任何节点都能看到你访问了哪些域名;
- 可被篡改:这就是 DNS 劫持——运营商或攻击者返回一个假 IP,把你导向广告页或钓鱼站。国内曾经普遍存在的"运营商插广告"就是这么做的。
三种应对方案:
| 方案 | 全称 | 传输方式 | 特点 |
|---|---|---|---|
| DoT | DNS over TLS | TLS,853 端口 | 独立端口,易于网络管理员识别和管控 |
| DoH | DNS over HTTPS | HTTPS,443 端口 | 混在普通 HTTPS 流量里,难以被区分和封锁 |
| DNSSEC | DNS Security Extensions | 仍是明文 | 给记录加数字签名,只防篡改不防窃听 |
关键区别要分清:DoH/DoT 解决的是机密性(防窃听)和传输完整性;DNSSEC 解决的是数据来源真实性(防篡改),但内容依然明文可见。三者不互相替代。
DoH 因为走 443 端口且和网页流量混同,抗封锁能力最强,Chrome 和 Firefox 都已默认或可选启用。但它也带来争议:企业和校园网的 DNS 层管控(内容过滤、审计)会被绕过,所以不少企业内网会主动禁用 DoH。
7. 实战:K8s 的 ndots 陷阱
这是 K8s 环境下最经典的 DNS 性能故障,也是"为什么要理解 FQDN 末尾那个点"的答案。
看一眼 Pod 里的 /etc/resolv.conf:
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
ndots:5 的含义:如果待解析的域名中点的数量少于 5 个,就先依次拼接 search 里的每个后缀去尝试,全部失败后才当作绝对域名直接查。
于是在 Pod 里访问 api.github.com(2 个点,小于 5)会发生:
① api.github.com.default.svc.cluster.local → NXDOMAIN ❌
② api.github.com.svc.cluster.local → NXDOMAIN ❌
③ api.github.com.cluster.local → NXDOMAIN ❌
④ api.github.com → 成功 ✓
一次解析变成 4 次查询。更糟的是,如果同时启用了 IPv6,每次查询会同时发 A 和 AAAA 两个请求,实际是 8 个 DNS 请求才换回一个 IP。高并发下这会把 CoreDNS 打满,表现为接口偶发超时、延迟毛刺,而应用日志里看不出任何异常——因为慢在解析阶段,不在业务逻辑里。
三种解法:
# 方案一(推荐):域名末尾加点,声明为 FQDN,直接跳过所有 search 后缀
# 代码里写 "api.github.com." 而不是 "api.github.com"
# 方案二:单个 Pod 覆盖 ndots
spec:
dnsConfig:
options:
- name: ndots
value: "2"
# 方案三:集群层面部署 NodeLocal DNSCache,在每个节点本地做缓存
方案一最干净——一个点解决问题,零配置成本。但注意它只对你能改的代码有效,第三方 SDK 里硬编码的域名管不了,这种情况才需要方案二或三。
集群内部访问反而不受影响:my-service 只有 0 个点,第一个 search 后缀 my-service.default.svc.cluster.local 就命中了。受害的恰恰是访问外部域名的场景,这也是它容易被忽视的原因——本地开发和集群外测试都正常,一上集群就偶发超时。
8. 本章面试题
说一下完整的 DNS 解析过程
八步,前四步都是缓存:
- 浏览器缓存 → 2. 操作系统缓存 → 3. hosts 文件 → 4. 本地 DNS 缓存 → 5. 根域服务器(告知 TLD 服务器地址)→ 6. TLD 服务器(告知权威服务器地址)→ 7. 权威服务器(返回最终 IP)→ 8. 本地 DNS 缓存并返回
实际生产中绝大多数查询在前四步就返回了,走完全程是少数情况。
递归查询和迭代查询的区别?为什么这样设计?
区别在于谁替谁跑腿:
- 递归:被问方必须给出最终答案。客户端 → 本地 DNS 用递归。
- 迭代:被问方只告诉你下一步问谁。本地 DNS → 根/TLD/权威用迭代。
为什么这样分工:如果所有客户端都对根服务器做递归,根服务器要替全世界跑腿,瞬间被压垮。改成迭代后根服务器只回一句"去问 .com",压力极小。本地 DNS 数量有限且有缓存,承担递归开销是划算的。
DNS 用 UDP 还是 TCP?什么时候用 TCP?
默认 UDP 53——一问一答,无需握手,快。
两种情况用 TCP:
- 响应超过 512 字节:响应里置 TC(Truncated)标志位,客户端改用 TCP 重发;
- 主从区域传输(AXFR/IXFR):数据量大且要求可靠。
所以防火墙只放开 UDP 53 是错的。典型故障特征:小域名正常,返回记录多的域名稳定失败。
EDNS0 可声明支持更大的 UDP 包(通常 4096 字节),避免大部分 TCP 回退。
为什么改了 DNS 记录还是访问到旧 IP?
链路上每一层都有缓存(浏览器、操作系统、本地 DNS、运营商),任何一层未过期都会返回旧值。
更麻烦的是很多客户端和中间层不严格遵守 TTL:浏览器有自己的策略,某些运营商 DNS 会强行延长 TTL 降低查询量。
所以切换 IP 时必须让新旧两个 IP 同时可用一段时间,永远不要假设 TTL 到了就全网生效。
实践:平时设长 TTL,计划变更前几小时调短到 60s,变更稳定后再调回。
CNAME 和 A 记录的区别?根域名能用 CNAME 吗?
A 指向 IP,CNAME 指向另一个域名。
根域名不能用 CNAME。因为根域必须同时有 SOA 和 NS 记录,而 CNAME 不允许与其他记录共存。想让根域名指向 CDN 只能用云厂商私有扩展(AWS ALIAS、阿里云隐性 URL)。
另外 CNAME 会多一次解析,链条越长首次解析越慢。CDN 接入基本都靠 CNAME。
DoH、DoT、DNSSEC 分别解决什么问题?
必须分清防的是什么:
- DoH(over HTTPS,443)/ DoT(over TLS,853)解决机密性——防窃听、防劫持。DoH 混在普通 HTTPS 流量里,抗封锁最强。
- DNSSEC 给记录加数字签名,解决来源真实性——防篡改,但内容仍是明文,不防窃听。
三者不互相替代。DoH 的副作用是绕过企业 DNS 层管控,所以不少企业内网主动禁用它。
K8s 里访问外部域名偶发超时,怎么排查?
优先怀疑 ndots:5。Pod 的 /etc/resolv.conf 里 options ndots:5 意味着:域名中点数少于 5 个时,先依次拼接 search 后缀尝试。
访问 api.github.com(2 个点)会变成 4 次查询(3 次 NXDOMAIN + 1 次成功);若启用 IPv6,A + AAAA 并发则是 8 个请求。高并发下打满 CoreDNS,表现为接口偶发超时,但应用日志无异常——慢在解析阶段。
解法:① 域名末尾加点写成 FQDN api.github.com.(最干净);② Pod 的 dnsConfig 覆盖 ndots:2;③ 部署 NodeLocal DNSCache。
注意受害的是访问外部域名——集群内 my-service 第一个后缀就命中了。所以本地开发正常、上集群才偶发超时。
xingliuhua