假设你和几位同事搭起了一个小网络,你们的计算机之间能够通信,但是你们还没有自己的DNS服务器。
你们有一台机器,专门用来保存各种文件,这台机器的IP地址 是 192.0.2.10。
每次有人想要下载文件,都需要手动输入192.0.2.10这个地址,可是大家真正想找的是“文件服务器”,并不想记住它的那串数字。
那就给这个文件服务器起一个容易记住的名字吧。
每台计算机都要保存所有地址吗?
我们把文件服务器叫作 files,邮件服务器叫作 mail,开发机器叫作 dev,再把对应关系保存到一个文件中:
192.0.2.10 files
192.0.2.20 mail
192.0.2.30 dev
有了这个文件,计算机去查询这个文件,把名字替换成对应的IP地址,这样我们就不用再记那些IP地址了。
这是 hosts 文件的基本思路。
给十台电脑各复制一份,大家就能使用相同的名字。
但如果这个网络里有一万台计算机呢?
如果文件服务器的IP地址从 192.0.2.10 换到了 192.0.2.40,谁来通知所有人?如果有人没来得及更新自己的hosts文件,那么他就访问不了files服务器了。
于是我们想到,我们可以指定一个服务器,让它来统一存放这张表,其他计算器定期从它那里下载。
早期互联网确实这样做过:由网络信息中心 NIC 集中维护 HOSTS.TXT,各主机取得副本使用。
但是随着主机增多,文件分发和集中更新逐渐成为负担。1
我只想知道一台机器的地址,为什么必须下载整张表?
Computer A 想连接 files,先向查询服务器询问files服务器的地址,得到 192.0.2.40 后,再去请求文件服务器。

先查地址,再连接目标;查询服务器不转发文件。图中省略了握手和文件传输的具体报文。
“通过名字寻找对应信息”的过程,叫作名字解析(name resolution)。它解决了找地址的问题,但不保证目标一定能连上:服务器关机、网络不通,都可能让后面的连接失败。
查询服务器自己也需要一个入口。我们先把它的 IP,例如 192.0.2.53,写进客户端配置。
电脑仍需保存地址,但只需知道查询服务器的IP地址,而不必掌握整张通讯录。
下载全表的问题解决了,但是hosts文件的管理却仍集中在一台服务器上。
每所学校、每家公司修改自己的机器,都要访问同一台服务器。
增加查询服务器可以分担这台服务器的请求,但我们还可以进一步分散数据的管理。
全世界的名字,谁来管理?
我们可以让学校维护学校的记录,公司维护公司的记录,各自安排服务器,修改自己的数据。
不过,学校和公司都把文件服务器叫作 files,跨组织查询时该找哪一台?
把“属于谁”也放进名字就行。用两个虚构组织举例:2
files.school.example → 192.0.2.40
files.company.example → 192.0.2.80
局部名字都叫 files,完整名字却不同。
只要 school.example 和 company.example 的分配已经协调好,它们就能各自决定下面叫什么,不必为了新增一台机器再检查全世界的名字。
这些按层级组织的名字就是域名(domain name);命名树中一个节点及其下面的名字范围,构成一个域(domain)。从右向左看 files.school.example,范围逐步缩小。这种结构表达名字的归属,不代表机器的物理位置。
数据分开以后,原先的上级入口也可以简化:不用记住学校的每台机器,只需记住学校这部分交给谁负责。
例如,查询 files.school.example 时,上级可以回答:“请继续问 ns.school.example。”学校的服务器再给出最终地址。
这类指向下一位负责人的回答叫作转介(referral)。把子域的管理职责交给另一组权威服务器,叫作委派(delegation);DNS 用 **NS 记录(Name Server record)**记录相关服务器的名字。3

箭头表示管理与委派关系,不是数据包的路线。实际可以为一个区域配置多台权威服务器。
学校负责维护并发布自己管理范围内的数据,相应的服务器叫作权威 DNS 服务器(authoritative name server)。
普通机器换地址,通常只需更新学校一侧的数据;查询入口发生变化时,才可能还要维护上级的委派信息。
NS 记录给出的是服务器名字,查询程序仍需取得它的 IP。必要时,父区会附带入口的地址记录,避免“先问学校才能找到学校入口”的循环依赖,这些地址记录叫作胶水记录(glue records)。它们提供的是查询服务器的地址,不是 files 的最终地址。
同样的分工可以逐层进行。命名树的共同起点叫作根(root),用 . 表示;真实 DNS 的根下面有 .com、.edu 等顶级域,再往下是各自的子域。根由多台服务器提供服务;名字里每多一个点,也不要求额外部署一台服务器。
现在,我们有了可追随的查找路径。但每个客户端都要沿着这条路逐级询问吗?
查一个地址,需要自己一路问下去吗?
Computer A 可以只提交一个任务:“请帮我查到 files.school.example 的地址。”专门接受这类任务、继续寻找答案的服务,就是递归解析器(recursive resolver)。
客户端配置的 192.0.2.53 可以提供这种服务。递归解析器取得上级的转介,再联系学校的权威 DNS,最后把结果返回 Computer A。

