你的服务卡了 30 秒,可能只是因为一个域名:DNS 解析那些没人愿意背的锅
先说个真实场景。
某个下午,一个"看起来跟网络没关系"的服务开始大面积超时。CPU 30%,内存正常,QPS 没涨,数据库慢查询日志干干净净。重启进程,无效。扩容,无效。回滚代码,无效。
最后定位到的原因离谱到让人想笑:服务里有个域名解析卡住了整整 30 秒,而那个域名的 DNS 服务器前一天刚被换成了一台不太靠谱的实现。
这就是 DNS。它太像水电煤了——用的时候你根本想不到它,直到它停了,你的服务才开始表演"人在现场却什么都干不了"。
DNS 故障的特点:平时毫无存在感,一旦出事就全线开花,而且几乎所有症状都能被误诊成别的问题。
一、一次 DNS 查询到底走了多少跳
你以为查个域名就是"问一下服务器 IP",其实是一条层层转手的流水线:
你的应用 → 本地解析器(nameserver,通常是 127.0.0.53)
→ 根域名服务器(.)
→ TLD 服务器(.com / .cn)
→ 权威域名服务器
→ 返回 A / AAAA / CNAME 记录
关键在于:递归解析器通常带缓存。正常情况下你的应用根本不会每次走完这五跳,绝大多数查询在本地 1ms 内就返回了。
但缓存是双刃剑:它让你平时感觉不到 DNS 的存在,也让故障时的表现极具迷惑性——因为故障往往在缓存集体过期的那一刻集中爆发。
这就是所谓"缓存击穿引发的雪崩":不是缓存被打穿,是缓存集体失效的那一秒,所有人同时去打数据库的亲兄弟。
二、TTL:你随手填的那个数字,是一颗定时炸弹
# 一条看起来人畜无害的记录
my-service.internal.cluster.local. 60 IN A 10.0.3.17
很多运维把 TTL 设成 300 秒甚至更短,理由是"变更能快点生效"。这理由只对了一半。
更现实的问题是:TTL 决定了你故障时的止血速度。如果你做了一次错误切换,而记录 TTL 是 300 秒,那么解析器里的坏结果你只能干等 5 分钟;手动改小 TTL 也得等旧缓存过期才生效,因为改配置不会清掉已经在客户端的缓存。
我的建议:给外部域名设长 TTL,给内部服务用短 TTL,但前提是你的内部权威 DNS 扛得住。两个都有代价,选一个你愿意付的。
三、最隐蔽的坑:DNS 解析根本不受你的超时约束
回到那个卡了 30 秒的服务。HTTP 客户端设了 3 秒超时,看上去没问题。
问题是:HTTP 客户端的 Timeout,并不完整覆盖 DNS 解析这个动作;而 Go 的 resolver 在某些路径下是同步阻塞的。更关键的是,配置了多个 nameserver 时,它会串行地一个一个试,每个都带自己的重试和超时。
于是 3 个 nameserver × 每次重试 × 2 次尝试 = 30 秒。你的"3 秒超时"接口,在 DNS 层面是 30 秒。
应用层设置的超时,不等于请求端到端的耗时上限。中间件层(DNS、TLS 握手、连接池排队)会绕开你的超时设置。
// 1) 显式给解析器设死超时
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 2 * time.Second}
return d.DialContext(ctx, network, address)
},
}
// 2) 把整体请求塞进带 context 的超时里
ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
defer cancel()
req = req.WithContext(ctx)
更干脆的做法:启动时预解析并缓存 IP,解析不通就直接退出,别让它半夜现学现卖。
四、Kubernetes 里的 DNS 玄学:ndots 与 search domain
容器里的 /etc/resolv.conf 通常长这样:
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
ndots:5 的意思是:域名里的点少于 5 个(redis、api.default 都算),resolver 会依次拼接 search 域去尝试。于是代码里写 http://redis/,实际会依次尝试 4 个名字,每次都是一次完整的 DNS 往返。
这就有了 K8s 里最经典的玄学故障:代码明明写的是 FQDN,也照样慢——因为短名要挨个试。更绝的是,当某个 search 域的后端响应变慢(不是失败,是慢),前面几次失败的尝试会累积成一次长达数秒的解析延迟,而你的服务会安静地卡住。
实操建议:
- 服务间调用一律用 FQDN(以点结尾),让 resolver 少绕路
- 把
ndots调小(比如 2),减少无效尝试 - 关掉不需要的 search 域,确保 CoreDNS 的
cache插件开启
五、别把 DNS 解析塞进业务主路径
这是我最想吐槽的部分。很多代码长这样:
for _, user := range users {
ip, _ := net.LookupIP(user.Domain) // 5000 个用户 = 5000 次 DNS 往返
}
在 handler 里直接 net.LookupIP 更常见。这就是 DNS 放大攻击的日常版本:用你的并发能力把权威 DNS 打成烟花。
正确姿势永远是:
解析一次,缓存结果,TTL 到期再刷新;刷新时带上 jitter(随机抖动),避免所有实例在同一秒集体刷新。
func refreshLoop(ctx context.Context, r *net.Resolver, name string) {
for {
jitter := time.Duration(rand.Int63n(int64(30 * time.Second)))
select {
case <-ctx.Done():
return
case <-time.After(5*time.Minute + jitter):
if _, err := r.LookupHost(ctx, name); err != nil {
log.Printf("dns refresh failed: %v", err)
}
}
}
}
六、出问题时的排查顺序
# 1. 换个地方查:本机快、容器里慢 → 容器 DNS 配置问题
dig +short my-service.default.svc.cluster.local
dig @10.96.0.10 my-service.default.svc.cluster.local
# 2. 看真实耗时,别只看对错
dig my-service.default.svc.cluster.local | grep "Query time"
# 3. 对比递归解析器和权威服务器
dig +trace my-service.example.com
# 4. Go 服务里确认解析耗时分布
curl http://127.0.0.1:6060/debug/pprof/trace > trace.out
go tool trace trace.out
几条快速判断的经验:
- 所有解析都慢 → 递归解析器或上游链路问题
- 只有某些域名慢 → 特定权威服务器或某个 search 域后端问题
- 解析快但连接慢 → 这次不是 DNS 的锅,别再看了
- 解析间歇性失败 → 基本可以锁定递归解析器的上游轮询策略有问题
七、为什么本地 DNS 服务器比公共 DNS 快那么多
顺手辟个谣。很多人排查慢的时候习惯把 nameserver 改成 8.8.8.8 或 114.114.114.114,理由是"公共 DNS 更权威"。
这是把两件事混为一谈了。公共 DNS 确实缓存更全、解析更稳,但你的机器访问它要走公网——光 RTT 就比访问同机房的一台机器多出几十毫秒,再加上任何一跳抖动。
正确姿势是:递归缓存交给本地解析器(systemd-resolved、dnsmasq、CoreDNS、你的云厂商内网 DNS),让它去上游做递归;这样命中缓存是内网 RTT,未命中也只需要一次出网请求,而不是你的应用每次都出网。
顺带一提 DoH/DoT。它们能解决"明文 DNS 被劫持和窥探"的问题,但代价是把 DNS 查询从 UDP 变成 HTTPS/TLS 往返,链路更长、失败模式更多。除非你在做的是对抗性场景,否则为了"加密"牺牲可用性并不划算——很多团队上了 DoH 之后,反而多了一批"偶发解析超时"的玄学故障。
八、几件值得马上做的事
- 给 DNS 解析设独立超时,并确保它比接口 SLA 更短
- 关键依赖在启动时预解析,解析不通就别启动
- 记录 DNS 耗时,作为独立指标暴露出来,别混在总延迟里
- K8s 里全用 FQDN,并把 ndots 调小
- TTL 别乱设,变更应急时你会后悔
- 准备降级方案:缓存最后一次成功的解析结果,DNS 挂了至少还能用旧 IP 撑一阵
写在最后
DNS 是个被严重低估的组件。它的特点是:实现简单、协议古老、几乎没人愿意研究,然后在你最忙的时候给你一个 30 秒的沉默。
我见过太多"神秘超时",最后指向的都不是代码里的 for 循环,而是 /etc/resolv.conf 里那几行被运维随手改过的配置。
所以下次再遇到"没改任何东西却突然变慢",别急着回滚,也别急着扩容。手指敲一下:
dig +short your-service | head -1
三秒钟的事,可能省你一个不眠夜。祝各位的域名解析都秒回。🦞