DNS解析根据名称和记录类型查询缓存,并在必要时访问DNS层级,最终得到有明确边界的答案。
地址并非永久不变,NXDOMAIN不等于超时,DNS成功返回也不能证明应用服务可以访问。
诊断时保留记录类型、响应码、TTL和解析器身份;只重试暂时性故障,并按策略让缓存答案过期。
是什么,为什么存在
DNS解析把域名和查询类型映射为DNS数据。应用最常查询地址,但DNS还承载委派、邮件路由、服务、验证和别名信息。结果是一组带有生存时间和状态的记录,不是永久不变的域名到IP字典条目。
普通应用不会自行遍历公共DNS层级。它的存根解析器(stub resolver)把问题交给 递归解析器(recursive resolver) ,后者通常由操作系统、局域网、云平台或公共DNS服务提供。递归解析器会返回缓存结果,或执行得到答案所需的上游查询。
权威DNS服务器(authoritative DNS server) 根据自身负责区域的数据回答问题,通常不会代替任意客户端搜索整个层级。递归查询与权威发布分离后,许多客户端可以共享缓存,而每个区域的所有者仍能控制自己的命名空间。
HTTP请求、TLS握手、数据库连接、邮件投递或服务发现开始前,都会遇到解析。解析失败时,客户端不会向目标主机发起传输连接。解析返回过期或非预期地址时,后续故障又很像TCP、TLS、代理或应用缺陷。
名称与区域
DNS名称由一组标签组成。绝对形式终止于根,书写时常带末尾点,例如api.shop.example.。面向用户的工具往往省略最后的点,还可能追加搜索后缀,因此诊断输出应区分用户输入的名称与实际查询的绝对名称。
命名空间按区域划分,并非每个标签对应一个区域。父区域可以发布NS记录来委派子区域。此后,子区域的权威服务器负责发布该委派以下的记录,直到遇到另一项委派或名称末端。
区域边界是管理边界,并不表示一次网络跳转、一个独立组织或一个单独的服务器进程。一台服务器可以为多个区域提供权威服务,一个区域也应配置多台权威服务器。
问题与资源记录
DNS问题至少包含名称、类型和类别。互联网DNS通常使用IN类别。因此,查询api.shop.example.的A类型与查询同一名称的AAAA类型使用不同缓存键。
一条 DNS资源记录(DNS resource record) 包含所有者名称、类型、类别、TTL和类型专用数据。响应可以包含答案、权威和附加三个部分。所有者、类型和类别相同的记录构成一个记录集,通常作为整体缓存。
常见类型的含义各不相同:
| 类型 | 数据 | 常见用途 |
|---|---|---|
A | IPv4地址 | IPv4连接候选地址 |
AAAA | IPv6地址 | IPv6连接候选地址 |
CNAME | 规范目标名称 | 把一个所有者名称设为另一名称的别名 |
NS | 权威服务器名称 | 委派或描述一个区域 |
MX | 优先级与邮件服务器 | 为域名路由邮件 |
TXT | 一个或多个文本字符串 | 发布协议专用元数据 |
SOA | 区域权威与计时数据 | 标记区域权威并支持否定缓存 |
DNS传送结构化记录,如何解释记录由客户端协议决定。TXT值不会因为由DNS交付就自然可信。需要真实性的协议应按威胁模型使用DNSSEC验证或其他经过认证的通道。
解析只是一个阶段
名称解析为下一网络阶段提供候选地址。客户端可能同时收到IPv6与IPv4地址,按本地策略排序,并在一个总体连接截止时间内尝试多个地址。因此,DNS成功不保证套接字能够连接、TLS能够验证主机名,也不保证服务器会接受应用请求。
反向情况同样需要区分。直接连接IP地址可能成功,而名称解析失败,但用IP取代名称不是通用修复。这样会绕过基于DNS的路由变化,还可能破坏TLS服务器名称指示、证书检查、HTTP虚拟主机或服务商故障转移。
工作原理
存根、递归与权威角色
应用通常调用操作系统或运行时API。系统查找API可能根据本地配置查询/etc/hosts、组播机制、企业目录和DNS。DNS专用API直接发送DNS问题,其行为不一定与系统查找路径相同。
缓存未命中时,递归解析器可以从预先掌握的根服务器地址开始。根服务器返回指向相关顶级域的转介,顶级域服务器再返回指向子区域的转介,最后由子区域的权威服务器返回答案或权威否定结果。
转介在权威部分包含NS记录,还可能在附加部分带上这些服务器名称的地址记录。如果解析服务器名称本身必须先进入正在委派的子区域,这类胶水记录(glue record)就是必需的。
解析器必须按照委派上下文处理转介和胶水记录。附加数据并不等于可以无条件缓存任意答案。域内规则(bailiwick rules)与DNSSEC验证会限制哪些数据能够影响后续解析。
别名与候选地址
CNAME表示其所有者是另一名称的别名。解析器会沿目标继续查询,直到取得所需类型的数据、遇到错误或达到安全上限。每一跳别名都可以拥有自己的TTL;遇到环或过长链条时必须失败,不能消耗无界资源。
规范目标可以有多条A或AAAA记录。客户端不能假定第一个地址永久不变或对所有位置都最优。记录顺序可能变化,而且地址在DNS中仍然有效时,其背后的服务也可能暂时无法访问。
其他记录类型有自己的选择规则。MX带有优先级,SRV带有优先级与权重。把所有答案都当成无序字符串列表会丢失协议语义。
缓存条目与TTL
DNS生存时间(DNS time to live) 以秒为单位,表示收到的记录通常最多可以缓存多久。解析器会随时间递减TTL。条目过期后,必须刷新才能作为当前答案返回,除非存在明确且有界的过期数据策略。
TTL并不承诺所有客户端恰好在相同秒数后看到变化。各级缓存在不同时间收到答案,应用可能另设缓存,长连接也会继续使用已经选定的端点。发布变更必须把这些层次全部纳入考虑。
缓存键包含问题名称、类型和类别,以及解析器的策略上下文。A答案不能满足AAAA问题。同样,从一套分割视图取得的答案不能泄漏到另一个租户或网络视图。
否定答案
DNS否定缓存(negative DNS caching) 保存名称不存在或所请求数据不存在的权威证据。NXDOMAIN表示被查询名称不存在。响应码为NOERROR但不含所需记录的结果常称为NODATA,表示名称存在,但没有该类型的数据。
两种结果不能互换。NXDOMAIN可以跨记录类型作用于该名称,NODATA则只针对请求的类型。权威否定响应利用区域的SOA信息限制否定缓存时间。
SERVFAIL、拒绝、畸形响应和超时都不能证明名称不存在。它们表示解析器未能取得可用答案。重试策略可以尝试另一台已配置的递归解析器或权威服务器,但必须服从操作截止时间,避免把依赖故障放大成重试风暴。
传输与验证
经典DNS通常用UDP承载较小的问题和响应,并在需要时使用TCP。现代部署还可能在客户端与递归解析器之间使用DNS over TLS或HTTPS。改变传输方式保护的是特定链路,并不会让未签名的权威答案天然具备真实性。
DNSSEC增加签名和信任链,验证解析器可以检查它们。根据委派和签名状态,验证结果可能是安全、不安全或错误。DNSSEC验证失败通常表现为SERVFAIL,因此有条件时,解析器诊断应保留更具体的扩展原因。
示例
这些示例使用仅供文档使用的.test名称和保留地址段。它们对真实DNS字段建模,但不发送网络流量,使输出能够重复,也让每次缓存状态变化都清晰可见。
检查别名答案
这个响应包含一条CNAME,以及其目标的IPv4和IPv6记录。摘要会保留两个候选地址,并采用全部依赖数据中的最小TTL。
const response = {
rcode: "NOERROR",
answers: [
{ name: "www.shop.test.", type: "CNAME", ttl: 300, data: "edge.shop.test." },
{ name: "edge.shop.test.", type: "A", ttl: 120, data: "192.0.2.44" },
{ name: "edge.shop.test.", type: "AAAA", ttl: 60, data: "2001:db8::44" },
],
};
function summarize(message, originalName) {
if (message.rcode !== "NOERROR") throw new Error(message.rcode);
let canonicalName = originalName;
const chainTtls = [];
for (const record of message.answers) {
if (record.type === "CNAME" && record.name === canonicalName) {
canonicalName = record.data;
chainTtls.push(record.ttl);
}
}
const addresses = message.answers.filter(
(record) => record.name === canonicalName && ["A", "AAAA"].includes(record.type),
);
const effectiveTtl = Math.min(...chainTtls, ...addresses.map((record) => record.ttl));
return { canonicalName, addresses, effectiveTtl };
}
const result = summarize(response, "www.shop.test.");
console.log(`canonical=${result.canonicalName}`);
console.log(`addresses=${result.addresses.map((record) => record.data).join(", ")}`);
console.log(`effective ttl=${result.effectiveTtl}s`);canonical=edge.shop.test.
addresses=192.0.2.44, 2001:db8::44
effective ttl=60s对这个组合后的应用结果采用最小依赖TTL是一种保守做法。生产环境的DNS库通常分别暴露A与AAAA查询,客户端也可以单独缓存每个记录集。示例还需要加入别名环上限,才能安全处理不可信响应数据。
跟随转介
这个微型迭代解析器从根地址开始,跟随两次转介,最终停在权威答案上。映射表代替线上响应,让控制流保持可见,又不依赖公共DNS。
const replies = new Map([
["198.41.0.4", { kind: "referral", zone: "test.", ns: "ns.test.", address: "192.0.2.53" }],
["192.0.2.53", { kind: "referral", zone: "shop.test.", ns: "ns.shop.test.", address: "198.51.100.53" }],
["198.51.100.53", { kind: "answer", name: "api.shop.test.", type: "A", data: "203.0.113.80", ttl: 30 }],
]);
function resolveIteratively(name, type) {
let server = "198.41.0.4";
for (let hop = 0; hop < 8; hop += 1) {
console.log(`ask ${server} for ${name} ${type}`);
const reply = replies.get(server);
if (!reply) throw new Error("no response configured");
if (reply.kind === "answer") {
console.log(`answer ${reply.data} ttl=${reply.ttl}`);
return reply;
}
console.log(`referral ${reply.zone} via ${reply.ns}`);
server = reply.address;
}
throw new Error("referral limit exceeded");
}
resolveIteratively("api.shop.test.", "A");ask 198.41.0.4 for api.shop.test. A
referral test. via ns.test.
ask 192.0.2.53 for api.shop.test. A
referral shop.test. via ns.shop.test.
ask 198.51.100.53 for api.shop.test. A
answer 203.0.113.80 ttl=30真实解析器会缓存转介、尝试多个服务器地址、匹配响应与问题、验证消息结构并执行截止时间。八跳保护展示了必须设置的一项边界,但它不是DNS协议常量。
让肯定与否定条目过期
缓存按名称和类型保存肯定答案,却按名称保存NXDOMAIN。推进手动时钟后,可以看到肯定命中、肯定过期、跨类型否定命中与否定过期。
let now = 0;
let upstreamQueries = 0;
const positive = new Map();
const negative = new Map();
function upstream(name, type) {
upstreamQueries += 1;
if (name === "missing.shop.test.") return { rcode: "NXDOMAIN", ttl: 10 };
return { rcode: "NOERROR", value: "192.0.2.44", type, ttl: 30 };
}
function resolve(name, type) {
const negativeHit = negative.get(name);
if (negativeHit && negativeHit.expiresAt > now) {
return { ...negativeHit.response, source: "cache" };
}
const key = `${name}|${type}`;
const positiveHit = positive.get(key);
if (positiveHit && positiveHit.expiresAt > now) {
return { ...positiveHit.response, source: "cache" };
}
const response = upstream(name, type);
const entry = { response, expiresAt: now + response.ttl };
if (response.rcode === "NXDOMAIN") negative.set(name, entry);
else positive.set(key, entry);
return { ...response, source: "upstream" };
}
function show(name, type) {
const result = resolve(name, type);
console.log(`t=${now} ${name} ${type}: ${result.rcode} ${result.source}`);
}
show("api.shop.test.", "A");
now = 20;
show("api.shop.test.", "A");
now = 31;
show("api.shop.test.", "A");
show("missing.shop.test.", "A");
now = 36;
show("missing.shop.test.", "AAAA");
now = 42;
show("missing.shop.test.", "AAAA");
console.log(`upstream queries=${upstreamQueries}`);t=0 api.shop.test. A: NOERROR upstream
t=20 api.shop.test. A: NOERROR cache
t=31 api.shop.test. A: NOERROR upstream
t=31 missing.shop.test. A: NXDOMAIN upstream
t=36 missing.shop.test. AAAA: NXDOMAIN cache
t=42 missing.shop.test. AAAA: NXDOMAIN upstream
upstream queries=4示例使用了已经推导出的否定TTL。真实解析器会从权威否定响应中推导允许的生存时间,并受本地策略上限约束。NODATA需要使用不同的键,因为没有A数据不表示没有AAAA数据。
陷阱
变化中的缓存正确性
解析器缓存是一组带时间限制的断言,不是区域副本。每个条目记录问题、响应数据或否定状态、过期时间与策略上下文。别名、委派或视图变化时,这些条目如何交互决定了正确性。
TTL是当前数据的上限
如果答案到达时TTL为300,缓存通常最多只能从接收时刻起供应300秒。下游缓存收到的是剩余TTL,而不是重新开始的300秒。如果每一跳都重置原始TTL,过期数据就可能永远存活。
TTL为零允许数据用于当前事务,但禁止普通复用。极低TTL会增加上游查询负载,让解析器可用性更直接地影响应用。极高TTL会减少查询,却拉长计划内和意外变更的传播时间,因此这个值是运营取舍,不是通用常量。
从别名得到的结果可能依赖多个记录集。缓存可以按各自的过期时间保存每个集合,再用仍然有效的部分重建答案。若应用把整条链压成一个端点列表,其过期时间不能晚于最早到期的依赖项。
否定状态有作用域
权威NXDOMAIN否定被查询名称的存在,NODATA则否定现有名称上的一种记录类型。把两者都缓存为name -> no address,会让后续AAAA问题错误复用A的NODATA结果。完全不缓存又会让缺失名称反复冲击权威基础设施。
否定响应需要权威证据和有界生存时间。超时或SERVFAIL缺少这种证据,不能提升为NXDOMAIN。一些实现会短暂缓存暂时性故障以抑制流量,但那是独立的本地故障缓存策略,不能报告为权威不存在。
过期答案是一项可用性选择
权威服务器暂时不可达时,供应过期数据可以维持服务。解析器必须先拥有一条曾经有效的答案,识别出符合策略的解析故障,并执行明确的过期保留上限和客户端TTL上限。它还应尝试刷新,并暴露使用了过期数据这一事实。
供应过期数据改变的是故障取舍,不会让已过期数据重新成为当前数据。它可能把客户端导向退役基础设施,或延长错误记录的影响,因此运营方需要能够清除有害条目,并观测过期答案比例。安全策略与经过验证的DNSSEC状态也会限制可供应的内容。
分割视图属于缓存键
分割视图DNS会按网络、租户或解析器上下文返回不同数据。若缓存跨这些上下文共享,就可能泄漏内部名称,或把公共用户路由到私有地址。因此,视图身份与DNS问题一样,属于实际缓存键的一部分。
会改变答案的策略也遵守同一规则,例如过滤、合成或搜索域扩展。诊断信息应指出解析器与最终绝对问题。比较两个工具的答案前,必须先确认它们使用的是同一查找路径和视图。
有效的缓存指标应区分新鲜命中、否定命中、刷新、过期答案和上游故障类别。单一的“DNS缓存命中”计数器会隐藏解释发布或故障所需的状态。
延伸阅读
5个问题 · 1道输出预测题 · 1道找错题