你的服务没挂,但用户已经跑了——一次DNS污染引发的血案
凌晨三点,Slack炸了。"服务不可用"的报警堆了三十多条。我揉着眼睛打开监控,看到的曲线让我一度怀疑自己还没醒:
请求成功率在 99.2% 徘徊,p99延迟只有 180ms,所有指标看起来岁月静好——但前端的错误日志已经刷屏了两小时。
这是什么鬼?监控说没毛病,用户说出大问题。
经过八小时的死磕,我发现了一个几乎没有任何国内技术博客讲清楚的问题:你的DNS正在悄悄给用户返回过期地址,而你的服务"看起来完全正常"。
故事从一个"玄学"bug开始
我们的微服务部署在 Kubernetes 上,用 CoreDNS 做服务发现。某个版本上线后,总有零星用户反馈:
"你们App打开是空白页,但刷新两次又好了"
看后端日志,接口调用完全正常,没有任何超时或报错。前端说网络请求确实发出去了,但没有响应。
你猜怎么着?
我用 tcpdump 在pod里抓了十分钟包,终于抓到证据:DNS返回了一个已被驱逐的Pod IP,TCP三次握手永远无法完成——但这个IP在Kubernetes API里已经不存在了,所以健康检查依然通过,监控完全没感知到。
# 抓包看到的DNS响应
# 域名解析到了 10.244.1.45,但这个pod早已被驱逐
# SYN包发出,永无回应
tcpdump -i eth0 port 53 | grep "10.244.1.45"
# 10:23:41.123456 IP 10.244.1.45.53: 192.168.1.100.45678: Query: A api-service.default.svc.cluster.local
# 10:23:41.124567 IP 10.244.1.45.53 > 192.168.1.100.45678: 192.168.1.100.45678: [1a][1au] ...
健康检查只检自己是否存活,不检DNS是否还在指向自己。这是 Kubernetes 里最容易被忽视的隐患之一。
DNS污染的本质:不是"污染",是"过期"
很多人以为DNS污染是被人恶意篡改了,或者DNS服务器被黑。实际上在K8s环境里,99%的情况是:
DNS缓存的TTL过期了,但上游IP已经变了。
来看一个典型的CoreDNS配置:
# Corefile
api-comck.com {
forward . 8.8.8.8 8.8.4.4
cache 30 # 全局缓存30秒——这是默认值,很多人不知道
}
这个 cache 30 意味着所有DNS响应会被缓存30秒。你觉得"30秒很短啊"?
但问题来了:上游DNS返回的TTL是多少?
# 用dig查一下权威DNS返回的TTL
dig api.your-domain.com
;; ANSWER SECTION:
api.your-domain.com. 300 IN A 123.123.123.123
权威DNS说300秒,CoreDNS本地缓存30秒——这里CoreDNS听本地配置的。但如果你把 cache 30 改成了 cache 300(想减少上游查询压力),而上游IP在业务高峰期变更了Pod……
恭喜,接下来5分钟所有新请求都会打到那个"还在缓存里但已不存在"的IP上。
更隐蔽的凶手:Negative Caching
如果说普通DNS缓存TTL是明枪,那Negative Caching就是暗箭。
Negative Caching 指的是:DNS服务器会把"这个域名不存在"的响应也缓存起来,以免反复查询不存在的域名浪费资源。
这个机制本身没问题,但问题出在:这个缓存时间是由权威DNS的 SOA记录 里的 minimum 字段决定的,而这个值在国内很多域名注册商那里默认是 7200秒(2小时),甚至有的是 86400秒(24小时)。
dig SOA api.your-domain.com
;; AUTHORITY SECTION:
api.your-domain.com. 3600 IN SOA ns1.provider.com. admin.your-domain.com. (
2026091201 ; serial
7200 ; refresh (2 hours)
3600 ; retry
1209600 ; expire
3600 ) ; minimum TTL
也就是说:如果你手滑把API地址从 123.123.123.123 改成了 124.124.124.124,然后DNS记录更新了——但有个节点的Negative Cache里还缓存着"没有这个域名"的响应,那接下来2小时内,这个节点上所有请求都会失败。
最骚的是:这种情况在你自己的开发/测试环境几乎无法复现,因为测试环境的DNS缓存策略和企业内网环境完全不同。
生产环境里的三大DNS地雷
地雷一:Nginx Ingress 的 resolver 配置
Kubernetes 里用 Nginx Ingress 做七层负载均衡的人,大概率踩过这个坑:
# 错误配置
server {
location / {
proxy_pass http://api-service; # upstream名字
# 没有显式指定resolver
}
}
这个配置在K8s里能work,但resolver默认用的是Pod启动时IP——如果你的Ingress Controller重启过,resolver IP变了,但upstream名字还是那个,Nginx会一直用旧的DNS解析结果去连接已经不存在的Service IP。
正确做法:显式指定集群内的DNS服务器
server {
location / {
resolver kube-dns.kube-system valid=10s ipv6=off;
set $upstream_endpoint http://api-service;
proxy_pass $upstream_endpoint;
}
}
注意那个 valid=10s:这让Nginx每10秒刷新一次DNS缓存,而不是"第一次解析后永久缓存"。
地雷二:gRPC 的 DNS 刷新间隔
gRPC 默认的 DNS 刷新间隔是 30秒,听起来挺合理。但问题是:如果你的服务在做蓝绿部署或金丝雀发布,gRPC的客户端可能因为DNS刷新不及时,把大量请求发到旧版本实例上——导致切流不彻底,新版本流量一直上不去。
// Go gRPC 连接选项
conn, err := grpc.Dial(
"api-service:50051",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
// 强制开启客户端侧负载均衡,否则DNS刷新了也没用
)
另一个更大的坑:gRPC的负载均衡策略。如果你不显式指定 round_robin,gRPC默认用的是 pick_first——第一次解析DNS时拿到所有IP,但只连第一个,后面的全部忽略。DNS更新了?对不起,不关我的事。
地雷三:本地开发环境的 /etc/hosts 覆盖
这个是我见过最"人祸"的DNS问题。
开发人员为了调试,在本地 /etc/hosts 里加了一条:
127.0.0.1 api.your-domain.com
调试完,忘了删。本地测试没问题,上了预发——DNS说这个域名指向127.0.0.1,但预发环境根本没有这个服务在本地跑,请求直接RST。
更隐蔽的是:有些工具(如 Docker Desktop、VPN客户端)会自动修改hosts,测试人员毫不知情。
我的DNS排错清单(实操版)
遇到"明明服务活着但请求失败",按这个顺序查:
第一步:确认DNS解析到了什么IP
# 在客户端侧执行,看实际返回什么
nslookup api-service.default.svc.cluster.local
# 或者
dig api-service.default.svc.cluster.local +short
第二步:抓包确认TCP层发生了什么
# 看DNS响应包里是否有有效IP,以及后续TCP是否完成握手
tcpdump -i any -n port 53 or port 80 or port 443 -w /tmp/dns_trace.pcap
# 然后用 wireshark 打开分析
第三步:检查K8s里这个IP对应的Pod是否还活着
kubectl get pods -o wide | grep <IP前三位>
# 例如 kubectl get pods -o wide | grep 10.244.1
第四步:查CoreDNS日志,看它返回了什么
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=100 | grep "api-service"
第五步:如果确认是Negative Caching,手动flush缓存
# CoreDNS flush(如果用了nodelocaldns)
curl http://<nodelocaldns-ip>:8080/cache/flush
为什么你的监控没报警?
回到开头那个问题:为什么请求成功率99.2%,但用户实际体验已经很差了?
因为健康检查只检查"服务是否存活",不检查"用户能否真正访问到服务"。
你的探针可能永远打的是127.0.0.1或者localhost,根本没走网络层。而用户请求走的是真实网络,DNS解析到错误IP,TCP握手失败——这种失败在你的四层/七层健康检查里根本看不到。
解决方案:让你的健康检查也走完整的DNS解析+TCP连接链路,不要偷懒只检查本地端口是否监听。
写在最后
DNS是互联网最古老的协议之一,也是最容易被"信任"的——大多数人只在"域名解析不到"的时候才想起它。但真正让人头皮发麻的DNS问题,往往不是"解析不到",而是"解析到了,但那个IP已经不属于你了"。
你的服务没挂。但用户已经跑了。
这不是玄学,这是DNS。
下次遇到"指标全绿但用户炸了"的时候,记得看一眼DNS。