查了 3 天 DNS 间歇超时,排除了网络、CNI、kube-proxy——最后发现是 CoreDNS 自动扩缩容的参数设反了
场景:微服务集群 ~120 个 Service,高峰期部分 Pod 报
Temporary failure in name resolution,API 响应从 <50ms 飙到 3s+ 路径:Pod DNS 验证 → CRI 容器状态 → CoreDNS QPS 日志 → Autoscaler 配置 版本:K8s v1.25, CoreDNS v1.9.3, cluster-proportional-autoscaler v1.8.6
上篇讲了 Gateway API 与 Ingress 共存时的隐性冲突——新旧两套路由规则叠加,控制器层面的兼容性坑。
这篇我们看看另一个让集群"看起来正常但间歇抽风"的隐蔽配置:CoreDNS 自动扩缩容的参数。
【坐标】Pod 间歇 DNS 超时——但节点网络一切正常
业务大面积超时,查了一圈全正常
周一早上 10:00,报警群里炸了:线上环境突然大面积服务调用超时,重试后能恢复,但过几分钟又超时。
API 响应时间走势:
10:00 之前 P50=12ms P99=45ms
10:03 之后 P50=1.2s P99=8.5s ← 飙了 100 倍
涉及 7 个核心服务,影响 3 条业务线。第一时间怀疑网络抖动。
Day 1——查节点网络:ping 通、curl 通、kubectl get po 全部 Running。节点 eth0 丢包率 0%,带宽正常。Calico 日志无报错。kube-proxy iptables 规则数 2847 条,正常。
Day 2——查 CNI + kube-proxy:Calico Felix 健康检查通过,BGP 路由正常。kube-proxy iptables 到 IPVS 模式切换一切正常。Service 端口 telnet 能连。
两天排查结束,毫无进展。
Pod 内 dig,发现 DNS 全部超时
Day 3,再查应用日志,发现一个共同点:所有超时调用的调用链里,都在 DNS 解析环节卡住了。 不是网络问题,是 DNS 问题。
在 Pod 里直接 dig 验证——集群内 Service 和外网全部超时,但 telnet 10.96.0.10 53 端口却能连上:

DNS 完全超时。但这不是网络问题——是 CoreDNS 自身扛不住了。
关键洞察:DNS 超时的外在表现(timeout/retry/reset)和网络丢包几乎一样——这是为什么 80% 的人第一反应走错方向的原因。
【分层】Pod → CRI → CoreDNS → Autoscaler,逐层不跳
第 1 层:Pod 内验证——链路通但解析失败
Pod 内 dig @<cluster-dns-ip> 超时,但直接访问 cluster-dns-ip:53 端口却可以建立 TCP 连接。
kubectl -n kube-system get svc kube-dns
# CLUSTER-IP: 10.96.0.10 ✔ 正常
kubectl -n kube-system get endpoints kube-dns
# ENDPOINTS: 10.244.1.5:53,10.244.2.7:53 ✔ 有后端
这说明一条关键结论:Pod 到 CoreDNS Service 的网络链路是通的,但 CoreDNS 自身处理不过来了。
链路通 + 解析不通 = 问题不在网络层,在 DNS 服务层。
第 1.5 层:CRI 容器视角(很多人跳过的关键层)
如果 CoreDNS 因 OOM 或 CPU throttled 重启过快,kubectl logs 可能读不到最后几秒的日志。这时候要用 crictl——它能看到已退出容器的历史日志和 cgroup CPU 是否被 throttled:

