Skip to main content

5. Resolvers (解析器)

本章探讨DNS解析器的功能、接口和内部算法。


5.1. Introduction (简介)

解析器是将用户程序连接到域名服务器的程序。

基本功能

在最简单的情况下,解析器从用户程序 (例如,邮件程序、TELNET、FTP) 接收请求,形式为子例程调用、系统调用等,并以与本地主机数据格式兼容的形式返回所需信息。

位置:

  • 解析器位于请求解析器服务的程序所在的同一台机器上
  • 但可能需要咨询其他主机上的名称服务器

性能特征

时间变化: 因为解析器可能需要咨询多个名称服务器,或者可能在本地缓存中有请求的信息,解析器完成所需的时间可能会有很大差异,从毫秒到几秒。

响应时间范围:

最快: 1-10ms (本地缓存命中)
正常: 50-200ms (单次查询)
较慢: 500ms-2s (多次查询、超时)
最慢: 2s+ (网络问题、服务器故障)

缓存的重要性

解析器的一个非常重要的目标是通过从其先前结果的缓存中回答大多数请求来消除网络延迟和名称服务器负载。

缓存效率:

  • ✅ 由多个进程、用户、机器等共享的缓存比非共享缓存更高效
  • ✅ 集中式缓存可以提高整体性能
  • ✅ 减少对权威服务器的查询

缓存架构示例:

方案1: 每个应用独立缓存
App1 → Resolver1 → Cache1
App2 → Resolver2 → Cache2
App3 → Resolver3 → Cache3
(低效,重复查询)

方案2: 共享缓存
App1 ↘
App2 → Resolver → 共享Cache
App3 ↗
(高效,缓存命中率高)

5.2. Client-resolver interface (客户端-解析器接口)

5.2.1. Typical functions (典型功能)

解析器的客户端接口受本地主机约定的影响,但典型的解析器-客户端接口有三个功能:

功能1: 主机名到主机地址转换 (Host Name to Host Address Translation)

目的: 模拟以前基于HOSTS.TXT的功能

输入: 字符串 (主机名)
输出: 一个或多个32位IP地址

DNS实现:

  • 转换为对类型A RR的请求
  • DNS不保留RR的顺序
  • 此功能可以选择对返回的地址进行排序
  • 或选择"最佳"地址 (如果服务仅向客户端返回一个选择)

示例:

# 传统接口
gethostbyname("www.example.com")
→ 返回: 93.184.216.34

# 现代接口 (多地址)
getaddrinfo("www.example.com", AF_INET)
→ 返回: [93.184.216.34, 93.184.216.35, ...]

注意:

  • 推荐多地址返回
  • 但单地址可能是模拟先前HOSTS.TXT服务的唯一方法

功能2: 主机地址到主机名转换 (Host Address to Host Name Translation)

目的: 反向DNS查找

输入: 32位IP地址
输出: 字符串 (主机名)

DNS实现:

  1. IP地址的八位字节被反转
  2. 用作名称组件
  3. 后缀为"IN-ADDR.ARPA"
  4. 使用类型PTR查询获取主机的主名称

示例:

IP地址: 1.2.3.4
转换步骤:
1. 反转八位字节: 4.3.2.1
2. 添加后缀: 4.3.2.1.IN-ADDR.ARPA
3. 查询PTR记录
4. 返回: host.example.com

实际查询:
dig -x 1.2.3.4
→ 等同于: dig PTR 4.3.2.1.IN-ADDR.ARPA

功能3: 通用查找功能 (General Lookup Function)

目的: 从DNS检索任意信息

特点: 在以前的系统中没有对应物

输入:

  • QNAME (查询名称)
  • QTYPE (查询类型)
  • QCLASS (查询类)

输出: 所有匹配的RR

特征:

  • 通常对所有RR数据使用DNS格式而不是本地主机的格式
  • 返回所有RR内容 (例如,TTL) 而不是带有本地引用约定的处理形式

示例:

# 通用查询接口
query("example.com", "MX", "IN")
→ 返回完整的MX记录,包括TTL、优先级等

query("example.com", "TXT", "IN")
→ 返回所有TXT记录

query("example.com", "ANY", "IN")
→ 返回所有类型的记录

解析器结果类型

当解析器执行指示的功能时,它通常有以下结果之一传递回客户端:

