用户登录

技术文章

外部链接将在新标签页打开。

DNS 区变更为何全量重建:把“为什么变”变成可证明的最小构建

一句话回答

旧版 Zone Builder 不读取事件原因,只按当前数据库状态重建整个区,这种策略在小规模下便宜且正确,但区记录数达到数千万后,全量构建既放不进内存又拖慢延迟。Cloudflare 的路径是让事件携带变更字段,由规划器选择可证明安全的最小构建类型,无法证明时退回全量,并对超大规模区设置例外。据其博客,全量构建减少 40%、峰值内存下降 85%、变更写入 Quicksilver 的延迟减半。

因为旧版 Zone Builder 不需要知道“这个区为什么变了”。它每次都读当前数据库状态,把整区记录重新编码、与 Quicksilver 中的现有键比对、写入差异。这个策略的正确性很容易论证,代价则是工作量随区规模线性增长。当单个区已有数千万条记录、并预计走向数亿条时,全量重建既无法在内存中容纳,也会把延迟推高。定向构建要做的不是“少做点事”,而是把“为什么变”变成一份可证明安全的最小工作清单;证明不了,就仍然全量重建。

瓶颈不是事件数量,是工作量的增长方式#

按 Cloudflare 博客的说法,2022 年 Zone Builder 每秒接收约 250 个 DNS 记录变更;到发文时约 700 个,另有约 100 个其他事件,例如区设置变更。数量增长近三倍,但作者明确说事件数从来不是真正的问题。问题在于某些事件所需要的那类工作,以及这类工作如何随区的大小增长。部分区已超过 2000 万条记录,预期会继续向数亿条增长。

这里的“构建”(build)有明确定义:读取当前数据库状态,转换成权威 DNS 服务器在边缘可用的格式,把发生变化的部分写入 Quicksilver——Cloudflare 的分布式键值存储。

全量构建曾经是“无需知道原因”的正确解#

原始 Zone Builder 有一个很有价值的性质:它不必理解事件语义。流程是固定的:

  1. 读取当前区及其设置;
  2. 读取该区全部 DNS 记录;
  3. 把记录编码为 Quicksilver 键;
  4. 与新键和已存键比对;
  5. 删除过期键、写入变更键、更新区元数据。

最终写入的确实只有差异,但“读出全部、编码全部、比对全部”这几步一步都省不掉,而且过程中整个区要驻留内存。小区无所谓;对拥有数千万条记录的区,区本身根本放不进内存。

2022 年的那篇博客给过一组对比:按记录定向的构建平均比全量构建快 150 倍;在一个 100 万条记录的测试区上快 4250 倍。这也是当时的收尾结论——记录级已经做完,下一步应该对区设置做同样的事。注意这两个数字是 Cloudflare 自报的历史对比,本次材料未提供测试环境、负载模型与测量方法的完整说明,不应直接当作可复现的性能基准。

设置类事件为什么难定向#

记录事件天然指明了变化对象。区设置事件则可能来自多个 Cloudflare 产品,未必说明变更原因,还可能改动被多个同名区共享的数据。博客举的例子是 Universal SSL:启用或关闭它会更新元数据,还可能改变区顶点(裸域名,如 example.com)上的 CAA 记录(Certification Authority Authorization,用于告知证书颁发机构是否可以为该域名签发证书)。

于是定向构建必须回答三个问题:

  • 哪些 DNS 名称可能发生变化?
  • 共享元数据或已发布区列表是否可能变化?
  • Quicksilver 中哪些旧状态必须被移除?

为安全回答这三问,博客列出五个必须先解决的问题:事件必须说明变了什么;每个变更要映射到仍然正确的最小构建;构建要覆盖同名区共享的数据;事件与构建之间数据库可能已变化;合并工作不能丢 Kafka 消息。

五种构建类型#

Quicksilver 的输出在这里分成两部分:记录键承载某个区某个名称的 DNS 答案;一个更小的共享元数据键告诉权威服务器如何使用该区,包括到哪找 nameserver、通配符这类特殊记录。据此规划器在五种粒度中选择:

构建类型重建范围典型触发
仅资源(Resources only)特定 DNS 名称下的记录普通的 A、AAAA、TXT、CNAME 变更
资源与元数据特定名称 + 共享元数据nameserver、通配符变更
顶点与元数据区顶点 + 元数据Universal SSL 一类设置
清除(Purge)移除不应再发布的区区下线
全区(Full zone)全部记录无法证明更小的工作安全时

规则只有一条,但很硬:定向构建写入的每一个键,必须与全量构建会写出的内容逐字节一致。证明不了,就做全量。旧状态的清理方式沿用 2022 年的记录级做法——事件携带被删除记录和更新记录的旧版本,Zone Builder 扫描其重建的每个名称在 Quicksilver 下的现有键,删掉新构建没有写入的那些。

