Files
dns-server/.trae/documents/dns-servfail-issue-analysis.md
T
Alex Yang f9e2e5a6bc update
2026-04-12 21:40:22 +08:00

369 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DNS 查询 SERVFAIL 问题分析(已禁用 DNSSEC
## 问题描述
客户端请求 DNS 解析时,需要请求两次才能得到正常结果:
* 第一次请求:返回 SERVFAIL(服务器失败)
* 第二次请求:返回正常解析结果
## 重要信息
**DNSSEC 已禁用** - 因此问题与 DNSSEC 验证无关
## 可能的原因分析
### 1. 缓存初始化时机问题
**现象**:第一次查询时缓存为空,第二次查询命中缓存
**可能原因**
* 缓存加载逻辑可能存在问题
* 第一次查询的响应没有正确缓存
* 缓存键生成或查找逻辑有问题
**相关代码位置**
* `/root/dns-server/dns/cache.go` - `Get``Set` 方法
* `/root/dns-server/dns/server.go` - `handleCacheResponse` 方法
### 2. DNSSEC 验证超时或失败
**现象**:第一次查询触发 DNSSEC 验证,验证超时导致 SERVFAIL;第二次直接使用缓存结果
**可能原因**
* DNSSEC 验证逻辑可能存在超时
* 验证失败后没有正确处理响应
* `verifyDNSSEC` 方法可能阻塞或失败
**相关代码位置**
* `/root/dns-server/dns/server.go` - `verifyDNSSEC` 方法
* 第 1563 行左右的 DNSSEC 验证逻辑
### 3. 上游服务器选择逻辑问题
**现象**:第一次查询时服务器状态统计为空,导致选择了不可用的服务器
**可能原因**
* 服务器健康检查逻辑可能有问题
* 第一次查询时没有正确选择最快的服务器
* 并行查询逻辑可能存在竞态条件
**相关代码位置**
* `/root/dns-server/dns/server.go` - `forwardDNSRequestWithCache` 方法
* 第 1473-1600 行的并行查询逻辑
### 4. 响应处理逻辑问题
**现象**:第一次查询返回的响应被错误地判断为失败
**可能原因**
* 响应代码判断逻辑可能有误
* `hasValidRecords` 判断可能过于严格
* 某些类型的响应被错误地拒绝
**相关代码位置**
* `/root/dns-server/dns/server.go` - 第 871-891 行的响应验证逻辑
### 5. 并发锁竞争问题
**现象**:第一次查询时遇到锁竞争,导致超时返回 SERVFAIL
**可能原因**
* 缓存访问锁可能与其他操作冲突
* 统计信息更新可能导致阻塞
* 日志写入可能阻塞查询流程
**相关代码位置**
* `/root/dns-server/dns/cache.go` - 各种锁操作
* `/root/dns-server/dns/server.go` - 统计更新操作
## 调试建议
### 方案 1:增强日志记录
在关键位置添加详细日志:
* 缓存命中/未命中时
* DNSSEC 验证开始/结束/结果
* 上游服务器选择和响应
* 响应验证过程
### 方案 2:检查时间戳
分析查询日志中的时间戳:
* 第一次查询的实际处理时间
* 是否有超时现象
* DNSSEC 验证耗时
### 方案 3:复现测试
使用 `dig` 命令测试:
```bash
# 清除缓存后测试
dig @127.0.0.1 example.com +norecurse
dig @127.0.0.1 example.com
```
## 根本原因已确定
经过详细代码分析,发现问题出在 **并行请求模式下的响应处理逻辑缺陷**
### 问题流程:
1. **第一次查询**
* 缓存未命中,向上游服务器发起并行查询
* 代码进入并行请求模式(第 1490-1777 行)
* 收到上游返回的响应,`resp.response.Rcode == dns.RcodeSuccess`
* **关键问题**:在 `noDNSSEC` 模式下(第 1572-1588 行),代码直接返回第一个成功响应
* **但是**:没有检查响应是否包含有效记录(Answer/Ns/Extra
* 如果上游返回的是 `Rcode=Success``Answer 为空` 的响应(例如某些 DNS 服务器的截断响应)
* 该响应被直接发送给客户端
*`handleUpstreamRequest` 第 870-891 行,由于 `hasValidRecords` 检查失败(Answer 为空),`response.Rcode` 没有被设置为 `RcodeSuccess`
* 最终客户端收到 SERVFAIL 错误
2. **第二次查询**
* 第一次查询的响应已经被缓存(即使 Answer 为空,只要 Rcode=Success 就会被缓存)
* 直接从缓存返回
* 但此时缓存的可能是完整响应(因为上游第二次返回了完整结果)
* 客户端收到正常解析结果
### 具体代码问题:
**问题 1:并行模式下缺少 hasValidRecords 检查**
`/root/dns-server/dns/server.go` 第 1572-1588 行(并行模式,noDNSSEC 分支):
```go
if noDNSSEC && !bestResponseSent {
// 不验证 DNSSEC 的域名:直接返回第一个成功响应
if fastestResponse == nil || resp.rtt < fastestRtt {
fastestResponse = resp.response
fastestRtt = resp.rtt
fastestServer = resp.server
fastestDnssecServer = dnssecServerForResponse
fastestHasDnssec = false
// 立即发送结果,快速返回(只发送一次)
resultChan <- struct {
response: fastestResponse, // 可能 Answer 为空!
...
}
bestResponseSent = true
}
}
```
**对比**:在串行模式(第 1452-1469 行)中有 `hasValidRecords` 检查:
```go
// 检查响应是否包含有效的记录,如果包含,将 Rcode 设置为成功
hasValidRecords := false
if len(response.Answer) > 0 {
hasValidRecords = true
} else if len(response.Ns) > 0 {
hasValidRecords = true
} else if len(response.Extra) > 0 {
for _, rr := range response.Extra {
if rr.Header().Rrtype != dns.TypeOPT {
hasValidRecords = true
break
}
}
}
if hasValidRecords {
response.Rcode = dns.RcodeSuccess
}
```
**问题 2:Rcode 判断逻辑不准确**
第 1562 行只检查 `resp.response.Rcode == dns.RcodeSuccess`,但有些 DNS 服务器可能返回:
* `Rcode=Success``Answer 为空`(例如需要 TCP 回退时)
* 这种情况下应该继续等待其他服务器的响应,而不是立即返回
## 建议的修复步骤
### 方案 1:在并行模式下添加 hasValidRecords 检查(推荐)
**问题**:并行模式(noDNSSEC 分支)缺少 hasValidRecords 检查
**解决方案**
在并行模式的快速返回逻辑中添加 hasValidRecords 检查,确保只返回包含有效记录的响应
```go
// 修改前
if noDNSSEC && !bestResponseSent {
// 不验证 DNSSEC 的域名:直接返回第一个成功响应
if fastestResponse == nil || resp.rtt < fastestRtt {
fastestResponse = resp.response
fastestRtt = resp.rtt
fastestServer = resp.server
fastestDnssecServer = dnssecServerForResponse
fastestHasDnssec = false
// 立即发送结果,快速返回(只发送一次)
resultChan <- struct {
response: fastestResponse,
...
}
bestResponseSent = true
}
}
// 修改后
if noDNSSEC && !bestResponseSent {
// 不验证 DNSSEC 的域名:返回第一个包含有效记录的成功响应
if fastestResponse == nil || resp.rtt < fastestRtt {
// 检查响应是否包含有效记录
hasValidRecords := false
if len(resp.response.Answer) > 0 {
hasValidRecords = true
} else if len(resp.response.Ns) > 0 {
hasValidRecords = true
} else if len(resp.response.Extra) > 0 {
for _, rr := range resp.response.Extra {
if rr.Header().Rrtype != dns.TypeOPT {
hasValidRecords = true
break
}
}
}
// 只有包含有效记录才返回
if hasValidRecords {
fastestResponse = resp.response
fastestRtt = resp.rtt
fastestServer = resp.server
fastestDnssecServer = dnssecServerForResponse
fastestHasDnssec = false
// 立即发送结果,快速返回(只发送一次)
resultChan <- struct {
response: fastestResponse,
...
}
bestResponseSent = true
}
}
}
```
### 方案 2:改进 Rcode 判断逻辑
**问题**:只检查 Rcode,不检查 Answer 内容
**解决方案**
将 Rcode 检查和 Answer 检查结合起来,确保响应真正有效
```go
// 修改前
if resp.response.Rcode == dns.RcodeSuccess || resp.response.Rcode == dns.RcodeNameError {
// 处理响应
}
// 修改后
// 检查 Rcode 和 Answer 内容
isValidResponse := false
if resp.response.Rcode == dns.RcodeSuccess {
// 成功响应需要包含有效记录
if len(resp.response.Answer) > 0 || len(resp.response.Ns) > 0 {
isValidResponse = true
}
} else if resp.response.Rcode == dns.RcodeNameError {
// NXDOMAIN 响应可以没有 Answer
isValidResponse = true
}
if isValidResponse {
// 处理有效响应
}
```
### 方案 3:增加重试机制
**问题**:第一次返回空 Answer 后没有重试
**解决方案**
检测到空 Answer 时,继续等待其他服务器的响应或重试
```go
// 在并行模式中添加重试逻辑
if len(resp.response.Answer) == 0 && resp.response.Rcode == dns.RcodeSuccess {
// Answer 为空,可能是截断响应,继续等待其他服务器
logger.Debug("响应 Answer 为空,继续等待其他服务器", "domain", domain)
continue // 不立即返回,继续等待
}
```
### 方案 4:调整上游 DNS 服务器配置
**问题**:某些上游 DNS 服务器可能返回截断响应
**解决方案**
1. 检查上游 DNS 服务器配置
2. 尝试更换更可靠的上游 DNS 服务器
3. 增加 UDP 缓冲区大小,避免截断
## 推荐修复顺序
1. **立即修复**:方案 1(在并行模式下添加 hasValidRecords 检查)- 直接解决问题
2. **优化改进**:方案 2(改进 Rcode 判断逻辑)- 提高健壮性
3. **可选优化**:方案 3(增加重试机制)- 进一步提高可靠性
## 需要查看的关键代码
1. `verifyDNSSEC` 方法实现
2. `forwardDNSRequestWithCache` 中的 DNSSEC 验证逻辑
3. `handleUpstreamRequest` 中的响应处理
4. DNSSEC 相关配置和超时设置