结果1: 一个或多个RR (Data Found)

给出请求数据的RR。解析器以适当的格式返回答案。

查询: www.example.com A
结果: 93.184.216.34 (成功)

结果2: 名称错误 (Name Error, NE)

当引用的名称不存在时发生。

场景: 用户可能拼错了主机名

查询: wwww.example.com (拼写错误)
结果: NXDOMAIN (名称不存在)

结果3: 数据未找到错误 (Data Not Found Error)

当引用的名称存在,但适当类型的数据不存在时发生。

场景: 主机地址功能应用于邮箱名称会返回此错误,因为名称存在,但没有地址RR存在

查询: [email protected] A
结果: NOERROR但无数据 (名称存在,但没有A记录)

错误处理注意事项

重要:

  • 在主机名和地址之间转换的功能可以将"名称错误"和"数据未找到"错误条件组合为单一类型的错误返回
  • 但通用功能不应该这样做

原因: 应用程序可能首先询问有关名称的一种类型的信息,然后对同一名称的某些其他类型的信息进行第二次请求;如果两个错误被组合,则无用的查询可能会减慢应用程序。

示例:

查询1: example.com A → 无数据
查询2: example.com MX → 有数据

如果查询1返回"名称不存在",应用可能不会尝试查询2
但如果返回"数据未找到",应用知道可以尝试其他类型

5.2.2. Aliases (别名)

别名处理

在尝试解析特定请求时,解析器可能发现所讨论的名称是别名。

场景: 解析器在查找CNAME RR时可能发现为主机名到地址转换给出的名称是别名

建议: 如果可能,应该从解析器向客户端发出别名条件信号

别名跟随规则

一般情况: 解析器在遇到CNAME时简单地在新名称处重新启动查询

特殊情况: 执行通用功能时,当CNAME RR匹配查询类型时,解析器不应追踪别名

原因: 这允许询问别名是否存在的查询

示例:

# 查询类型是CNAME - 用户对CNAME RR本身感兴趣
query("www.example.com", "CNAME", "IN")
→ 返回: www.example.com CNAME web.example.com
→ 不继续查询web.example.com

# 查询类型是A - 解析器应该跟随CNAME
query("www.example.com", "A", "IN")
→ 发现: www.example.com CNAME web.example.com
→ 继续查询: web.example.com A
→ 返回: 93.184.216.34

别名的特殊条件

多级别别名 (Multiple Levels of Aliases)

处理: 应该避免,因为效率低下,但不应作为错误发出信号

www.example.com → CNAME → web.example.com
web.example.com → CNAME → server.example.com
server.example.com → A → 93.184.216.34

允许但不推荐 (3次查询)

别名循环 (Alias Loops)

处理: 应该被捕获并将错误条件传递回客户端

a.example.com → CNAME → b.example.com
b.example.com → CNAME → a.example.com

错误: CNAME循环检测

指向不存在名称的别名 (Aliases to Non-existent Names)

处理: 应该被捕获并将错误条件传递回客户端

www.example.com → CNAME → nonexistent.example.com
nonexistent.example.com → NXDOMAIN

错误: CNAME目标不存在

5.2.3. Temporary failures (临时失败)

失败场景

在不太完美的世界中,所有解析器偶尔会无法解析特定请求。

原因:

  1. 解析器由于链路故障或网关问题而与网络的其余部分分离
  2. 特定域的所有服务器巧合失败或不可用 (较少见)

正确的错误处理

关键原则: 这种情况不应作为名称或数据不存在错误发送给应用程序

为什么重要:

  • 这种行为对人类来说很烦人
  • 当邮件系统使用DNS时可能造成严重破坏

错误示例:

临时网络故障
→ 返回NXDOMAIN (错误!)
→ 邮件系统认为地址无效
→ 退回邮件 (不应该!)

正确做法:
→ 返回SERVFAIL (临时失败)
→ 邮件系统稍后重试
→ 邮件最终送达 ✓

处理策略

不推荐: 无限期阻塞请求

原因: 当客户端是可以继续其他任务的服务器进程时,这通常不是一个好选择

推荐解决方案: 始终将临时失败作为解析器功能的可能结果之一

权衡: 这可能使现有HOSTS.TXT功能的模拟更困难


5.3. Resolver internals (解析器内部)

