"解析改了,为什么还有用户访问旧地址"——答案不在 DNS 配置面板,而在从浏览器到权威服务器的六层缓存上,以及每层遵守(或不遵守)TTL 的方式。
先拆开看一次解析:谁替谁跑腿、缓存的过期时间从哪来、哪一层会骗你。
1. 一次解析的完整链路
| 层级 | 位置 | 典型 TTL 行为 | 观察方式 |
|---|---|---|---|
| 浏览器缓存 | 浏览器进程内存 / 磁盘 | 短,且受页面连接复用影响 | 清缓存、开无痕 |
| 系统缓存 | systemd-resolved / nscd / mDNSResponder |
按响应 TTL,部分实现有上限 | resolvectl statistics |
hosts 文件 |
/etc/hosts |
永不过期,优先级高于 DNS | getent hosts DOMAIN |
| 本地 DNS(递归器) | 运营商、8.8.8.8、127.0.0.53 |
按 TTL,可能被强制抬高或压低 | dig @LOCAL_DNS |
| 根 / 顶级域(迭代) | 13 组根、各 TLD | 委派记录的 TTL 较长 | dig +trace |
| 权威服务器 | 你的 DNS 服务商 | 由记录 TTL 决定 | dig @AUTH_NS |
$ dig +trace DOMAIN +short
$ dig +noall +answer DOMAIN A
+trace 会从根开始逐级查询,把每一步的委派与最终答案都打出来——这是判断"卡在哪一层"最直接的手段。
2. 递归查询与迭代查询:谁替谁跑腿
| 查询方式 | 发起者 | 谁承担逐级查找 | 结果形态 |
|---|---|---|---|
| 递归查询 | 客户端 → 本地 DNS | 本地 DNS 独自跑完全程 | 要么给最终答案,要么给错误 |
| 迭代查询 | 本地 DNS → 根/TLD/权威 | 每一跳只给"下一跳去哪" | 逐级委派(NS + glue 记录) |
客户端(stub resolver)总是发递归查询,报文里置 RD=1;递归器对上游发迭代查询并自己维护缓存。所以"权威服务器上没有请求日志"是正常现象——请求都在递归器那层被缓存拦住了。
# 关闭递归,模拟"只问不跑腿",观察是否只拿到委派
$ dig @8.8.8.8 DOMAIN A +norecurse +noall +authority
3. 记录类型各自干什么
| 类型 | 用途 | 容易踩的点 |
|---|---|---|
A |
名字 → IPv4 | 只改 A 不改 AAAA,双栈客户端仍走旧地址 |
AAAA |
名字 → IPv6 | 没有 IPv6 服务却配了 AAAA,会拖慢连接建立 |
CNAME |
别名 → 另一个名字 | 不能与 MX/TXT 共存于同一名字;链路过长会断 |
MX |
邮件路由,带优先级 | 指向 CNAME 是违规的 |
TXT |
任意文本:SPF、DKIM、域名验证 | 多条 TXT 是并列关系,不是覆盖关系 |
NS |
委派:该区由谁权威 | 改 NS 后旧 NS 仍可能应答一段时间 |
SRV |
服务发现:主机 + 端口 + 权重 | 客户端支持度不一,_service._proto.name 格式易写错 |
CAA |
限制哪些 CA 能签发本域证书 | 不配等于不限制;配错会导致续签失败 |
$ dig DOMAIN MX +short
$ dig DOMAIN TXT +short
$ dig _https._tcp.DOMAIN SRV +short
4. TTL 与"改了解析为什么不生效"
TTL 是缓存方遵守的建议值,不是强制命令。改记录后,旧值要等各级缓存逐个过期,链路才收敛到新值。
| 现象 | 真实原因 | 处理 |
|---|---|---|
| 自己刷新就生效,别人不生效 | 你命中的是另一台递归器 | 换递归器对比 dig @8.8.8.8 与 @1.1.1.1 |
| 部分城市仍旧地址 | 运营商递归器不严格遵守 TTL,或做了透明缓存 | 提前把 TTL 调低再切,切完再调回 |
| 改了 A 记录但 IPv6 用户异常 | AAAA 没同步改 |
两个类型一起改,或先删 AAAA |
| 浏览器清缓存仍不生效 | 连接复用 / HTTP/2 会话保持 / DNS 预取 | 换浏览器或无痕窗口验证 |
# 拿到权威值,绕开所有中间缓存
$ dig @AUTH_NS DOMAIN A +noall +answer
# 看当前递归器缓存里剩多少
$ dig @LOCAL_DNS DOMAIN A +noall +answer
切流量的正确顺序:提前一个 TTL 周期把 TTL 降到 60 秒量级 → 观察一个周期 → 改记录 → 验证 → 再调回长 TTL。反过来做,故障时就没有快速回退的余地。
5. CNAME 的两条硬约束
- 不能与
MX、TXT等共存于同一名字(RFC 1034/2181 的语义约束)。example.com上放了 CNAME,就不能再放 MX。所以裸域(apex)通常用ALIAS/ANAME这类厂商扩展,而不是标准 CNAME。 - 链路过长会失败。每多一跳就多一次查询,部分递归器对 CNAME 链深度有限制,超了直接返回
SERVFAIL。
$ dig www.DOMAIN CNAME +short
$ dig www.DOMAIN A +noall +answer # 看解析器是否已把链走完
另一种故障:CNAME 指向内部域名(如 svc.internal),外部递归器解析不到,外部用户拿到 NXDOMAIN 或 SERVFAIL,内网一切正常。
6. 负缓存:NXDOMAIN 也会被缓存
查询一个不存在的名字,权威服务器会在 SOA 记录里给出一个 negative TTL(通常取 SOA 的 minimum 字段),递归器据此缓存"不存在"这一事实。
| 返回码 | 含义 | 是否被缓存 |
|---|---|---|
NOERROR + 空 answer |
名字存在但没有该类型记录 | 是(按 SOA minimum) |
NXDOMAIN |
名字不存在 | 是(按 SOA minimum) |
SERVFAIL |
解析器自身出错或上游超时 | 部分实现会短暂缓存 |
REFUSED |
上游拒绝递归 | 通常不缓存 |
$ dig DOMAIN SOA +noall +answer
$ dig not-exist.DOMAIN A +noall +comments | grep -i "status"
这解释了"新域名配好了但部分用户打不开":他们命中的递归器在记录尚不存在时查过一次,负缓存没过期前会一直返回 NXDOMAIN。
7. resolv.conf、ndots 与搜索域
$ cat /etc/resolv.conf
nameserver 127.0.0.53
search svc.cluster.local internal
options ndots:5 timeout:2 attempts:3
| 参数 | 作用 | 典型影响 |
|---|---|---|
nameserver |
递归器地址,可多个 | 只有第一个超时才会试下一个 |
search |
短名补全的搜索域列表 | 短名会按顺序依次尝试拼接 |
ndots:N |
名字里点数 ≥ N 才先当绝对名查 | 设太大 → 先拼搜索域,多出若干次无效查询 |
timeout / attempts |
单次超时与重试 | 叠加后总等待可达数秒 |
ndots 是 Kubernetes 环境最典型的隐形延迟源:默认值偏高时,访问 api.example.com(2 个点)会先被拼成 api.example.com.svc.cluster.local 查一遍,失败再查原名——外部请求因此平白多出一个往返。
8. dig 与 getent hosts 结果不同
$ dig DOMAIN +short
$ getent hosts DOMAIN
原因很直接:dig 直连 DNS 服务器,只走 DNS 协议;getent hosts 走 NSS(/etc/nsswitch.conf 的 hosts: 行),会按顺序查 files(/etc/hosts)、dns,可能还有 mdns4_minimal、myhostname。
| 工具 | 走什么 | 适用判断 |
|---|---|---|
dig / nslookup |
纯 DNS 协议 | 判断 DNS 记录本身对不对 |
getent hosts |
NSS 全链路 | 判断应用实际会拿到什么 |
ping / curl |
通常走 NSS | 与真实行为最接近 |
resolvectl query |
systemd-resolved | 看本地缓存与 DNSSEC 结果 |
所以"dig 能解析但程序连不上",第一件事是 getent hosts,而不是继续查 DNS。
9. CDN 与 DNS 的关系
CDN 的核心机制就是在 DNS 层按来源返回不同地址:递归器的出口 IP 决定它拿到哪个边缘节点。
| 概念 | 作用 |
|---|---|
| 调度粒度 | 按递归器出口 IP 归属地/运营商分流,不按真实用户 |
| TTL 与切流速度 | TTL 越短切得越快,代价是查询量上升 |
CNAME 接入 |
业务域 CNAME 到 CDN 域,由 CDN 决定最终 A 记录 |
| 回源段 | CDN 到源站的连接与用户侧解析无关,需单独评估 |
| 解析异常的表现 | 部分地区拿到错误节点 → 表现为区域性慢或证书不匹配 |
常见误判:dig @8.8.8.8 看到的地址与用户实际访问的不同,就断定"解析错了"。其实你把查询源换成了 Google 的出口,本就该拿到另一个节点。
10. DoH / DoT 的取舍
| 方案 | 端口 | 优点 | 代价 |
|---|---|---|---|
| 明文 DNS | 53/UDP、53/TCP | 简单、可抓包、延迟最低 | 中间人可读可改 |
| DoT | 853/TCP | 加密且不污染现有解析链路 | 需要 853 放行,部分网络封 |
| DoH | 443/TCP | 与 HTTPS 同端口,穿透性最好 | 混在 Web 流量里,排障与审计变难 |
| 内部 DoH 网关 | 443 | 统一出口、可审计 | 单点故障与额外一跳 |
取舍的关键不是"要不要加密",而是排障能力:上了 DoH,tcpdump port 53 什么都看不到,问题会从"解析错"变成"不知道发生了什么"。至少要在客户端侧留查询日志。
11. 常见坑
| 坑 | 现象 | 正解 |
|---|---|---|
只改 A 忘了 AAAA |
双栈用户走旧地址 | 两类一起改,或先摘 AAAA |
CNAME 指向内部域名 |
外部用户 SERVFAIL,内部正常 |
对外只用公网可达的名字 |
| TTL 设得过长 | 故障时无法快速切流 | 切流前先把 TTL 降到 60 秒量级 |
容器里看到 127.0.0.11 |
以为上游出错 | 那是 Docker 内嵌 DNS,真实上游看 docker inspect |
用 ping 验证解析 |
ICMP 被墙导致误判 | 用 dig + getent hosts 分开验证 |
| 忽略负缓存 | 新记录打不开 | 等 SOA minimum 过期,或换递归器 |
ndots 过大 |
每个外部请求多出无效查询 | 调小 ndots 或用绝对名(结尾加点) |
$ cat /etc/nsswitch.conf | grep '^hosts'
$ docker exec APP cat /etc/resolv.conf
$ resolvectl query DOMAIN
12. 边界
| 需求 | 更合适的做法 | 理由 |
|---|---|---|
| 需要秒级切流 | 短 TTL + 双写过渡 | DNS 缓存不可控,做不到秒级 |
| 需要按用户身份分流 | 应用层调度 | DNS 只能看到递归器出口 |
| 需要防劫持 | DoT/DoH + DNSSEC | 明文 DNS 无法保证完整性 |
| 内网服务发现 | 服务注册中心 / SRV |
大规模动态实例不适合手工 A 记录 |
| 排查"某地打不开" | 多地探针 + +trace |
单点 dig 无法复现区域差异 |
一句话:DNS 只负责"名字到地址"的一次性映射,任何要求它承担"实时调度"的设计都会在缓存这一层失效。
13. 自测
- 递归查询与迭代查询分别由谁承担逐级查找?为什么权威服务器上看不到用户请求?
- 改了 A 记录却没改 AAAA,哪类用户会出问题,表现是什么?
CNAME为什么不能和MX同时存在于同一个名字上?NXDOMAIN会被缓存多久,由哪条记录决定?dig能解析但应用连不上,下一步该用哪个命令验证?- 容器里
resolv.conf指向127.0.0.11,它代表什么?真实上游怎么查?