这套约束也解释了为什么“只更新时间戳”的数据库更新仍然触发全量:这类事件在语义上与“区内某处数据变了”无法区分。在能识别调用方和用途之前,只有时间戳的事件、缺失细节的事件、未知事件,一律按全量处理。规划器不会把不确定性折算成更少的工作。

执行前还要再规划一次#

事件描述的是过去。构建可能排队等待,期间又有新事件到达、数据库也变了,所以规划发生两次:事件到达时生成一份保守的工作清单;真正执行前,再读一次最新的发布快照进行校验。本次抓取的正文在此处被截断,后续校验细节与失败处理未包含在材料中,这里不做补充。

同名多区:一个名称不止一个区#

记录键按区分别存储,但元数据键是该域名下所有 Cloudflare 区共享的一个键,作用类似目录:告诉权威服务器应答查询时可以用哪些内部区 ID。

一个域名有多个区 ID,是因为客户开通、关停及其他生命周期变更期间,同一公开域名可能短暂存在多个区对象。系统用优先级策略选出一个小的、有界的集合对外发布。博客的例子是 example.com 下有 Zone 101、202、303、404,元数据键当前列出 101、202、303;404 存在但未被选中。这些编号代表不同的 Cloudflare 区对象,客户看到的仍是一个域名。

由此推出两条执行规则:对 Zone 202 下 www.example.com 的 TXT 变更只影响 202 的记录键,元数据键不变;而 nameserver 与通配符变更必须同时更新记录与元数据键。执行时 Zone Builder 重新读取当前列表,如果要写元数据键,就从这份新视图整体重建该键,而不是在旧副本上打补丁;并且先写触发区的记录,再写指向它们的元数据。

大区的例外:永不升级为全量#

规则有一个刻意的例外。全量构建要同时把数据库里的全部记录和该区在 Quicksilver 中的全部现有键装进内存,最大的区做不到。因此超过某个固定记录数阈值后,Zone Builder 不再升级到全量,而是重建顶点、元数据,以及排队事件所标识的每一个名称。材料未公布该阈值数值,此处待核实。

这是被接受的权衡:对这些区而言全量构建根本无法完成,而它在重试期间会阻塞同一 Kafka 分区上的其他所有变更。多数本会落到全量的场景涉及多个区共享同一名称,而 zone hold 会阻止同名被加入另一个账户,从而降低风险;剩余罕见场景(例如在另一个网络上启用某个区)可以手工重建区内的单个名称。

延迟减半是怎么来的#

这一点容易被误读。定向构建并不是把单次构建的编码速度提高了一倍。博客的因果关系是:定向规划器与新调度器(合并同一区的工作)把峰值内存降低 85%,腾出的余量让团队可以安全缩短管线一直为韧性而保留的等待时间,于是从 DNS 变更到数据写入 Quicksilver 的时间减半;同时全量构建数量下降 40%。换句话说,内存余量被换成了延迟预算。

这四个数字(40%、85%、延迟减半)来自 Cloudflare 博客摘要及其正文,属于厂商自报结果;区间定义、统计窗口与测量方法在本次材料中不完整,无法独立复核。它们说明的是这条路线的量级,不是可直接引用的行业基准。

对自建变更管线的启示#

  • 让事件携带因果,而不是只带信号。事件里能写明旧值、新值和受影响字段,下游才有资格做定向;只有“某物变了”的信号,只能退回全量。
  • 把“字节级等价”当作降级边界。定向路径的正确性判据要能机械判定,判不了就走保守路径,而不是靠人工审查。
  • 内存是延迟预算的一部分。峰值内存下降之后,原本为安全留出的等待窗口才有条件缩短;反过来说,先压缩等待时间而不降内存,通常是在拿正确性换指标。
  • 合并与二次校验必须同时具备。合并同一对象的工作能省容量,但事件到执行之间的时间窗会变长,执行前的重新读取不是可选项。

需要说明的边界:以上内容依据 Cloudflare 博客的正文片段与摘要整理,正文在“执行前读取最新发布快照”之后被截断,因此调度器实现、阈值取值、失败重试的完整设计不在本文结论范围内。若要照此改造自有系统,建议先在自己的区规模上测出全量构建的内存峰值与耗时分布,再决定定向粒度——这些数据无法从公开博客直接移植。

参考来源

  1. How we halved DNS build delay and cut memory use by 85%

以上为本文写作时的参考资料,引用观点以原文为准。

作者:沐小悠

本文约 9 分钟读完 · 关键词:DNS 区构建、定向构建、Zone Builder、Quicksilver、增量构建

更多文章 GET 示例接口

「」标签下的其他文章