这次 CoreDNS Pod 没重启,kubectl logs 可用,跳过这层直接到下一步。但在排查实践中,容器退出快时 crictl 是唯一能看到最后几秒日志的手段。 尤其是 CoreDNS 这种高 QPS 服务,OOM 退出只在一瞬间。
第 2 层:CoreDNS 自身日志——SERVFAIL 全面爆发
kubectl -n kube-system logs deploy/coredns --tail=100 一看,满屏的 SERVFAIL:
[INFO] 10.244.1.8:35271 - 48716 "A IN myservice.production.svc.cluster.local. udp 0 false 512" SERVFAIL - 0 0.0s
[INFO] 10.244.1.8:35271 - 48717 "A IN myservice.production.svc.cluster.local. udp 0 false 512" SERVFAIL - 0 0.0s
[INFO] 10.244.1.9:45322 - 48718 "A IN otherservice.default.svc.cluster.local. udp 0 false 512" SERVFAIL - 0 0.0s
[INFO] 10.244.1.8:35271 - 48719 "A IN myservice.production.svc.cluster.local. udp 0 false 512" SERVFAIL - 0 0.0s
[INFO] 10.244.1.10:51234 - 48720 "A IN apigw.default.svc.cluster.local. udp 0 false 512" NOERROR - 0 0.0s
[INFO] 10.244.1.10:51234 - 48721 "A IN apigw.default.svc.cluster.local. udp 0 false 512" NOERROR - 0 0.0s
[INFO] 10.244.1.8:35271 - 48722 "A IN myservice.production.svc.cluster.local. udp 0 false 512" SERVFAIL - 0 0.0s
SERVFAIL 是最关键的一个信号。 它说明 CoreDNS 收到了请求(不是网络丢包),但处理不过来(超时或内部错误)。
统计一下 QPS:
kubectl -n kube-system logs deploy/coredns --tail=1000 | grep -c SERVFAIL
# 623
kubectl -n kube-system logs deploy/coredns --tail=1000 | wc -l
# 870 → 870 行日志中 623 个 SERVFAIL,错误率 71%
再看资源消耗:
kubectl -n kube-system top pod -l k8s-app=kube-dns
# NAME CPU(cores) MEMORY(bytes)
# coredns-7d6f7c8b9-5tkn2 85m 45Mi
# coredns-7d6f7c8b9-9wp3v 92m 52Mi
2 个 CoreDNS Pod,每个已经吃到了 ~90m CPU,但 limit 是 100m——已经到天花板了。这是资源瓶颈的直接证据。

CoreDNS 的正常处理路径是:接收 UDP 查询 → 查缓存(如果命中直接返回)→ 查 kube-apiserver(如果缓存未命中)→ 返回响应。当 QPS 超过单 Pod 的处理能力时,请求排队的 goroutine 堆积、超时、最终回复 SERVFAIL。这不是崩溃,是慢死。
第 2.5 层:Prometheus 看 CoreDNS 真实 QPS(如果有监控)
如果集群装了 Prometheus + CoreDNS metrics(CoreDNS 默认暴露 :9153/metrics),可以更精确地看:
# 用 promQL 查 CoreDNS 每秒请求数
sum(rate(coredns_dns_requests_total{zone="."}[5m])) by (pod)
# 结果:
# coredns-5tkn2: 4200 qps
# coredns-9wp3v: 3800 qps
# 合计 ~8000 qps,而正常基线是 2000 qps
对比前一周同期数据,正常 QPS 是 ~2000,峰值 8000——翻了 4 倍。集群规模(节点数和 CPU 核数)这周完全没变,所以 autoscaler 认为不需要扩。
第 3 层:Autoscaler 配置——根因就在这
data:
clusters: |-
{
"coresPerReplica": 256,
"nodesPerReplica": 16,
"min": 2,
"max": 8,
"preventSinglePointFailure": true
}
这个 ConfigMap 是 cluster-proportional-autoscaler 的配置。它决定 CoreDNS 的期望副本数,但不是基于 QPS——而是基于集群的节点数和 CPU 总核数。你新增节点了吗?CoreDNS 会扩。你 QPS 翻了 4 倍?它不关心,集群规模没变。
它的计算公式:
replicas = max(min, ceil(cores / coresPerReplica), ceil(nodes / nodesPerReplica))
代入当前集群:36 核 × 24 节点:
ceil(36 / 256) = 1 ← 每 256 核才需要 1 个副本
ceil(24 / 16) = 2 ← 每 16 节点需要 1 个副本
max(min=2, 1, 2) = 2
所以一直只有 2 个副本。高峰期 QPS 翻了 4 倍,但 coresPerReplica=256 和 nodesPerReplica=16 参数太高,autoscaler 认为不需要扩——从它的视角看,集群确实没变。
核心理解:cluster-proportional-autoscaler 算的是"集群规模 vs CoreDNS 容量"的静态比例,不是"实际负载 vs CoreDNS 容量"的动态关系。集群节点数不变但业务流量翻倍——CoreDNS 不会主动扩。这不是 bug,是设计使然——它是 proportional autoscaler(比例扩缩),不是 load-based autoscaler(负载扩缩)。

