7.1 DNS 解析、记录与缓存边界
DNS 是分层的分布式数据库,不只把域名换成 IP。一个名字可以有多组资源记录集(RRset),分别用于地址、邮件路由、委派、服务发现或域名控制权验证。
1. Stub、递归解析器与权威服务器
典型客户端路径:
application
|
OS / runtime stub resolver
|
recursive resolver
|-- root server: referral to a TLD
|-- TLD server: referral to an authoritative zone
`-- authoritative server: answer or authoritative negative answer应用通常调用系统或运行时的名字服务接口。该接口可能按平台配置查询 hosts、本地缓存或企业名字服务,再把 DNS 请求交给递归解析器;这些步骤不一定全由 DNS stub 自己完成。递归解析器根据缓存决定是否还需询问根、顶级域和权威服务器。
根和顶级域服务器通常返回委派线索,不会代递归解析器继续查到底。应答中的 NS 指向下一组名字服务器,必要的 glue 地址帮助打破循环:例如 example.test 的名字服务器是 ns.example.test,不能先要求完整解析这个区,才知道去哪里查询它。glue 是启动查询的地址线索,不等于该地址的 DNSSEC 验证结果。
“13 个根服务器”指 A 至 M 这 13 个根服务标识,实际由多地点的任播实例提供,并非全球只有 13 台机器。
2. 递归请求与迭代解析
客户端通常向递归解析器要最终结果或错误。递归解析器则逐次询问各级权威服务器,收到委派后继续查询下一组服务器。它也可能把请求转给另一递归解析器,具体看配置。
# 询问当前配置的递归解析器
dig www.example.com A
# 由 dig 从根开始跟随委派,演示迭代解析
dig +trace www.example.com A
# 指定递归解析器
dig @1.1.1.1 www.example.com AAAAdig +trace 不是用户上一条查询的回放。它先获取根服务器信息,再由本机跟随委派;@server 在 trace 模式下只影响最初的根 NS 查询。真实递归解析器可能已命中缓存,或采用转发、QNAME 最小化和不同路径。上面命令会发起实际 DNS 查询;企业网络中应选择获准使用的解析器。
3. 记录集与别名约束
| Type | 所属名字下的数据 | 常见用途 |
|---|---|---|
A | IPv4 地址 | 地址解析 |
AAAA | IPv6 地址 | 地址解析 |
CNAME | 规范名字 | 别名 |
MX | 优先级与邮件交换器名字 | 邮件路由 |
NS | 权威名字服务器名 | 区委派与权威服务 |
TXT | 一个或多个字符串 | 域名验证、SPF 等 |
SOA | 主名字服务器、序列号与区定时参数 | 区管理与负缓存参数 |
CAA | CA 授权策略 | 约束允许签发该域名证书的 CA |
RRset 由相同 owner name、class 和 type 的记录组成。一个 A RRset 可以有多个 IPv4 地址,AAAA 则是另一组;不要把每个地址当成单独一组,也不要把应答顺序当作稳定的优先级。连接库还可能交错尝试 IPv6 与 IPv4。
CNAME 所属名字不能再带其他普通数据,DNSSEC 相关记录有特定例外。因此普通区顶点必需的 SOA/NS 与 CNAME 冲突。提供商的 ALIAS、ANAME 或 flattening 是扩展能力,通常通过合成地址答案处理,不能把它们当作标准 CNAME。
CNAME 链增加解析工作,但不一定多一次往返:缓存或权威应答中的附带数据可能省去查询。
4. TTL 与迁移时序
权威记录携带 TTL,解析器缓存后递减剩余时间。TTL 到期表示需要刷新,不是向全球缓存广播删除。RFC 8767 允许配置了 serve-stale 的解析器在无法刷新时受控返回过期数据,并限制保留时间和返回的 TTL;它不会把旧记录重新变成权威最新值。
TTL 的取舍包括:
- 长 TTL 减少权威查询和解析延迟,但变更前的答案也保留更久;
- 短 TTL 让新查询更快看到变更,但增加对 DNS 服务的依赖与查询负载;
- 解析器和应用可能另设缓存上限或本地保留策略,不能只看权威 TTL 推测实际行为;
- 已建立的 TCP/TLS 连接不会因 DNS TTL 到期自动迁移。
计划迁移时先降低 TTL,并确认权威服务器已发布这个值,再等待至少一个旧 TTL 窗口,最后切换地址。比如从 3600 秒降为 60 秒,降值前刚缓存的记录仍可能保留近一小时;等 60 秒就切换并不能加速那份旧缓存。切换后还要保留旧端点并观察流量,考虑应用缓存、长连接和 serve-stale。
5. 负缓存与新建名字
NXDOMAIN 与 NODATA 都可能进入负缓存。按 RFC 2308,权威负应答中 SOA 的 TTL 取 min(SOA 自身 TTL, SOA.MINIMUM),接下来像普通 TTL 一样递减。下面只计算正常负缓存的剩余时间,不实现解析器或 serve-stale:
soa_ttl = 3600
soa_minimum = 300
negative_ttl = min(soa_ttl, soa_minimum)
for elapsed in [0, 120, 300]:
remaining = max(0, negative_ttl - elapsed)
print(elapsed, remaining)
# 0 300
# 120 180
# 300 0新建记录不会抹掉此前缓存的不存在应答。排查时要区分:
- NXDOMAIN:该名字不存在;
- NODATA:名字存在,但没有所查询类型的记录集;
- SERVFAIL:解析器未能得到可用答案,可能涉及 DNSSEC 验证、上游故障或协议错误。
NODATA 不是独立 RCODE,通常由 NOERROR、缺少所查询类型的答案及权威区信息判断,不能把所有空答案都认作 NODATA。NXDOMAIN 的负缓存按名字与 class 生效,NODATA 还需区分查询类型;存在 A 而没有 AAAA,不能推导整个名字不存在。
6. 枚举系统解析的 IPv4 与 IPv6 地址
Python getaddrinfo() 使用平台名字服务配置,结果未必来自 DNS,socket API 也不暴露权威 TTL。下面按 IPv4/IPv6 去重,并保留 IPv6 链路本地地址的 scope ID,避免把不同接口上的同一地址合并:
#!/usr/bin/env python3
import socket
import sys
def resolve_stream_addresses(host: str, service: str = "443") -> list[str]:
results = socket.getaddrinfo(
host, service, family=socket.AF_UNSPEC,
type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP,
)
addresses: list[str] = []
seen: set[tuple[int, str, int]] = set()
for family, _type, _proto, _canonname, sockaddr in results:
address = sockaddr[0]
scope = sockaddr[3] if family == socket.AF_INET6 else 0
key = (family, address, scope)
if key not in seen:
seen.add(key)
label = "IPv6" if family == socket.AF_INET6 else "IPv4"
suffix = f"%{scope}" if scope and "%" not in address else ""
addresses.append(f"{label} {address}{suffix}")
return addresses
if __name__ == "__main__":
host = sys.argv[1] if len(sys.argv) > 1 else "localhost"
try:
for item in resolve_stream_addresses(host):
print(item)
except socket.gaierror as error:
raise SystemExit(f"resolution failed: {error}")默认查询 localhost,地址种类和顺序取决于机器配置;使用 127.0.0.1 等数字地址可检查输出格式而不依赖公共 DNS。socket.setdefaulttimeout() 既不清缓存,也不为 getaddrinfo() 设置解析截止时间。重复调用不能稳定测量冷、热 DNS 缓存;观察 TTL 或指定解析器时应使用 dig 或 DNS 库,并记录环境。
7. DNS 传输与安全边界
传统 DNS 同时支持 UDP 和 TCP,通用实现需要支持 TCP。EDNS(0) 通告可接收的 UDP 负载大小,但不保证路径能无分片传送那么大的报文。UDP 应答的 TC 位表示截断,客户端通常重试 TCP;丢包也可能直接表现为超时。TCP 承载的每条 DNS 消息还有 2 字节大端长度前缀。
DNSSEC 认证 DNS 数据来源并检查完整性,不隐藏查询名字。DoT、DoH 加密客户端到解析器这一段,解析器仍可看到查询,也不会因为传输加密就自动验证权威数据。应分别检查:
- 客户端到解析器的传输隐私;
- 解析器的日志与信任策略;
- DNS 数据的真实性;
- 应用协议自己的 TLS 认证。
8. 诊断顺序
- 查当前解析器,不要默认所有主机都用同一公共 DNS。
- 分别查 A 和 AAAA,记录状态、答案、权威区信息和 TTL。
- 直接问权威服务器,与递归应答对比。
- 检查 CNAME 链、委派 NS 与 glue。
- 区分正缓存、负缓存与已有连接复用。
- 有 DNSSEC 时查验证失败,别把 SERVFAIL 直接当作没有记录。
9. 验收问题
- stub、递归解析器和权威服务器分别负责什么?
- 为什么 CNAME 不适合直接放在普通区顶点?
- 为一次 DNS 切换写出降 TTL、等待、切换、观测和下线旧端点的顺序。
- 区分 NXDOMAIN、NODATA 和 SERVFAIL。
- 为什么 DNSSEC 不能替代 HTTPS?
下一课进入7.2 CDN 缓存、回源与流量导向。