2026 年 1 月 8 日,Cloudflare 的 1.1.1.1 公共 DNS 解析器经历了一次看似微小却影响深远的变更。一次旨在优化内存使用的代码更新,意外改变了 DNS 响应中 CNAME 记录与 A 记录的相对顺序,导致全球范围内部分 DNS 客户端解析失败。这一事件揭示了 DNS 协议中一个存在近 40 年的模糊地带:CNAME 与 A 记录的优先级关系。
事件回顾:顺序即正确性
Cloudflare 的工程师在优化缓存实现时,修改了 CNAME 链合并的逻辑。原本的代码确保 CNAME 记录始终出现在响应列表的前端:
answer_rrs.extend_from_slice(&self.records); // CNAMEs first
answer_rrs.extend_from_slice(&entry.answer); // Then A/AAAA records
优化后的版本简化为:
entry.answer.extend(self.records); // CNAMEs last
这一看似无害的变更,却让 CNAME 记录有时出现在 A 记录之后。对于大多数现代 DNS 客户端,这不成问题。然而,一些广泛部署的客户端实现 —— 特别是 glibc 的getaddrinfo函数和某些 Cisco 交换机的 DNS 进程 —— 依赖顺序解析算法。
这些客户端按顺序遍历 DNS 响应,当遇到 CNAME 记录时,更新期望的域名,然后继续寻找匹配该域名的 A 记录。如果 CNAME 出现在 A 记录之后,算法会忽略不匹配当前期望域名的 A 记录,导致解析失败。
协议模糊性:40 年的技术债务
RFC 1034(1987 年发布)在 4.3.1 节中提到:"递归查询的响应可能是查询的答案,可能前面有一个或多个 CNAME RR,指定在到达答案途中遇到的别名。"
关键词是 "可能前面"(possibly preface)。这个表述没有使用现代 RFC 中明确的规范词(MUST、SHOULD),因为 RFC 2119(定义这些关键词的规范)在 1997 年才发布,比 RFC 1034 晚了 10 年。
更复杂的是,RFC 1034 主要讨论资源记录集(RRset)内的顺序无关性,但没有明确规定不同 RRset(如 CNAME RRset 和 A RRset)在消息段中的相对顺序。这种模糊性导致了实现差异。
工程解决方案一:缓存策略与 TTL 优化
分层缓存架构
现代 DNS 解析器应采用分层缓存策略,区分 CNAME 链缓存和终端 A 记录缓存:
- CNAME 链缓存:缓存完整的别名链关系,即使部分链段过期
- 终端记录缓存:独立缓存 A/AAAA 记录,基于各自的 TTL
Cloudflare 事件中的优化正是试图合并这两个缓存层以节省内存,但忽略了顺序依赖。
TTL 优化参数
基于 CNAME 和 A 记录独立缓存的特性,推荐以下 TTL 配置策略:
- CNAME 记录 TTL:设置为较长时间(如 3600-86400 秒),减少别名解析频率
- A 记录 TTL:根据 IP 稳定性设置(稳定 IP:300-3600 秒,动态 IP:60-300 秒)
- 负缓存 TTL:遵循 RFC 9520,失败缓存不超过 300 秒
关键实现细节:当 CNAME 链部分过期时,解析器应仅重新解析过期部分,而不是整个链。这要求缓存实现能够识别和分离链中的独立段。
工程解决方案二:客户端兼容性保障
强制 CNAME 优先顺序
尽管协议模糊,但为了确保向后兼容性,所有 DNS 解析器应强制实施以下规则:
- CNAME 记录必须出现在对应 A/AAAA 记录之前
- CNAME 链必须按解析顺序排列(从查询域名到规范域名)
- 同一响应中禁止 CNAME 与 A 记录共存(遵循 RFC 1034 独占性原则)
实现代码应包含明确的断言测试:
// 验证CNAME顺序的测试用例
fn test_cname_ordering() {
let response = build_dns_response();
let mut seen_cname = false;
let mut seen_a = false;
for record in &response.answer {
match record.rtype {
T_CNAME => {
assert!(!seen_a, "CNAME must appear before A records");
seen_cname = true;
}
T_A | T_AAAA => {
seen_a = true;
}
_ => {}
}
}
}
客户端重试机制设计
基于 RFC 9520 的规范,客户端重试机制应遵循以下参数:
- 最大重试次数:同一服务器 / 传输协议组合最多 3 次尝试(初始查询 + 2 次重试)
- 退避策略:指数退避,初始延迟 1 秒,最大延迟 5 秒
- 服务器标记:连续 3 次失败后,标记服务器不可用至少 300 秒
- 查询合并:对同一查询的并发请求应合并,避免 "重试风暴"
实现示例:
class DNSClientWithRetry:
def __init__(self):
self.server_failures = {} # 服务器地址 -> 失败时间戳
self.pending_queries = {} # 查询ID -> 回调列表
def query_with_retry(self, domain, server, max_retries=2):
if self._is_server_marked_down(server):
return self._try_alternative_server(domain)
query_id = self._send_query(domain, server)
self.pending_queries[query_id] = {
'domain': domain,
'server': server,
'retries': 0,
'max_retries': max_retries,
'callbacks': []
}
# 设置超时重试
self._schedule_retry(query_id, initial_delay=1.0)
def _schedule_retry(self, query_id, initial_delay):
query = self.pending_queries[query_id]
if query['retries'] >= query['max_retries']:
self._mark_server_down(query['server'])
return
delay = min(initial_delay * (2 ** query['retries']), 5.0)
# 安排重试定时器...
监控与告警要点
关键监控指标
- CNAME 顺序违规率:监控响应中 CNAME 出现在 A 记录之后的比例
- 解析失败分类:区分 CNAME 顺序失败、超时失败、服务器失败等
- 客户端兼容性矩阵:跟踪不同客户端版本对顺序变化的敏感性
- 缓存命中分层:分别监控 CNAME 链缓存和 A 记录缓存命中率
告警阈值建议
- CNAME 顺序违规:> 0.1% 时警告,> 1% 时严重告警
- 解析失败率:> 1% 时警告,> 5% 时严重告警
- 客户端影响面:影响特定客户端版本 > 5% 用户时告警
- 缓存效率下降:命中率下降 > 20% 时调查
部署与回滚策略
渐进式部署检查清单
-
预发布测试:
- 在测试环境中验证 CNAME 顺序保持不变
- 使用历史流量重放验证客户端兼容性
- 针对已知敏感客户端(glibc 特定版本)专项测试
-
金丝雀发布:
- 初始部署比例不超过 1%
- 密切监控 CNAME 顺序相关指标
- 准备即时回滚机制(回滚时间目标 < 5 分钟)
-
全面部署:
- 每小时增加部署比例不超过 10%
- 保持旧版本实例作为快速回滚目标
- 部署后持续监控至少 24 小时
紧急回滚参数
- 检测到问题时间:目标 < 2 分钟
- 决策时间:目标 < 1 分钟
- 回滚执行时间:目标 < 2 分钟
- 影响消除时间:目标 < 5 分钟
Cloudflare 事件中,从问题检测到回滚完成仅用了 76 分钟,但通过自动化监控和回滚流程,这一时间可以进一步缩短。
长期标准化建议
Cloudflare 已向 IETF 提交互联网草案(draft-jabley-dnsop-ordered-answer-section),建议明确规范 CNAME 记录顺序。工程团队应:
- 参与标准制定:贡献实现经验和边缘案例
- 提前适配:在标准正式发布前更新实现
- 推动客户端更新:鼓励老旧客户端实现更健壮的解析逻辑
总结
CNAME 与 A 记录的优先级冲突问题,本质上是协议模糊性与实现多样性之间的张力。工程解决方案需要在性能优化、内存效率和客户端兼容性之间找到平衡点。
关键实践要点:
- 始终保持 CNAME 优先顺序,即使协议没有强制要求
- 实现分层缓存策略,独立管理 CNAME 链和终端记录
- 遵循 RFC 9520 重试规范,避免过度查询
- 建立细粒度监控,特别是 CNAME 顺序相关指标
- 准备快速回滚机制,应对客户端兼容性问题
DNS 作为互联网基础设施的核心组件,其稳定性直接影响全球网络可用性。通过工程化的方法处理这类协议边缘案例,是构建可靠网络服务的重要保障。
资料来源:
- Cloudflare 博客文章《What came first: the CNAME or the A record?》(2026-01-14)
- RFC 9520: Negative Caching of DNS Resolution Failures
- Cloudflare DNS 文档:Records with the same name
- RFC 1034: Domain Names - Concepts and Facilities