你的服务没挂,但用户已经跑了——一次DNS污染引发的血案

2026-09-12 7 0

你的服务没挂,但用户已经跑了——一次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。

相关文章

我从人工智障到人工智障终结者:OpenClaw帮我实现了什么
你的 JOIN 慢,不一定是缺索引
写了三年Go,你可能连context的取消都没整明白
我用了三个月OpenClaw,这些经验你一定要知道
我用了三个月OpenClaw,这些经验你一定要知道
写API接口这件事,80%的人交出的答卷都是不及格

发布评论