在 Kubernetes 集群里,一个让人头疼的问题是:Pod 明明调度成功了,却长时间卡在 ContainerCreating 状态,describe 出来的事件里赫然写着 no such host。更多的时候,问题出在应用容器启动阶段去解析某个 Service 域名,而此时这条 DNS 记录还没有在集群内传播完成,解析失败后容器直接退出或初始化失败。这种 DNS 传播延迟看似随机、难以复现,其实背后有明确的机制可以分析。本文从 DNS 解析链路入手,梳理延迟产生的根因,并给出一套可落地的排查与处理方案。

先弄清楚集群 DNS 的解析链路
Kubernetes 默认通过 CoreDNS 提供服务发现能力。当一个容器发起域名解析时,请求会按照容器内 /etc/resolv.conf 的配置逐级进行:先查容器的 search 域,再查集群域 cluster.local,最后才查外部域名。kubelet 会为每个 Pod 注入类似下面的配置:
nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5
这里的 ndots:5 是关键。它意味着只要域名中的点数少于 5 个,解析器都会先把 search 域依次拼接后再尝试,最后才走绝对查询。举例来说,容器里访问一个外部地址 api.ipipp.com,实际上会产生四次查询:api.ipipp.com.default.svc.cluster.local、api.ipipp.com.svc.cluster.local、api.ipipp.com.cluster.local,以及最终真正的 api.ipipp.com。每一次查询都是一次网络往返,任何一环超时,整体解析就会变慢甚至失败。
再看传播这一侧。Service 和 Endpoint 的变化由 kube-apiserver 写入 etcd,CoreDNS 通过 Watch 机制感知变化并更新内存中的缓存记录。正常情况下这个延迟在毫秒级别,但当 apiserver 负载高、网络抖动或者 CoreDNS Pod 本身资源不足时,Watch 事件的处理就会出现明显滞后。此外,客户端 libc 解析器自身没有缓存(Go 应用则依赖内部的 nss 解析逻辑),每一次失败都会真实地打到 CoreDNS 上,形成放大效应。
常见故障场景与排查路径
遇到创建失败时,不要急着重启,先按固定顺序收集证据。第一步用 kubectl describe pod 查看事件,确认失败发生在镜像拉取、卷挂载还是容器启动阶段。如果是应用日志中出现 Lookup XXX: no such host,基本可以锁定 DNS 问题。第二步进入一个同命名空间的调试 Pod 验证解析:
# 安装了 dnsutils 的调试容器 kubectl run -it --rm debug --image=registry.k8s.io/e2e-test-images/jessie-dnsutils -- sh # 在容器内执行 nslookup my-svc.default.svc.cluster.local cat /etc/resolv.conf
如果调试 Pod 能解析而业务 Pod 不能,多半是时序问题——业务 Pod 启动时 DNS 记录尚未生效,属于典型的传播延迟。反过来如果调试 Pod 也解析失败,就要看 CoreDNS 自身状态了:检查 CoreDNS Pod 是否 Running、是否发生 OOMKilled、kubectl logs -n kube-system -l k8s-app=kube-dns 中是否有 SERVFAIL 或循环代理报错。
还有一个容易被忽视的场景:headless Service 配合 StatefulSet 使用时,Pod 的 DNS 记录依赖 Pod 就绪。滚动更新过程中,旧 Pod 记录被删除、新 Pod 记录尚未写入,客户端拿到的可能是过期答案。另外,如果 CoreDNS 的 forward 插件把 cluster.local 也转发到了上游,会造成查询环,日志中会出现 loop 检测并直接退出的现象,这类问题在自建上游 DNS 的环境里特别常见。
从根源上缓解传播延迟
缓解方案分几个层次。最直接的是启用 NodeLocal DNSCache,它以 DaemonSet 形式在每个节点跑一个缓存实例,把 ClusterIP 的 DNS 查询改为本机 169.254.20.10 的 UDP 查询,减少 iptables NAT 和 conntrack 竞争,同时本地缓存能显著降低 CoreDNS 的压力:
# 官方提供的部署清单 kubectl apply -f nodelocaldns.yaml # 验证节点上的监听 ss -lunp | grep 169.254.20.10
第二个层次是调优 CoreDNS 本身。给 CoreDNS 的 Pod 设置合理的 CPU 请求与限制(DNS 是 CPU 密集型服务,限流会直接推高查询延迟),并将副本数与节点规模匹配。Corefile 中可以开启日志与监控插件,配合 Prometheus 指标 coredns_dns_request_duration_seconds 观察延迟分布,判断瓶颈在集群内传播还是上游转发。
第三个层次在应用侧。对短域名的外部访问,显式写成 FQDN 并补上结尾的点,例如 api.ipipp.com.,可以跳过 search 域拼接,一次查询直达。也可以通过 Pod 的 dnsConfig 自定义 ndots 值:
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:latest
dnsConfig:
options:
- name: ndots
value: "2"让应用对 DNS 延迟更有韧性
即便基础设施已经优化,传播延迟也不可能降到绝对的零,应用侧必须具备重试能力。以 Go 为例,默认的解析失败不会重试,建议在初始化逻辑里加入带退避的重试,或者直接使用 grpc 的内置重试与 backoff 机制。Java 应用要留意 JDK 8 及以下的 DNS 缓存默认值是 10 秒且失败不缓存,可以通过 networkaddress.cache.ttl 与安全属性调整。
对于依赖 headless Service 的场景,客户端应避免在启动瞬间依赖具体的 Pod 域名,改用 Service 域名加健康检查的方式,等解析稳定后再执行业务初始化。InitContainer 中做一次带重试的等待也是常见做法:
initContainers:
- name: wait-for-dependency
image: busybox:1.36
command:
- sh
- -c
- |
until nslookup my-db.default.svc.cluster.local; do
echo "waiting for dns record..."
sleep 2
done最后一点经验之谈:变更窗口内的失败往往被误判为应用 Bug。在做大规模扩容、节点排水或 CoreDNS 升级时,提前确认 PDB(PodDisruptionBudget)配置,保证 CoreDNS 始终有可用副本;同时避免应用把启动时的首次解析失败视为致命错误。把 DNS 当作一个可能抖动的依赖来设计,才是对付传播延迟最稳妥的姿态。
Kubernetes DNSCoreDNSDNS传播延迟修改时间:2026-09-07 21:07:09