那为什么 K8s 默认用 coresPerReplica=256?因为这个值来自 K8s 社区的基准测试:单 CoreDNS 副本在 1 核 CPU 的典型环境下实测能处理约 4000-6000 qps,一台 256 核的集群预期 QPS 不超过 4000。问题在于:这个假设在微服务密集的场景下完全不成立。 每个 Pod 每秒都要做多次服务发现 DNS 查询,120 个 Service × 每个 Service 多个 Pod × 每个 Pod 多次查询 = 8000+ qps 很轻松。
【路径】🔍 下次先跑这组命令
遇到 DNS 间歇超时,不要先查网络。按这个顺序——每条命令 10 秒,4 步共 40 秒完成初判:
Step 1:看 CoreDNS 报错(SERVFAIL/NXDOMAIN)
kubectl -n kube-system logs deploy/coredns --tail=50 | grep -E 'SERVFAIL|NXDOMAIN|timeout'
Step 2:看 CoreDNS CPU 是否打满
kubectl -n kube-system top pod -l k8s-app=kube-dns
Step 3:看副本数和 autoscaler 配置
kubectl -n kube-system get pod -l k8s-app=kube-dns | grep -c Running
kubectl -n kube-system get configmap coredns-autoscaler -o yaml
Step 4:从 Pod 内实测 DNS
kubectl run dnsutils --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3 --restart=Never -- dig @10.96.0.10 myservice.production.svc.cluster.local

【定位】查网络还是查 DNS?为什么 80% 的人走错
❌ 大多数人:先查网络(Day 1-2)
DNS 超时的表象(timeout / retry / connection reset)和网络丢包太像了。团队第一反应一定是查:
- 节点网卡丢包——
netstat -i看 dropped - CNI 插件问题——Calico/Cilium 日志
- kube-proxy iptables 规则——
iptables -L - Service 端口通不通——
telnet
这些查完都没问题,花了 2 天。 为什么?因为排查方向完全错了——你在找"网络丢包"的证据,但 DNS 超时的原因根本不在网络。
✅ 正确:先分清楚"链路"和"服务"(Day 3)
方向是:Pod 到 CoreDNS 的网络链路通不通(telnet 端口),和 CoreDNS 能不能处理查询(是否返回 SERVFAIL),是两回事。 不要混在一起查。
# 链路通
telnet 10.96.0.10 53 # 连接成功
# 但处理不了
dig @10.96.0.10 myservice # SERVFAIL
这两个命令的差异,决定你该查网络还是该查 DNS。
为什么人会走错这个方向——心理因素
因为"DNS 超时"四个字里的"超时"带偏了排查路径。网络超时和 DNS 服务端超时,在调用方看到的现象完全一样——都是卡住几秒后报 timeout。
这在认知心理学上叫表象归因偏差:两个现象看起来一样,大脑自动归类为同一原因。不登录 CoreDNS Pod 看日志,你分不清是"请求没到 CoreDNS"还是"CoreDNS 收到了但回不来"。
区别方法只有一个:telnet CoreDNS 53 端口看连接是否建立。能连上但解析失败 → 问题在 CoreDNS 自身。
这个操作 10 秒就能做完。如果团队 Day 1 先跑这个,能省 2 天。

【标点】修复——让 CoreDNS 真正扛住高峰流量
修复 1:调低 autoscaler 阈值
# before → after
coresPerReplica: 256 → 64
nodesPerReplica: 16 → 8
重新计算:ceil(36/64)=1, ceil(24/8)=3, max(min=2,1,3)=3。从 2 副本变成 3 副本,多了 50% 的 DNS 吞吐容量。
但这只是缓解——因为 autoscaler 仍然不感知 QPS。业务再翻一倍还是会超时。
修复 2:叠加 HPA 基于 CPU 扩缩容
当 CoreDNS CPU 使用率超过 70% 时,HPA 独立于 autoscaler 扩缩,真正感知实际负载:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
minReplicas: 2
maxReplicas: 10
metrics:
- resource:
name: cpu
target:
averageUtilization: 70
HPA 和 cluster-proportional-autoscaler 同时作用时,两者会各自计算期望副本数,Deployment 取最大值。推荐让 autoscaler 设定基线(最小副本数,确保集群增长时同步扩),HPA 做动态调节(应对流量突发)。
修复 3:Corefile 增加缓存
Corefile: |
.:53 {
cache 60 # 缓存从 30s 加到 60s,降低重复查询
# kubernetes 插件内部也会缓存 Service 解析结果(默认 30s ttl)
}
缓存从 30s 加到 60s,相同 Service 的 DNS 查询减半。对于高频查询的服务(如 API Gateway),效果显著。
修复 4:设置 Prometheus 告警
# 报警规则:CoreDNS SERVFAIL 率 > 5%
sum(rate(coredns_dns_responses_total{rcode="SERVFAIL"}[5m]))
/ sum(rate(coredns_dns_responses_total[5m]))
> 0.05
这样 CoreDNS 开始报错时,不用等到用户投诉,告警会先到。