每个解析器实现使用略有不同的算法,通常花费更多逻辑处理各种错误而不是典型情况。

本节: 概述了解析器操作的推荐基本策略,但将详细信息留给[RFC-1035]


5.3.1. Stub resolvers (存根解析器)

概念

实现解析器的一个选项是将解析功能移出本地机器并进入支持递归查询的名称服务器。

好处:

  • 为缺乏资源执行解析器功能的PC提供域服务的简单方法
  • 可以为整个本地网络或组织集中缓存

存根解析器要求

最小需求: 将执行递归请求的名称服务器地址列表

配置:

  • 此类型的解析器可能需要配置文件中的信息
  • 因为它可能缺乏在域数据库中定位它的复杂性

验证: 用户还需要验证列出的服务器将执行递归服务

注意: 名称服务器可以自由拒绝为任何或所有客户端执行递归服务

建议: 用户应咨询本地系统管理员以查找愿意执行服务的名称服务器

存根解析器架构

应用程序

存根解析器 (配置: nameserver 8.8.8.8)
↓ [递归查询, RD=1]
递归DNS服务器 (8.8.8.8)
↓ [迭代查询多个服务器]
权威DNS服务器

返回最终答案

存根解析器的缺点

问题1: 超时优化困难

递归请求可能需要任意时间来执行,存根可能难以优化重传间隔以处理丢失的UDP数据包和死服务器。

问题2: 服务器过载风险

如果名称服务器将重传解释为新请求,过于热心的存根可能会轻易使名称服务器过载。

问题3: TCP的权衡

使用TCP可能是一个答案,但TCP可能会给主机的能力带来与真正解析器类似的负担。


5.3.2. Resources (资源)

共享区域数据

除了自己的资源外,解析器还可能共享访问由本地名称服务器维护的区域。

优势: 更快速的访问

注意: 解析器必须小心,永远不要让缓存信息覆盖区域数据

术语: "本地信息"表示缓存和此类共享区域的联合

规则: 当两者都存在时,权威数据始终优先于缓存数据使用

数据结构

以下解析器算法假设所有功能都已转换为通用查找功能,并使用以下数据结构来表示解析器中正在进行的请求的状态:

SNAME (Search Name)

我们正在搜索的域名。

STYPE (Search Type)

搜索请求的QTYPE。

SCLASS (Search Class)

搜索请求的QCLASS。

SLIST (Server List)

描述名称服务器和解析器当前尝试查询的区域的结构。

内容:

  • 区域名称等效
  • 区域的已知名称服务器
  • 名称服务器的已知地址
  • 历史信息 (建议下一个尝试哪个服务器)
  • 匹配计数 (从根向下SNAME与被查询区域共有的标签数)

用途: 作为解析器距离SNAME"接近"程度的度量

SBELT (Safety Belt)

与SLIST相同形式的"安全带"结构。

初始化: 从配置文件初始化

内容: 当解析器没有任何本地信息来指导名称服务器选择时应使用的服务器列表

匹配计数: -1表示不知道标签匹配

典型配置:

SBELT包含:
- 2个根服务器 (提供对所有域空间的最终访问)
- 2个本地域服务器 (允许在网络隔离时继续解析本地名称)

CACHE (缓存)

存储先前响应结果的结构。

TTL处理:

  • 解析器负责丢弃TTL已过期的旧RR
  • 大多数实现将到达RR中指定的间隔转换为某种绝对时间
  • 而不是单独倒计时TTL

清理策略:

  • 在搜索过程中遇到旧RR时忽略或丢弃它们
  • 或在定期扫描期间丢弃它们以回收旧RR消耗的内存

5.3.3. Algorithm (算法)

顶层算法的四个步骤

步骤1: 检查本地信息

查看答案是否在本地信息中,如果是,则将其返回给客户端。

检查顺序:
1. 权威区域数据 (如果有直接访问)
2. 缓存数据

如果找到且未过期:
返回给客户端
否则:
继续步骤2

注意:

  • 一些解析器有用户界面选项,将强制解析器忽略缓存数据并咨询权威服务器
  • 这不推荐作为默认值

步骤2: 找到最佳服务器

查找要询问的最佳服务器。

策略: 查找本地可用的名称服务器RR

搜索顺序:

  1. 从SNAME开始
  2. 然后SNAME的父域名
  3. 然后祖父域名
  4. 依此类推向根