图中假设解析器已知 example 这一层的入口,省略根查询、入口地址的辅助查询和重试。这里的 .example 仍是教学设定。
递归和迭代的区别,在于谁负责继续查。 Computer A 请求“帮我查完”,是在请求递归服务;助手收到“去问下一台服务器”的转介后,自己继续询问,是在执行迭代解析。同一次名字解析中,两者可以配合出现。4
助手也需要起点。自行执行迭代查找的解析器,可以预先准备根服务器的名称和地址,称为根提示(root hints)。没有更近的可用线索时,就从根开始。实际配置中,解析器也可以把任务转交给另一个上游解析器。
这并没有把全球数据重新集中起来。学校、运营商、公共 DNS 服务都可以提供递归解析器,分别服务不同客户端;助手负责查询,仍由各区域的权威服务器负责发布自己的数据。
这也解释了 nslookup 常见的 Non-authoritative answer(非权威回答):递归解析器把取得的结果交给我们,但通常不是目标区域的权威服务器。“非权威”不等于错误或过期,“权威”也不等于回答已经通过了身份认证。3
如果 Computer B 随后又向同一个助手查询相同记录,刚才的结果应该还能派上用场。
刚查到的答案,可以用多久?
解析器可以保存查到的结果,让后面的查询复用,这就是缓存(cache)。权威服务器随记录给出 TTL(Time To Live,生存时间),以秒为单位,规定它可以按普通缓存规则复用多久。5
假设 files.school.example 返回 192.0.2.40,TTL 为 300 秒。解析器收到后开始计时,60 秒后,Computer B 查询同一记录,就可以直接取得缓存答案,不必重新联系上游。
此时返回的剩余 TTL 约为 240 秒,命中缓存不会把倒计时重新变回 300 秒。如果答案继续被下游缓存,传递的也应是剩余期限。解析器可以更早丢弃记录,但不能因为有人访问,就不断重置旧答案的有效期。
如果学校在第 180 秒把权威记录改成 192.0.2.90 呢?这次修改不会自动推送到所有缓存。原先那份 192.0.2.40 还有 120 秒可以复用,因此新旧答案可能暂时并存。

黄色区间内,权威已经发布新地址,解析器仍可返回有效期内的旧地址。时间轴按比例绘制,采用无提前刷新等机制的基础缓存模型。
TTL 给的是缓存期限,并不保证地址在这段时间内绝不改变。短 TTL 通常能缩短旧缓存带来的滞后窗口,代价是更频繁的查询。计划迁移时,提前降低 TTL 还需要等待旧期限过去;修改权威记录,不能追溯缩短已经分发出去的倒计时。
普通缓存到期后,在下一次需要时重新取得结果,也不一定从根重来:解析器可能仍保存着可用的 NS 等中间线索。DNS 缓存到期也不会自动断开已经建立的文件传输连接。
缓存还可能存在于应用或操作系统中。若本机已有可用结果,网卡上可能看不到新的 DNS 查询;若只是递归解析器命中缓存,本机通常仍会向它提问,只是它不再访问上游。抓包时,要分清自己观察的是哪一段通信。
从一张本地对照表出发,我们逐步得到了一套分工明确的查询系统:名字按层级组织,数据由各方维护,解析器负责寻找答案,缓存减少重复劳动。这就是 DNS 的核心骨架。
-
本文是基于真实背景的设计推演,不严格复述历史发明顺序。早期 HOSTS.TXT 的集中维护与分发问题见 RFC 1034 §2.1。文中的学校、公司、时间及
192.0.2.*地址均为教学示例。 ↩︎ -
.example为文档和示例保留,文中的学校、公司及委派并非实际公网配置。见 RFC 2606 §2。 ↩︎ -
委派、NS、胶水与区域管理见 RFC 1034 §4.2。权威属性针对具体区域和回答,由响应中的 AA 标志表达;一台服务器也可以兼任不同职责,见 RFC 1035 §4.1.1。 ↩︎ ↩︎
-
递归服务和解析算法见 RFC 1034 §4.3.1与§5.3。只负责向递归服务提出请求的客户端组件,通常称为存根解析器(stub resolver)。 ↩︎
-
TTL 定义见 RFC 1035 §4.1.3。这里采用基础缓存模型;提前刷新、按规则使用过期数据等机制另见 RFC 8767。DNS 也可以暂存不存在等负回答,规则见 RFC 2308;网络超时本身不能证明名字不存在。 ↩︎