跳到主要内容

7. 解析器实现

推荐的解析器算法顶层在 RFC-1034 中讨论. 本节在假定采用本备忘录名称服务器实现章节所建议的数据库结构的前提下, 讨论实现细节.


7.1. 将用户请求转换为查询

解析器执行的第一步是将客户端请求 (以适合本地 OS 的格式表示) 转换为搜索规范: 在某个特定名称处查找匹配特定 QTYPE 和 QCLASS 的 RR.

查询规范指南

单一类型和类别优先: 在可能情况下, QTYPE 和 QCLASS 应对应单一 type 和单一 class, 因为这会让缓存数据的使用简单得多.

原因: 缓存中存在某种类型的数据, 并不能确认其他类型数据存在或不存在. 因此, 唯一能确认的方法是咨询权威源.

QCLASS=* 限制: 如果使用 QCLASS=*, 则无法获得权威应答.

请求状态信息

由于解析器要高效执行其功能就必须能够复用处理多个请求, 因此每个待处理请求通常会用某个状态信息块表示.

该状态块通常包含:

1. 时间戳

用途: 指明请求开始的时间.

用法:

  • 时间戳用于判断数据库中的 RR 能否使用或是否已过期
  • 该时间戳使用先前为区域和缓存中的 RR 存储所讨论的绝对时间格式

TTL 解释:

  • 当 RR 的 TTL 表示相对时间时, 该 RR 必须是及时的, 因为它属于某个区域
  • 当 RR 具有绝对时间时, 它属于缓存, RR 的 TTL 会与请求开始时间戳比较

时间戳优势: 使用时间戳优于使用当前时间, 因为这样允许 TTL 为零的 RR 以常规方式进入缓存, 但仍可被当前请求使用, 即使由于系统负载、查询重传超时等原因经过了许多秒.

2. 工作限制参数

用途: 使用某种参数限制该请求将执行的工作量.

理由: 解析器响应客户端请求所做的工作量必须受限, 以防范:

  • 数据库中的错误, 例如循环 CNAME 引用
  • 操作问题, 例如网络分区导致解析器无法访问所需名称服务器

实现:

  • 对解析器向某个特定名称服务器地址重传某个特定查询的次数设置本地限制是必要的
  • 解析器还应有一个每请求全局计数器, 用于限制单个请求上的工作量
  • 该计数器应设置为某个初始值, 并在解析器执行任何动作 (重传超时、重传等) 时递减
  • 如果计数器低于零, 该请求以临时错误终止

并行请求: 注意, 如果解析器结构允许一个请求启动其他并行请求, 例如某请求需要访问名称服务器而导致并行解析该名称服务器地址, 则派生请求应以更低的计数器启动. 这可以防止数据库中的循环引用触发解析器活动的连锁反应.

3. SLIST 数据结构

RFC-1034 中讨论的 SLIST 数据结构.

用途: 如果请求必须等待外部名称服务器的应答, 该结构会跟踪请求状态.


7.2. 发送查询

如 RFC-1034 所述, 解析器的基本任务是构造一个能够回答客户端请求的查询, 并将该查询发送给能够提供信息的名称服务器.

查询构造挑战

解析器通常只有关于应询问哪些服务器的非常强的提示, 形式为 NS RR, 并且可能必须:

  • 根据 CNAME 修改查询
  • 根据委派响应修改正在询问的名称服务器集合, 这些响应会把解析器指向更接近期望信息的名称服务器

除客户端请求的信息外, 解析器可能还必须调用自身服务, 以确定希望联系的名称服务器地址.

解析器模型

本备忘录使用的模型假定:

  • 解析器在多个请求之间复用注意力, 其中一些来自客户端, 一些由内部生成
  • 每个请求由某种状态信息表示
  • 期望行为是解析器向名称服务器发送查询时:
    • 最大化请求被回答的概率
    • 最小化请求耗时
    • 避免过多传输

关键算法

关键算法使用请求状态信息来:

  • 选择下一个要查询的名称服务器地址
  • 计算超时, 如果响应未到达, 超时会触发下一步动作

下一步动作通常是向其他服务器传输, 但也可能是向客户端返回临时错误.

SLIST 初始化

起点: 解析器总是从一个待查询服务器名称列表 (SLIST) 开始.

初始内容:

  • 该列表将包含解析器已知的最近祖先区域对应的所有 NS RR
  • 为避免启动问题, 解析器应有一组默认服务器, 当没有合适的当前 NS RR 时向它们查询

地址添加: 随后解析器将名称服务器的所有已知地址加入 SLIST. 当解析器知道名称服务器名称但不知道其地址时, 可启动并行请求来获取这些服务器地址.

历史信息

为完成 SLIST 初始化, 解析器会把它拥有的任何历史信息附加到 SLIST 中的每个地址上.

典型历史数据:

  • 该地址响应时间的某种加权平均
  • 该地址的命中率 (即该地址到底多久会响应该请求)

重要说明:

  • 此信息应按地址保存, 而不是按名称服务器保存, 因为特定服务器的响应时间和命中率可能随地址而显著变化
  • 此信息实际上特定于解析器地址/服务器地址对, 因此有多个地址的解析器可能希望为自己的每个地址保存独立历史
  • 对于没有这种历史的地址: 预期往返时间的最坏情况应为 5-10 秒, 对同一局域网等情况可使用更低估计