示例:

SNAME = Mockapetris.ISI.EDU

搜索NS RR:
1. Mockapetris.ISI.EDU
2. ISI.EDU
3. EDU
4. . (根)

处理:

  • 这些NS RR列出了位于或高于SNAME的区域的主机名称
  • 将名称复制到SLIST
  • 使用本地数据设置它们的地址

地址不可用时的选择:

  • 最佳: 启动并行解析器进程查找地址,同时继续使用可用地址
  • 设计选择是本地主机能力的函数

设计优先级:

  1. 限制工作量 (发送的数据包、启动的并行进程),以便请求不会陷入无限循环或启动请求或查询的连锁反应,即使某人错误配置了某些数据
  2. 尽可能获得答案
  3. 避免不必要的传输
  4. 尽快获得答案

失败回退: 如果搜索NS RR失败,则从安全带SBELT初始化SLIST

SBELT理念:

  • 当解析器不知道要询问哪些服务器时,应使用配置文件中的信息
  • 配置文件列出了几个预期有帮助的服务器
  • 通常选择是两个根服务器和两个主机域的服务器
  • 每个两个的原因是冗余

排序优化:

  • SLIST数据结构可以排序以首先使用最佳服务器
  • 确保以轮询方式使用所有服务器的所有地址
  • 排序可以是优选本地网络上的地址的简单功能
  • 或可能涉及来自过去事件的统计数据,例如先前的响应时间和打击率

步骤3: 发送查询

发送查询直到收到响应。

策略:

  • 在所有服务器的所有地址周围循环
  • 每次传输之间有超时

重要性:

  • 使用多宿主主机的所有地址很重要
  • 过于激进的重传策略实际上会减慢响应速度

原因:

  • 当多个解析器争夺同一名称服务器时
  • 甚至偶尔对于单个解析器

控制: SLIST通常包含数据值以控制超时并跟踪先前的传输

步骤4: 分析响应

涉及分析响应。

解析器应该:

  • 在解析响应时高度偏执
  • 检查响应是否使用响应中的ID字段匹配它发送的查询

理想答案: 来自对查询具有权威性的服务器,给出所需数据或名称错误

处理:

  • 数据传递回用户
  • 如果其TTL大于零,则输入缓存以供将来使用

委派处理:

  • 如果响应显示委派,解析器应检查委派是否比SLIST中的服务器"更接近"答案
  • 通过比较SLIST中的匹配计数与从SNAME和委派中的NS RR计算的匹配计数来完成
  • 如果不是,回复是伪造的,应该被忽略
  • 如果委派有效,应缓存NS委派RR和服务器的任何地址RR
  • 名称服务器输入SLIST,搜索重新启动

CNAME处理:

  • 如果响应包含CNAME,则在CNAME处重新启动搜索
  • 除非响应具有规范名称的数据或CNAME本身就是答案

错误处理:

  • 如果响应显示服务器失败或其他奇怪的内容
  • 从SLIST中删除服务器并返回步骤3

算法流程图

步骤1: 检查本地信息
↓ [未找到]
步骤2: 找到最佳服务器 (SLIST)

步骤3: 发送查询

步骤4: 分析响应
├─→ [答案] → 返回客户端
├─→ [委派] → 更新SLIST → 返回步骤2
├─→ [CNAME] → 更新SNAME → 返回步骤1
└─→ [错误] → 删除服务器 → 返回步骤3

关键概念总结

解析器类型

  1. 完整解析器: 实现完整的迭代查询算法
  2. 存根解析器: 依赖递归服务器
  3. 转发解析器: 将查询转发到其他解析器

客户端接口

  1. 主机名→地址: 最常用的功能
  2. 地址→主机名: 反向DNS查找
  3. 通用查找: 灵活的DNS查询

错误处理

  1. 名称错误: 名称不存在
  2. 数据未找到: 名称存在但无该类型数据
  3. 临时失败: 网络或服务器问题

性能优化

  1. 缓存: 减少查询延迟
  2. 并行查询: 提高解析速度
  3. 智能重试: 避免过载服务器
  4. 地址排序: 优先使用本地服务器

详细实现: 参见 RFC 1035 - DNS实现和规范 了解协议细节和数据格式

下一节: Glossary (术语表) - DNS术语和定义