第三章:常见DNS异常类型详解
DNS解析这事儿,表面上看就是个「域名→IP」的转换。但你要是真在生产环境里踩过坑,就会发现返回的每一个状态码背后都藏着不同的故事。我最早被DNS坑,是刚接手一个电商平台的时候,用户反馈APP启动偶尔白屏,过一会儿又自己好了。查了一整天,最后发现是DNS返回了SERVFAIL,客户端没做重试,直接崩溃了。
咱们这一章,就把最常见的几种DNS异常类型掰开揉碎讲清楚。我会结合自己踩过的坑,告诉你每种异常怎么识别、怎么复现、怎么处理。
1. NXDOMAIN —— 域名压根不存在
NXDOMAIN,全称Non-Existent Domain。说白了就是DNS服务器告诉你:「这个域名我不认识。」
关键特征:DNS应答报文里RCODE=3。客户端收到这个值后,解析器通常会直接返回失败,不会再去别的DNS服务器问了。
我遇到过好几次这样的案例:运维更新了域名解析记录,但忘了在权威DNS上添加新域名。某个微服务的SDK里硬编码了旧域名,结果部署新集群后,服务启动时一查这个域名就报NXDOMAIN,整个启动流程直接卡死。
用dig命令很容易复现:
# 故意查一个不存在的域名
dig nonexistent.example.com
# 返回结果类似:
; <<>> DiG 9.18.0 <<>> nonexistent.example.com
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 45211
;; QUESTION SECTION:
;nonexistent.example.com. IN A
;; AUTHORITY SECTION:
example.com. 60 IN SOA ns1.example.com. ...
启动崩溃的触发条件:
- 应用启动时必须解析一个关键域名(比如注册中心、配置中心)
- 解析库把NXDOMAIN当作硬错误,不重试
- 有些老旧JDK的InetAddress.getByName()会直接抛UnknownHostException
我曾经踩过的坑:有一次我在测试环境模拟了NXDOMAIN,结果发现Java应用启动时抛了异常,但没打日志。原因是System.Logger配置了ERROR级别,UnknownHostException被吞掉了。排查浪费了半天。
2. SERVFAIL —— DNS服务器自己挂了
SERVFAIL(RCODE=2)比NXDOMAIN更诡异——它不是告诉你「域名不存在」,而是说「我查了,但查失败了」。可能是因为权威DNS挂了、DNSSEC验证不一致、或者上游递归超时。
我印象很深的一次:某云厂商的公共DNS节点出了故障,返回SERVFAIL但不明显——间歇性的,大约每5个请求里有1个返回SERVFAIL。我们有个内部监控系统每30秒拉一次APM数据,结果启动后的第一次请求刚好撞上SERVFAIL,进程就直接退出了。
为什么?因为那个监控初始化代码里没做重试,把SERVFAIL当成「域名不可恢复」,直接调用System.exit(1)。
| 常见根因 | 出现频率 | 我推荐的排查命令 |
|---|---|---|
| 上游权威DNS宕机 | 中 | dig +trace example.com 追查故障节点 |
| DNSSEC签名过期或验证失败 | 低 | dig example.com +dnssec 看AD标记 |
| 递归DNS内部资源耗尽 | 较高 | 使用备用DNS绕过去验证 |
# 模拟SERVFAIL – 使用一个关闭了端口53的本地DNS
$ dig @127.0.0.1 example.com
; <<>> DiG 9.18.0 <<>> @127.0.0.1 example.com
; (1 server found)
;; connection timed out; no servers could be reached
# 如果上游返回servfail,会看到:
;; status: SERVFAIL
小技巧:你可以在本地用dnsmasq设置一个上游为不可用,强制返回SERVFAIL,测试你的应用启动逻辑。我自己常用这种方法来验证重试机制是否生效。
3. 超时(Timeout)—— 最隐蔽的杀手
超时不是一种「错误码」,而是一种「没响应」。客户端发出查询后等了几秒,DNS服务器一句话都没回。这在网络不稳定时太常见了。
我见过最离谱的案例:某物联网设备在弱网环境下启动,DNS查询超时设置为2秒。结果启动时并发查询3个域名,每个都超时,总耗时超过6秒,同时线程池被占满,整个主线程卡死,看起来就像「启动崩溃」。
超时和SERVFAIL的区别:
- 超时:客户端没收到任何响应,解析库会尝试下一个DNS服务器(如果配置了多个)
- SERVFAIL:收到了响应,但内容是「I failed」,有些解析库不会自动重试
在Linux上你用dig +time=1 +tries=1可以强制超时测试:
# 给一个不通的IP发查询,1秒超时
dig @10.0.0.1 +time=1 +tries=1 example.com
# 输出会显示:
;; connection timed out; no servers could be reached
避坑指南:我曾经在代码里把超时时间设成3秒,以为够长了。结果有一次上游DNS响应慢到5秒,3秒超时成了常态。应用启动时连续超时,解析失败,直接崩溃。后来我改成「总超时=单次超时×重试次数」,并对关键域名单独设置更长的超时。
4. DNS拒绝(Refused)—— 权限问题
Refused(RCODE=5)的意思是:DNS服务器收到了你的请求,但它不想处理。通常是ACL(访问控制列表)或者递归策略导致。
举个例子:公司内网DNS服务器只允许内部IP递归查询,你如果从外部发请求,它就回复Refused。还有某些公共DNS(比如某些运营商的)对特定域名或特定客户端做了限制。
启动崩溃的常见场景:应用容器化迁移后,新的Pod IP不在DNS服务器白名单里,DNS返回Refused。解析库直接放弃,进程启动失败。
# 用dig模拟Refused – 对只允许特定源的DNS发起查询
dig @dns.company.internal example.com
# 如果被拒绝,结果类似:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 54321
;; QUESTION SECTION:
;example.com. IN A
;; AUTHORITY SECTION:
. 86400 IN SOA a.root-servers.net. ...
5. 其他异常类型(简要提及)
除了上面四种,还有几个我在生产环境也遇到过:
- FormErr(RCODE=1):请求格式错误,多半是DNS头部或EDNS0字段写错了
- NotAuth(RCODE=9):服务器不权威,但客户端却要求AA标记
- NoError + 空应答:这最坑!状态码是0,但answers列表为空。很多解析库当作成功返回null,导致应用拿到null直接NPE
重点总结:不管是哪种异常,如果你的应用在启动时没有做DNS解析的「容错设计」,只要命中一次异常,就可能触发崩溃。我个人的建议是:至少做3次重试,每次间隔指数退避,并且一定要记录原始异常信息到日志。
下一小节开始,我们会在实验室环境里把这些异常全部复现出来,你看代码跑一遍就会彻底理解了。这里先记住这张异常分类图就行。