委派处理: 注意, 每当跟随一个委派时, 解析器算法都会重新初始化 SLIST.

服务器选择和超时

排序: 这些信息建立可用名称服务器地址的部分排序.

选择策略: 每次选择一个地址时, 都应改变状态, 防止在所有其他地址都已尝试之前再次选择该地址.

超时计算: 每次传输的超时应比平均预测值高 50-100%, 以容许响应方差.

特殊情况

自举问题:

  • 解析器可能遇到这样的情况: SLIST 中任何名称服务器都没有可用地址, 而列表中的服务器又正是通常用于查找其自身地址的服务器
  • 这种情况通常发生在 glue 地址 RR 的 TTL 小于标记委派的 NS RR, 或解析器缓存了 NS 搜索结果时
  • 解析器应检测这种情况, 并从下一个祖先区域重新开始搜索, 或者从根重新开始

服务器错误:

  • 如果解析器从某个名称服务器收到服务器错误或其他异常响应, 应将其从 SLIST 中移除
  • 解析器可能希望立即安排向下一个候选服务器地址传输

7.3. 处理响应

处理到达的响应数据报的第一步是解析响应.

响应解析过程

该过程应包括:

1. 头部检查:

  • 检查头部是否合理
  • 当期望响应时, 丢弃作为查询的数据报

2. 节解析:

  • 解析消息各节
  • 确保所有 RR 格式正确

3. TTL 检查 (可选):

  • 作为可选步骤, 检查到达数据的 TTL, 查找 TTL 过长的 RR
  • 如果某个 RR 的 TTL 过长, 例如大于 1 周:
    • 要么丢弃整个响应
    • 要么将响应中所有 TTL 限制为 1 周

响应匹配

下一步是将响应与当前解析器请求匹配.

推荐策略:

  • 使用域头部中的 ID 字段进行初步匹配
  • 然后验证 question section 是否对应当前期望的信息

实现要求: 这要求传输算法把 domain ID 字段中的若干位专用于某种请求标识符.

特殊考虑

源地址变化:

  • 某些名称服务器发送响应时使用的地址不同于接收查询的地址
  • 也就是说, 解析器不能依赖响应一定来自它发送对应查询时使用的同一地址
  • 这种名称服务器问题通常见于 UNIX 系统

重传处理:

  • 如果解析器向某个名称服务器重传特定请求, 它应能使用任意一次传输得到的响应
  • 但是, 如果它使用该响应来采样访问名称服务器的往返时间:
    • 它必须能够确定哪个传输与该响应匹配 (并保存每个出站消息的传输时间)
    • 或者只基于初始传输计算往返时间

缺失区域数据:

  • 名称服务器偶尔会没有它按某些 NS RR 应该拥有的当前区域副本
  • 解析器应简单地将该名称服务器从当前 SLIST 中移除, 并继续

7.4. 使用缓存

一般而言, 我们期望解析器缓存它在响应中收到的所有数据, 因为这些数据可能有助于回答未来客户端请求.

但是, 有几类数据不应缓存:

不应缓存的数据

1. 不完整集合:

  • 当某个 owner name 有多个同类型 RR 可用时, 解析器应要么全部缓存, 要么全都不缓存
  • 当响应被截断, 且解析器不知道它是否拥有完整集合时, 不应缓存可能只是部分集合的 RR

2. 非权威数据不能优先于权威数据:

  • 缓存数据绝不应优先于权威数据使用
  • 如果缓存会导致这种情况, 则不应缓存该数据

3. 反向查询结果:

  • 反向查询结果不应缓存

4. 通配符查询结果:

  • 当 QNAME 包含 * 标签且数据可能用于构造通配符时, 不应缓存标准查询结果
  • 原因: 缓存不一定包含限制通配符 RR 应用所需的现有 RR 或区域边界信息

5. 可靠性可疑的数据:

  • 可靠性可疑响应中的 RR 数据
  • 当解析器收到未经请求的响应或非所请求的 RR 数据时, 应不缓存并丢弃它
  • 基本含义: 在缓存数据包任何部分之前, 应对整个包执行所有健全性检查

缓存更新策略

当解析器在响应中拥有某个名称的一组 RR, 并希望缓存这些 RR 时:

  • 它应检查缓存中是否已有 RR
  • 根据具体情况, 优先使用响应中的数据或缓存中的数据
  • 二者绝不应合并
  • 如果响应中的数据来自 answer section 中的权威数据, 则始终优先使用它

最佳实践摘要

高效解析器实现建议

  1. 尽可能使用单一 type 和 class 查询, 以更好利用缓存

  2. 维护每请求状态, 包括时间戳和工作计数器

  3. 保留每地址历史, 以便智能选择服务器

  4. 实现适当超时策略 (比预测时间高 50-100%)

  5. 处理特殊情况, 如自举问题和服务器错误

  6. 仔细解析响应, 包括头部检查和格式验证

  7. 选择性缓存 - 并非所有数据都应缓存

  8. 优先使用权威数据, 当权威数据和缓存数据都可用时


相关: 服务器侧细节见 6. 名称服务器实现