DNS 区域设置变更只重建受影响的记录:定向构建怎么规划、怎么证明安全
Cloudflare 的 Zone Builder 过去遇到区域设置变更会重建整个 DNS 区域。改造的关键不是直接跳过重建,而是让事件携带变更范围、把变更映射到五类构建中最小且可证明正确的一类、处理同名区域共享的元数据 key、在执行前用新快照复核,并在无法证明安全时保留全量构建。本文按这套决策链解释做法、边界与适用条件。
- 定向构建的前提是事件能说明“变了什么”;细节缺失或未知事件一律退回全量构建。
- 五种构建类型从“只重建指定名称的记录”到“全量重建”,选择标准是能否证明输出与全量构建逐字节一致。
- 同名域名的多个区域共享一个元数据 key,写元数据必须从最新视图整键重建,且先写记录再写引用它的元数据。
- 超过固定记录数阈值的超大区域被刻意排除在全量构建之外,这是一种用正确性风险换可用性的取舍。
结论先说:区域设置变更并不等于“只重建那条设置”。Cloudflare 在 2026 年 11 月初公布的做法的核心,是让事件说清“变了什么”,再把变更映射到五类构建里最小且可证明正确的那一类;证明不了,就退回全量构建。安全性的判据只有一个——定向构建写入的每个 key,必须与全量构建写出的逐字节相同。
下面是这套设计的决策链、边界条件,以及可以从自建 DNS 或配置分发系统里借鉴的部分。数据与结论来自 Cloudflare 工程博客的自述,属于厂商公布的工程结果,本次未做独立复现。来源:How we halved DNS build delay and cut memory use by 85%(2026-11-02)。
问题出在“工作量的增长方式”#
Zone Builder 的职责是把数据库里的区域状态转换成权威 DNS 服务器能用的格式,写入 Quicksilver(Cloudflare 的分布式键值存储)。博客给出的量级是:2022 年每分钟约 250 次 DNS 记录变更,现在约 700 次,另有约 100 个其他事件,例如区域设置变更。部分区域已超过 2000 万条记录,并预计向数亿条增长。
事件数量不是主要矛盾,矛盾在于某些事件所需的工作量随区域规模线性增长。原来的全量构建有一个很好的性质:它不需要知道区域为什么变。流程是读区域与设置、读全部记录、编码成 key、与 Quicksilver 中已有 key 比较、删除陈旧 key、写入变更 key、更新区域元数据。最终写出去的只有变化部分,但读、编码、比较全量数据,以及把这些数据同时放进内存,都要付出代价。对大区域来说,严重时甚至放不进内存。
定向构建必须先回答三个问题#
只要不是全量构建,就必须回答:哪些 DNS 名称可能变化?共享元数据或“应发布区域列表”是否变化?要从 Quicksilver 中删除哪些旧状态?博客把落地过程拆成五个工程问题:事件要说明变更范围;变更要映射到仍然正确的最小构建;构建要考虑同名区域共享的数据;事件与构建之间数据库可能已变;合并工作不能丢 Kafka 消息。
第一步:让事件携带变更范围#
改造成本最低、收益却最关键的一步,是让事件生产者在事件里带上变更的区域字段与设置,尽量附上旧值和新值。随后需要沿着“设置 → 生成 DNS 输出”的代码路径逐个追踪,判断每项设置可能影响哪些输出。这一步不能靠猜,因为漏判的代价是边缘留下过期数据。
并非所有事件都这么具体。有些数据库函数只更新一个时间戳,语义上相当于“这个区域有东西变了”。它可能只是记账,也可能意味着别处数据变了、必须重读。这两类情况在事件里长得一样。因此博客明确:只更新时间戳的事件、细节缺失的事件、未知事件,以及任何可能影响区域内任意记录的变化,一律触发全量构建。规划器不会把不确定性变成更少的工作,这是整套设计里最该记住的一条。
第二步:五类构建,按可证明性选择#
Quicksilver 的输出在这里分成两部分:记录 key 承载某个区域某个名称的 DNS 答案;共享元数据 key 则告诉权威服务器如何使用该区域,包括去哪里找 nameserver、通配符这类特殊记录。有的变更只动记录 key,有的两者都要动。
| 构建类型 | 重建范围 | 典型触发 | 是否更新共享元数据 |
|---|---|---|---|
| 仅资源 | 指定名称(资源)下的记录 | 普通 A、AAAA、TXT、CNAME 变更 | 否 |
| 资源与元数据 | 指定名称加共享元数据 | nameserver、通配符变更 | 是 |
| 顶点与元数据 | 区域顶点(如 example.com)加元数据 | Universal SSL 等设置 | 是 |
| 清除 | 移除不应再发布的区域 | 区域下线 | 相关 |
| 全量区域 | 全部记录 | 无法证明更小范围安全时 | 是 |
Universal SSL 是说明“为什么设置变更也会牵连记录”的好例子:启用或禁用它不仅改元数据,还可能改变区域顶点(裸域名,如 example.com)的 CAA 记录(Certification Authority Authorization,证书颁发机构授权),而 CAA 记录决定证书颁发机构能否为该域名签发证书。所以“只改了一项设置”并不等于“只改一个 key”。
选择逻辑可以用下面这段伪代码示意。它是未执行的示例,只表达决策顺序,不代表 Cloudflare 的真实实现:
# 演示用伪代码,未执行
def choose_build(event, zone):
if event.kind == "unknown" or event.detail_missing:
return full_zone()
if event.may_affect_any_record(zone):
return full_zone()
if event.is_purge:
return purge(zone)
if event.touches_apex_or_setting:
return apex_and_metadata(zone)
if event.touches_nameserver_or_wildcard:
return resources_and_metadata(event.names)
return resources_only(event.names) # A / AAAA / TXT / CNAME 常见路径
无论选哪一类,验收标准不变:写入的 key 必须与全量构建的结果一致。旧状态的清理沿用 2022 年按记录构建的做法——事件包含被删除的记录和更新前的版本,Zone Builder 扫描 Quicksilver 中每个待重建名称下的既有 key,删掉新构建没有写出的那些。
第三步:处理“一个名字,多个区域”#
这是定向构建中最容易被忽略的部分。Quicksilver 分开存放各区域的记录 key,但同时为同一个域名下的所有 Cloudflare 区域存一个共享元数据 key,作用类似目录:告诉权威服务器回答查询时可以用哪些内部区域 ID。
为什么一个域名会有多个区域 ID?在客户上线、下线及其他生命周期变更期间,同一个公共域名可能短暂存在多个区域对象,系统用优先级策略选出一个有界的小集合来发布。博客的例子是:example.com 名下有区域 101、202、303、404,元数据 key 当前只列出 101、202、303,404 存在但未被选中(此处域名为示意)。
由此得出两条规则:其一,对区域 202 中 www.example.com 的 TXT 记录变更只影响该区域的记录 key,元数据不变;nameserver 与通配符变更则必须同时更新元数据。其二,执行时 Zone Builder 会重新读取当前的发布列表,如果要写元数据 key,就从这份新视图整键重建,而不是在旧副本上打补丁;并且先写触发区域的记录,再写引用这些记录的元数据。顺序反了,边缘会短暂看到指向尚不存在记录的目录。
第四步:规划两次,写入前复核#
事件描述的是过去。构建可能排队等待,期间数据库已被其他变更改写。因此规划发生两次:事件到达时先产出一份保守的工作清单;真正执行前,再读取一份新的发布快照。博客正文在描述这一步时被截断,后续的比对细节本次材料未提供,这里不做推测。
合并同区域的工作由调度器负责,摘要说明它把同一区域的多个工作合并成一次,与规划器共同作用。具体的合并窗口、优先级与去重实现,材料未提供。
边界:什么时候仍要全量,什么时候刻意不全量#
博客明确保留了两类全量构建场景:变更可能影响区域内任意记录;区域可能需要首次发布。
但还有一个刻意的例外。全量构建要同时把区域全部记录和全部既有 key 放进内存,最大的区域放不下;一旦开始并失败重试,还会堵住同一 Kafka 分区上的所有其他变更。所以在超过一个固定记录数阈值后,Zone Builder 不再升级为全量构建,改为重建 apex、元数据,以及排队事件标识出的每一个名称。材料未给出该阈值的具体数值。
这是一次用正确性风险换可用性的取舍:作者的理由是全量构建在这些区域本来也跑不完。为降低风险,系统依赖 zone holds 阻止同名区域被添加到另一个账户,从而减少“多区域共享一名”的情形;剩下少数情况(例如在另一个网络启用区域)可以人工重建区域内个别名称。如果你在自己的系统里模仿,这一条需要单独评估:是否接受“没有全量兜底”的运维负担。
效果与口径#
据该博客自述,规划器与调度器合起来使峰值内存下降 85%、全量构建减少 40%,并从 DNS 变更到数据写入 Quicksilver 的时间减半;后半句成立的前提是内存余量变大后,管道原本为韧性保留的等待可以被安全缩短。这些是厂商公布的工程数据,未说明测量窗口与区域样本构成,本次未独立核验。
另有一组常被混用的数字:2022 年按记录构建平均比全量构建快 150 倍,在 100 万条记录的测试区域上快 4250 倍。那是记录级增量构建的旧结果,不是本次区域设置变更改造的效果,引用时不宜合并。来源同前。
可迁移的做法(建议,非来源结论)#
- 先把“能否证明等价”写成硬性验收条件,再谈减少工作量;证明不了就放大范围,而不是赌一次。
- 在事件模式里显式加入变更字段与新旧值;对只有时间戳的事件显式标记为“范围未知”,让下游默认走大范围路径。
- 把共享元数据与独占记录分开建模,写共享键时整键重建,并固定“先写被引用数据、后写引用它的目录”的顺序。
- 规划与执行之间留一次重读,避免用事件时刻的状态去覆盖执行时刻的数据库。
- 为超大区域单独设计路径,并把“失去全量兜底”记为显式风险,而不是隐式行为。
说明:文中 example.com 及区域编号为材料中的示意;伪代码为未执行示例;涉及阈值、调度器实现、二次规划校验细节的部分,材料未提供或已截断,均标注为待补充而非结论。
常见问题(FAQ)
为什么不能对所有变更都做定向构建?
定向构建怎么处理被删除或改动的旧数据?
超大区域为什么反而不做全量构建?
参考来源
以上为本文写作时的参考资料,引用观点以原文为准。