这次故障的完整 Timeline
| 时间 | 动作 | 结果 |
|---|---|---|
| Day 1 10:00 | 报警 → 查节点网络 | 网络正常,无进展 |
| Day 1 14:00 | 查 CNI Calico | 正常运行,无进展 |
| Day 2 09:00 | 查 kube-proxy iptables/IPVS | 规则正常,无进展 |
| Day 2 16:00 | 查 Service/CoreDNS 端口 telnet | 端口通,更困惑 |
| Day 3 10:00 | 查 CoreDNS 日志 | SERVFAIL 全面爆发 |
| Day 3 11:00 | 查 autoscaler 配置 | 找到根因:coresPerReplica=256 |
| Day 3 11:30 | 调低阈值 | 副本数 2→3, SERVFAIL 消失 |
总结:前 2 天在错误的层级上排查,第 3 天才进入正确的路径。
CoreDNS 排查 Check-list
| Step | 命令 | 判断标准 |
|---|---|---|
| 1. 看 CoreDNS 是否有错误 | kubectl -n kube-system logs deploy/coredns \| grep SERVFAIL |
有 → CoreDNS 处理不过来 |
| 2. 看 CoreDNS CPU 使用 | kubectl -n kube-system top pod -l k8s-app=kube-dns |
接近 limit → 资源瓶颈 |
| 3. 看 autoscaler 参数 | kubectl -n kube-system get cm coredns-autoscaler -o yaml |
coresPerReplica 太高 → 扩不动 |
| 4. 从 Pod 内验证 | kubectl run dnsutils --image=... -- dig @10.96.0.10 myservice |
timeout/SERVFAIL → CoreDNS 问题 |
| 5. 看链路 vs 服务 | telnet 10.96.0.10 53 能连 + dig 超时 → CoreDNS 自身 |
两者不一致 → 问题不在网络 |
| 6. Prometheus 看 QPS | rate(coredns_dns_requests_total[5m]) |
QPS 翻倍且 CPU 打满 → 需扩容 |
附:完整命令清单
# 看 CoreDNS 是否有 SERVFAIL
kubectl -n kube-system logs deploy/coredns --tail=100 | grep -E 'SERVFAIL|timeout|REFUSED'
# 看 CoreDNS 资源用量
kubectl -n kube-system top pod -l k8s-app=kube-dns
# 看 autoscaler 配置
kubectl -n kube-system get configmap coredns-autoscaler -o yaml
# 看 CoreDNS 当前副本数
kubectl -n kube-system get pod -l k8s-app=kube-dns
# 从 Pod 内测试 DNS 解析
kubectl run dnsutils --image=registry.k8s.io/e2e-test-images/jessie-dnsutils:1.3 --restart=Never -- dig @10.96.0.10 kubernetes.default.svc.cluster.local
# CRI 层:容器已退出时用 crictl 看日志
crictl ps --name coredns
crictl logs <container-id> --tail=100
# 查看 CoreDNS Corefile 配置
kubectl -n kube-system get configmap coredns -o yaml
# Prometheus 查询 CoreDNS QPS
# sum(rate(coredns_dns_requests_total{zone="."}[5m])) by (pod)
故障排查的终点不是修好了——是把排查路径写成 check-list
这次学到的 3 个教训: 1. DNS 超时 ≠ 网络超时,先 telnet 端口区分"链路"和"服务" 2. cluster-proportional-autoscaler 不感知 QPS——适合兜底基线,不适合应对突发 3. 排查 K8s 问题要分层:Pod → CRI → Node → 集群,跳过一层就可能走 2 天弯路
下篇我们聊节点级的网络带宽限制——TC 和 CNI 流量整形导致的诡异问题。