你的微服务调用外部 API,平均延迟 200ms。你优化了连接池、调了超时参数、换了更快的序列化库,延迟纹丝不动。最后你抓了个包,发现每次 HTTP 请求之前,都有四条 DNS 查询在排队——其中三条注定失败,只有最后一条能拿到结果。
罪魁祸首:ndots:5。
这个不起眼的 /etc/resolv.conf 配置项,是
Kubernetes
集群里最常见的性能暗坑之一。它把一条简单的外部域名解析,膨胀成了四到五次
DNS 查询。而绝大多数开发者甚至不知道这个配置的存在。
本文拆解 K8s DNS 的完整链路:
- CoreDNS 的插件链架构与 kubernetes 插件的实现原理
/etc/resolv.conf中ndots、search domain、single-request-reopen的真正含义- 用 tcpdump 实测 ndots:5 的查询放大效应
- NodeLocal DNSCache 与 autopath 插件的优化方案
- 从 nslookup 到 tcpdump 的 DNS 排查全套路
实验环境:Kubernetes 1.29,CoreDNS 1.11.1,Ubuntu 22.04 节点。 CNI:Calico,集群域名后缀:cluster.local。
一、K8s DNS 规范:每个资源的 DNS 记录长什么样
在排查 DNS 问题之前,你得先知道 K8s 到底注册了哪些 DNS 记录。这不是 CoreDNS 的发明,而是 Kubernetes 的 DNS 规范(DNS-Based Service Discovery)定义的。
ClusterIP Service
一个普通的 ClusterIP Service 会产生两条记录:
# A 记录:Service 名称 → ClusterIP
my-svc.default.svc.cluster.local. IN A 10.96.100.23
# SRV 记录:端口发现
_http._tcp.my-svc.default.svc.cluster.local. IN SRV 0 100 80 my-svc.default.svc.cluster.local.
完整的 FQDN 格式是
<service>.<namespace>.svc.<cluster-domain>.,其中
cluster-domain 默认是
cluster.local。
Headless Service
Headless
Service(clusterIP: None)的行为完全不同。它不分配
ClusterIP,A 记录直接指向后端 Pod 的 IP:
# A 记录:返回所有就绪 Pod 的 IP(多条)
my-headless.default.svc.cluster.local. IN A 10.244.1.5
my-headless.default.svc.cluster.local. IN A 10.244.2.8
my-headless.default.svc.cluster.local. IN A 10.244.3.12
# 每个 Pod 还有独立的 A 记录
10-244-1-5.my-headless.default.svc.cluster.local. IN A 10.244.1.5
StatefulSet 配合 Headless Service 时,Pod 的 DNS 名称是稳定的:
# StatefulSet Pod 的 A 记录
web-0.my-headless.default.svc.cluster.local. IN A 10.244.1.5
web-1.my-headless.default.svc.cluster.local. IN A 10.244.2.8
这就是为什么数据库集群(MySQL、Redis Cluster)必须用 Headless Service——客户端需要知道每个实例的独立地址。
Pod DNS
Pod 本身也有 DNS 记录,但格式比较特殊——用破折号替代 IP 中的点:
# Pod A 记录(基于 IP 生成)
10-244-1-5.default.pod.cluster.local. IN A 10.244.1.5
如果在 Pod spec 中设置了 hostname 和
subdomain,并且存在对应的 Headless Service,Pod
会获得更友好的 DNS 名称:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
hostname: my-host
subdomain: my-headless # 必须与 Headless Service 同名
containers:
- name: app
image: nginxmy-host.my-headless.default.svc.cluster.local. IN A 10.244.1.5
ExternalName Service
ExternalName Service 返回 CNAME 而非 A 记录:
ext-svc.default.svc.cluster.local. IN CNAME api.external-provider.com.
CoreDNS 不会递归解析这个 CNAME——直接返回给客户端,由客户端的 resolver 继续解析。
二、CoreDNS 架构:Corefile 与插件链
CoreDNS 是 K8s 从 1.13 开始的默认集群 DNS。核心设计思想是一切皆插件:每个功能(缓存、日志、健康检查、Kubernetes 集成)都是独立的插件,通过 Corefile 配置串联成处理链。
Corefile 解析
查看集群中的 CoreDNS 配置:
kubectl -n kube-system get configmap coredns -o yaml一个典型的 Corefile:
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
插件的执行顺序不由 Corefile
中的书写顺序决定,而是由 CoreDNS
源码中的硬编码顺序决定。这个顺序定义在
plugin.cfg 文件中:
# 插件执行顺序(简化)
cache # 先查缓存
autopath # search domain 服务端展开
kubernetes # 处理 cluster.local 域
forward # 非集群域名转发到上游
loop
关键认知:cache 在 kubernetes
之前执行。这意味着集群内 DNS
查询也会被缓存,重复查询不会每次都走 API Server。
kubernetes 插件的实现原理
kubernetes 插件是 CoreDNS 与 K8s API Server 的桥梁。它的工作方式:
- 启动时建立 Watch:通过 K8s Informer 机制,Watch Service 和 Endpoints(或 EndpointSlice)资源
- 构建内存索引:把 Service/Endpoints 数据构建成按域名索引的内存数据结构
- 查询时直接查内存:收到 DNS 查询时不再调用 API Server,直接查内存索引
- 增量更新:Watch 事件触发索引的增量更新
API Server CoreDNS kubernetes 插件
| |
|--- Watch Services ---------->| 建立 Watch 连接
|--- Watch EndpointSlices ---->|
| |
| (Service 创建/更新/删除) |
|--- Event: ADDED ------------>| 更新内存索引
|--- Event: MODIFIED --------->| 更新内存索引
|--- Event: DELETED ---------->| 从索引中删除
| |
| DNS Query: my-svc.default.svc.cluster.local
| |---> 查内存索引,直接返回 A 记录
这个设计的优点是查询延迟极低(微秒级内存查找),缺点是 CoreDNS 需要消耗内存存储整个集群的 Service/Endpoints 数据。在大规模集群中(万级 Service),CoreDNS 的内存占用可以达到数百 MB。
pods insecure 选项允许基于 Pod IP 反向构造 A
记录(把 IP 中的点替换成破折号),但不验证 Pod
是否存在。pods verified 会验证,但增加 API
Server 负载。
fallthrough 机制
fallthrough 指定了哪些查询类型在 kubernetes
插件找不到记录时,应该继续传递给下一个插件(而不是直接返回
NXDOMAIN):
kubernetes cluster.local in-addr.arpa ip6.arpa {
fallthrough in-addr.arpa ip6.arpa
}
上面的配置表示:对于 cluster.local 域,如果
kubernetes 插件没有找到记录,直接返回
NXDOMAIN。但对于反向解析(in-addr.arpa、ip6.arpa),如果没找到,继续传递给
forward 插件——因为反向解析可能需要上游 DNS
来处理。
三、/etc/resolv.conf 的秘密
现在进入本文的核心:Pod 里那个不起眼的
/etc/resolv.conf。
kubelet 如何生成 resolv.conf
在 Pod 中执行 cat /etc/resolv.conf:
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
这个文件不是镜像自带的,而是 kubelet 在创建 Pod
时注入的。生成逻辑在 kubelet 源码
pkg/kubelet/network/dns/dns.go 中,kubelet 根据
Pod 的 dnsPolicy 决定生成什么内容:
| dnsPolicy | nameserver | search domain |
|---|---|---|
| ClusterFirst(默认) | kube-dns ClusterIP | <ns>.svc.cluster.local svc.cluster.local cluster.local |
| Default | 继承节点的 resolv.conf | 继承节点的 search domain |
| None | Pod spec 中自定义 | Pod spec 中自定义 |
| ClusterFirstWithHostNet | kube-dns ClusterIP | 同 ClusterFirst |
ndots:5 为什么是默认值
ndots
的含义:如果一个域名中包含的点的数量小于
ndots 值,glibc resolver
会先尝试把它当作相对名称,依次追加 search
domain 来查询。只有当所有 search domain 都返回 NXDOMAIN
之后,才会把原始名称作为 FQDN 来查询。
为什么 K8s 选了 5?因为集群内最长的 DNS 名称包含 4 个点:
my-svc.my-namespace.svc.cluster.local
^ ^ ^ ^
1 2 3 4 (4 个点)
SRV 记录更长:
_http._tcp.my-svc.my-namespace.svc.cluster.local
^ ^ ^ ^ ^ ^
1 2 3 4 5 6 (实际上 SRV 有下划线前缀)
ndots:5 确保集群内的任何 DNS 名称都会走
search domain
展开路径,从而正确解析。但代价是——所有外部域名也走这条路径。
search domain 展开顺序的性能陷阱
当 Pod 里的应用查询 api.github.com 时(2
个点,小于 ndots:5),glibc 会按顺序尝试:
1. api.github.com.default.svc.cluster.local. → NXDOMAIN (CoreDNS)
2. api.github.com.svc.cluster.local. → NXDOMAIN (CoreDNS)
3. api.github.com.cluster.local. → NXDOMAIN (CoreDNS)
4. api.github.com. → SUCCESS (上游 DNS)
四次查询,三次浪费。 而且因为 A 和 AAAA 是并行发的,实际上是 八个 DNS 包。如果上游 DNS 延迟 50ms,你的应用在每次新连接时都要额外等待 150ms 以上。
更糟糕的是,即使目标是四个点的域名(比如
api.us-west-2.amazonaws.com),只要点数小于
5,同样会触发展开。在 AWS/GCP
环境中,内部服务的域名经常有三四个点,全都中招。
为什么不直接改成 ndots:1
把 ndots 改小可以减少外部域名的查询次数,但会破坏集群内的短名称解析:
# ndots:1 时,查询 "my-svc"(0 个点,< 1)仍然会展开 search domain
# 但查询 "my-svc.my-namespace"(1 个点,>= 1)会直接作为 FQDN 查询
# 结果:NXDOMAIN,因为 "my-svc.my-namespace" 不是合法的公网域名
所以 ndots 的调整必须谨慎。如果你的应用只用
Service 短名称(my-svc)或 FQDN(末尾加点
my-svc.default.svc.cluster.local.),可以安全地把
ndots 降到 2。
options single-request-reopen 是在修什么 bug
在某些 Linux 内核版本中,glibc 会在同一个 UDP socket 上并行发送 A 和 AAAA 查询。当 conntrack 模块处理这两个查询时,可能因为竞态条件(race condition)导致其中一个查询的 conntrack entry 被错误地丢弃,表现为 DNS 查询超时(通常 5 秒)。
# 竞态条件:两个 UDP 包几乎同时发送,使用相同源端口
Pod → conntrack → CoreDNS
|--- A query (src port 12345) --→ conntrack 创建 entry
|--- AAAA query (src port 12345) --→ 与已有 entry 冲突,包被丢弃 → 5 秒超时
single-request-reopen 让 glibc 为 A 和 AAAA
查询使用不同的 socket(不同的源端口),从而避免 conntrack
冲突。另一个相关选项是 use-vc,它强制使用
TCP,完全绕过 conntrack 的 UDP 竞态问题,但开销更大:
apiVersion: v1
kind: Pod
spec:
dnsConfig:
options:
- name: single-request-reopen四、实验:tcpdump 抓取 ndots:5 的查询放大效应
说了这么多,来实际抓包验证。
实验环境搭建
kubectl run dns-debug --image=nicolaka/netshoot --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/dns-debug --timeout=60s抓取 DNS 查询
在节点上用 tcpdump 抓取 DNS 流量,然后在 Pod 中触发查询:
# 节点上:抓取 53 端口的 UDP 流量
tcpdump -i any -nn port 53 -l 2>/dev/null | grep -i "api.github.com"
# Pod 中:触发 DNS 查询
kubectl exec dns-debug -- nslookup api.github.comtcpdump 的输出(简化):
10:23:01.001 10.244.1.5.43210 > 10.96.0.10.53: A? api.github.com.default.svc.cluster.local.
10:23:01.001 10.244.1.5.43210 > 10.96.0.10.53: AAAA? api.github.com.default.svc.cluster.local.
10:23:01.002 10.96.0.10.53 > 10.244.1.5.43210: NXDomain 0/1/0
10:23:01.002 10.96.0.10.53 > 10.244.1.5.43210: NXDomain 0/1/0
10:23:01.003 10.244.1.5.43211 > 10.96.0.10.53: A? api.github.com.svc.cluster.local.
10:23:01.003 10.244.1.5.43211 > 10.96.0.10.53: AAAA? api.github.com.svc.cluster.local.
10:23:01.004 10.96.0.10.53 > 10.244.1.5.43211: NXDomain 0/1/0
10:23:01.004 10.96.0.10.53 > 10.244.1.5.43211: NXDomain 0/1/0
10:23:01.005 10.244.1.5.43212 > 10.96.0.10.53: A? api.github.com.cluster.local.
10:23:01.005 10.244.1.5.43212 > 10.96.0.10.53: AAAA? api.github.com.cluster.local.
10:23:01.006 10.96.0.10.53 > 10.244.1.5.43212: NXDomain 0/1/0
10:23:01.006 10.96.0.10.53 > 10.244.1.5.43212: NXDomain 0/1/0
10:23:01.007 10.244.1.5.43213 > 10.96.0.10.53: A? api.github.com.
10:23:01.007 10.244.1.5.43213 > 10.96.0.10.53: AAAA? api.github.com.
10:23:01.058 10.96.0.10.53 > 10.244.1.5.43213: 1/0/0 A 140.82.121.6
10:23:01.060 10.96.0.10.53 > 10.244.1.5.43213: 0/1/0
八个 DNS 包(4 组 A+AAAA 查询),只有最后一组拿到了结果。 前三组全部浪费。
对比:末尾加点的 FQDN 查询
kubectl exec dns-debug -- nslookup api.github.com.10:25:01.001 10.244.1.5.43220 > 10.96.0.10.53: A? api.github.com.
10:25:01.001 10.244.1.5.43220 > 10.96.0.10.53: AAAA? api.github.com.
10:25:01.052 10.96.0.10.53 > 10.244.1.5.43220: 1/0/0 A 140.82.121.6
10:25:01.054 10.96.0.10.53 > 10.244.1.5.43220: 0/1/0
只有两个包。 末尾加点告诉 glibc:这是绝对域名,不要展开 search domain。
用 dig 精确测量
# 查看完整的查询过程
kubectl exec dns-debug -- dig +search +showsearch api.github.com
# 对比 FQDN 查询
kubectl exec dns-debug -- dig api.github.com.
# 查看 Pod 的 resolv.conf
kubectl exec dns-debug -- cat /etc/resolv.conf五、DNS 缓存层:CoreDNS cache 与 NodeLocal DNSCache
查询放大的问题如果不能从根源消除,至少可以通过缓存来缓解。
CoreDNS cache 插件
Corefile 中的 cache 30 表示缓存成功响应 30
秒。更精细的配置:
cache {
success 9984 30 # 缓存成功响应,最多 9984 条,TTL 30 秒
denial 9984 5 # 缓存 NXDOMAIN/NODATA,最多 9984 条,TTL 5 秒
prefetch 10 60s 10% # 当缓存条目被访问 10 次以上时,在过期前 60 秒内提前刷新
}
denial 缓存对 ndots:5
的查询放大特别重要——那些 NXDOMAIN
响应会被缓存,第二次查询同一个外部域名时,前三次 search
domain 展开可以直接从缓存返回,不需要再查 API Server 或上游
DNS。
但问题是:所有 Pod 的 DNS 查询都要经过 CoreDNS Pod,网络路径是 Pod → CNI 网络 → CoreDNS Pod。在大规模集群中,CoreDNS 可能成为瓶颈。
NodeLocal DNSCache
NodeLocal DNSCache 在每个节点上运行一个 DNS 缓存 DaemonSet,把缓存层推到节点本地:
Pod → 本地缓存(DaemonSet)→ CoreDNS → 上游 DNS
169.254.20.10 10.96.0.10
安装方式:
# 使用官方 manifest
kubectl apply -f https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yamlNodeLocal DNSCache 的 Corefile:
cluster.local:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind 169.254.20.10
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
.:53 {
errors
cache 30
reload
loop
bind 169.254.20.10
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
它的工作原理:
- 通过 DaemonSet 在每个节点上运行一个 DNS 缓存进程
- 监听
169.254.20.10(link-local 地址)的 53 端口 - kubelet 修改 Pod 的
/etc/resolv.conf,把 nameserver 指向169.254.20.10 - 缓存命中时直接返回,不经过 CNI 网络
- 缓存未命中时,通过 TCP 转发给 CoreDNS(TCP 避免了 conntrack 问题)
性能收益:
| 指标 | 无 NodeLocal DNSCache | 有 NodeLocal DNSCache |
|---|---|---|
| DNS 查询延迟(缓存命中) | 1-5ms(经过 CNI 网络) | <0.5ms(本地 loopback) |
| CoreDNS 负载 | 所有查询 | 仅缓存未命中 |
| conntrack 竞态 | 存在风险 | TCP 转发,无风险 |
| 节点故障影响 | CoreDNS Pod 所在节点故障影响所有依赖者 | 仅影响本节点 |
注意:NodeLocal DNSCache 使用 force_tcp 连接
CoreDNS,避免了 conntrack 的 UDP
竞态问题,但在极大规模集群中需要关注 CoreDNS 的 TCP
连接数限制。
六、autopath 插件:把 ndots:5 的性能问题降到 ndots:2 水平
NodeLocal DNSCache 通过缓存减少了重复查询的开销,但第一次查询外部域名时,search domain 展开仍然要走四轮。autopath 插件从根本上解决这个问题。
autopath 的工作原理
autopath 把 search domain 展开从客户端(glibc)搬到了服务端(CoreDNS):
- Pod 发送第一个查询
api.github.com.default.svc.cluster.local. - CoreDNS 的 autopath 插件识别出这是 search domain 展开的第一步
- autopath 在服务端依次尝试所有 search domain,发现都是 NXDOMAIN
- 最终解析
api.github.com.拿到结果 - 把结果打包成一个 CNAME 链返回给客户端
# 客户端发送 1 个查询
Query: api.github.com.default.svc.cluster.local. A
# CoreDNS 返回 CNAME 链 + 最终结果
Answer:
api.github.com.default.svc.cluster.local. CNAME api.github.com.svc.cluster.local.
api.github.com.svc.cluster.local. CNAME api.github.com.cluster.local.
api.github.com.cluster.local. CNAME api.github.com.
api.github.com. A 140.82.121.6
客户端的 glibc resolver 收到第一个查询的响应后,发现最终的 A 记录已经在里面了,就不会再发送后续的 search domain 查询。四轮网络往返变成了一轮。
配置 autopath
.:53 {
errors
health
ready
autopath @kubernetes # 启用 autopath,使用 kubernetes 插件获取 Pod 的 search domain
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods verified # autopath 需要 pods verified 或 pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
@kubernetes 告诉 autopath 从 kubernetes
插件获取发起查询的 Pod 的 namespace 信息,从而知道该 Pod 的
search domain 列表。
autopath 的局限性
需要
pods verified或pods insecure:autopath 需要根据源 IP 查找 Pod 的 namespace,这要求 kubernetes 插件能解析 Pod IP。pods verified会增加 API Server 查询,pods insecure不验证但性能更好。CNAME 链可能与某些客户端不兼容:少数 DNS 客户端不能正确处理 CNAME 链。在实际生产中这种情况很少见,但如果遇到奇怪的解析失败,需要排查这一点。
不能与 NodeLocal DNSCache 直接配合:NodeLocal DNSCache 收到查询时,源 IP 是 Pod IP。但当它转发给 CoreDNS 时,源 IP 变成了 NodeLocal DNSCache 的 IP。autopath 拿到的是 NodeLocal DNSCache 的 IP,无法确定原始 Pod 的 namespace。需要额外配置来解决。
autopath 与 ndots 调整的对比
| 方案 | 外部域名查询次数 | 集群内短名称 | 配置复杂度 | 客户端兼容性 |
|---|---|---|---|---|
| 默认 ndots:5 | 4-5 次 | 正常 | 无需配置 | 最好 |
| ndots:2 | 1-2 次 | svc.ns 形式需要改写 |
Pod 级别配置 | 最好 |
| 末尾加点 | 1 次 | 需要全部改写 | 应用代码修改 | 最好 |
| autopath | 1 次(服务端展开) | 正常 | CoreDNS 配置 | 极少数客户端不兼容 |
七、ExternalDNS:Service 到公网 DNS 记录的自动同步
前面讨论的都是集群内 DNS。如果你需要把 K8s Service 或 Ingress 暴露到公网 DNS(比如 Route53、CloudFlare),手动维护 DNS 记录是不可接受的。ExternalDNS 解决这个问题。
工作原理
ExternalDNS 以 Pod 形式运行在集群中,Watch K8s 资源(Service、Ingress、Gateway),当检测到资源变化时,自动在外部 DNS 提供商(Route53、CloudFlare 等)创建、更新或删除对应的 DNS 记录。
部署示例
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: kube-system
spec:
replicas: 1
selector:
matchLabels:
app: external-dns
template:
spec:
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.14.0
args:
- --source=service
- --source=ingress
- --domain-filter=example.com
- --provider=aws
- --policy=upsert-only # 只创建/更新,不删除
- --registry=txt
- --txt-owner-id=my-cluster使用时,只需要在 Service 或 Ingress 上添加 annotation:
apiVersion: v1
kind: Service
metadata:
name: my-app
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
type: LoadBalancer
ports:
- port: 80
selector:
app: my-appExternalDNS 会自动创建 app.example.com 的 A
记录,指向 LoadBalancer 的外部 IP。
所有权与安全
ExternalDNS 使用 TXT 记录标记自己创建的 DNS
记录,防止多个集群之间的冲突。--policy=upsert-only
是生产环境的推荐配置——ExternalDNS
只创建和更新记录,不会删除,避免误删导致的生产事故。
八、DNS 排查套路
DNS 问题的排查有一套固定的升级路径。从最简单的工具开始,逐步深入。
第一步:nslookup / dig 基本验证
# 从 Pod 内部测试
kubectl exec -it dns-debug -- nslookup kubernetes.default
kubectl exec -it dns-debug -- nslookup my-svc.my-namespace
# 用 dig 获取更详细的信息
kubectl exec -it dns-debug -- dig kubernetes.default.svc.cluster.local A +short
kubectl exec -it dns-debug -- dig @10.96.0.10 my-svc.default.svc.cluster.local A
# 测试外部域名
kubectl exec -it dns-debug -- dig api.github.com +search +showsearch常见输出分析:
# 正常
;; ANSWER SECTION:
kubernetes.default.svc.cluster.local. 30 IN A 10.96.0.1
# NXDOMAIN:Service 不存在或 namespace 错误
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN
# SERVFAIL:CoreDNS 内部错误
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL
# 超时:网络不通或 CoreDNS 没有运行
;; connection timed out; no servers could be reached
第二步:检查 CoreDNS 状态
# CoreDNS Pod 是否正常运行
kubectl -n kube-system get pods -l k8s-app=kube-dns
# 查看 CoreDNS 日志
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=100
# 检查 CoreDNS Service 的 Endpoints
kubectl -n kube-system get endpoints kube-dns如果需要更详细的日志,在 Corefile 中添加 log
插件:
.:53 {
log # 记录所有 DNS 查询
errors
# ...
}
修改后 CoreDNS 会自动重新加载(默认 30 秒),或手动重启:
kubectl -n kube-system rollout restart deployment coredns第三步:dig +trace 追踪解析链路
# 从 CoreDNS 开始追踪
kubectl exec dns-debug -- dig +trace api.github.com
# 直接查上游 DNS(跳过 CoreDNS)
kubectl exec dns-debug -- dig @8.8.8.8 api.github.com如果直接查上游 DNS 成功但通过 CoreDNS 失败,问题出在
CoreDNS 的 forward 配置。
第四步:tcpdump 抓包
# 在 CoreDNS Pod 所在节点上抓包
tcpdump -i any -nn port 53 -w dns-capture.pcap
# 只看来自特定 Pod 的查询
tcpdump -i any -nn port 53 and host 10.244.1.5
# 实时解析显示
tcpdump -i any -nn port 53 -l 2>/dev/null第五步:检查 CoreDNS 指标
CoreDNS 暴露 Prometheus 指标在
:9153/metrics:
kubectl -n kube-system port-forward svc/kube-dns 9153:9153
curl -s localhost:9153/metrics | grep -E "coredns_(dns_request|cache|forward|panics)"关注这几个指标:coredns_dns_requests_total
突然暴涨可能是查询放大;coredns_dns_responses_total{rcode="NXDOMAIN"}
大量出现是 ndots
展开的特征;coredns_cache_hits_total
命中率过低需要调整缓存;coredns_panics_total
任何非零值都需要立即排查。
常见问题速查表
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| DNS 超时 5 秒 | conntrack 竞态 | 添加 single-request-reopen 或使用 NodeLocal
DNSCache |
| 间歇性 SERVFAIL | CoreDNS 无法连接上游 DNS | 检查节点的 /etc/resolv.conf 和上游 DNS
可达性 |
| 新创建的 Service 解析失败 | CoreDNS 的 Watch 延迟 | 等待几秒后重试,检查 CoreDNS 与 API Server 的连接 |
| 外部域名解析慢 | ndots:5 查询放大 | tcpdump 验证,考虑 autopath 或调整 ndots |
| Pod 重启后 DNS 短暂失败 | DNS 缓存中的旧记录 | 检查 TTL 设置,确保缓存过期时间合理 |
九、高级话题:dnsPolicy 与集群扩展
dnsPolicy 的完整语义
dnsPolicy: None + dnsConfig
给了你最大的灵活性:
apiVersion: v1
kind: Pod
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- 10.96.0.10
- 8.8.8.8
searches:
- default.svc.cluster.local
options:
- name: ndots
value: "2"
- name: single-request-reopen
- name: timeout
value: "2"跨 namespace 访问 Service 需要带
namespace:my-svc.other-namespace。在配置文件中,建议使用
FQDN(带末尾点
my-svc.other-namespace.svc.cluster.local.),这样无论
ndots 配置如何变化,解析行为都是确定的。
CoreDNS 水平扩展
默认的 CoreDNS Deployment 通常只有 2
个副本。在大规模集群中可以手动扩展或使用
cluster-proportional-autoscaler
根据节点数自动调整:
# 手动扩展
kubectl -n kube-system scale deployment coredns --replicas=5cluster-proportional-autoscaler
默认公式大约是每 16 个节点对应 1 个 CoreDNS
副本,可以自定义。
十、总结:一张图回顾 K8s DNS 全链路
从 Pod 发起一次 DNS 查询到拿到结果,完整链路是:
应用代码 getaddrinfo()
↓
glibc resolver(读 /etc/resolv.conf)
↓
ndots 判断:点数 < ndots → search domain 展开
↓
依次尝试各 search domain(A + AAAA 并行)
↓
NodeLocal DNSCache(169.254.20.10)
↓ cache miss
CoreDNS(10.96.0.10)
↓
插件链:cache → autopath → kubernetes → forward
↓
cluster.local → 查内存索引 外部域名 → forward 到上游 DNS
↓ ↓
返回 A/AAAA/SRV/CNAME 递归解析后返回
关键要点:
ndots:5 是性能杀手:外部域名每次解析都会触发 4-5 倍查询放大。在高频调用外部 API 的场景下,DNS 开销可能占请求延迟的 30% 以上。
优化方案有层次:
- 最简单:
dnsConfig中降低 ndots 到 2,配合应用代码中使用 FQDN - 中等:部署 NodeLocal DNSCache,利用本地缓存减少延迟和 CoreDNS 负载
- 最彻底:启用 autopath 插件,从服务端消除查询放大
- 最简单:
conntrack 竞态是真实存在的 bug:如果看到间歇性 5 秒 DNS 超时,第一反应应该是检查
single-request-reopen配置或启用 NodeLocal DNSCache。排查路径是固定的:nslookup → dig → CoreDNS log → tcpdump → Prometheus 指标。按这个顺序走,大多数 DNS 问题都能定位。
ExternalDNS 闭环了内外 DNS:集群内靠 CoreDNS 自动注册,集群外靠 ExternalDNS 自动同步到公网 DNS 提供商。Service 创建后,内外 DNS 记录都自动就位。
附录 A:DNS 排查命令速查
# --- 基本验证 ---
kubectl exec <pod> -- cat /etc/resolv.conf
kubectl exec <pod> -- nslookup kubernetes.default
kubectl exec <pod> -- dig +short kubernetes.default.svc.cluster.local
kubectl exec <pod> -- dig +search +showsearch api.github.com
# --- CoreDNS 状态 ---
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50
kubectl -n kube-system get configmap coredns -o jsonpath='{.data.Corefile}'
# --- 深入排查 ---
tcpdump -i any -nn port 53 -l 2>/dev/null
kubectl exec <pod> -- dig +trace example.com
kubectl exec <pod> -- dig @8.8.8.8 example.com # 绕过 CoreDNS
# --- 性能分析 ---
kubectl -n kube-system port-forward svc/kube-dns 9153:9153
curl -s localhost:9153/metrics | grep -E "coredns_(dns_request|cache|forward)"附录 B:推荐的 Corefile 配置模板
# 生产环境推荐配置
.:53 {
errors
health {
lameduck 5s
}
ready
# 启用 autopath 减少查询放大
autopath @kubernetes
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
policy sequential
}
# 精细缓存配置
cache {
success 9984 30
denial 9984 5
prefetch 10 60s 10%
}
loop
reload 10s
loadbalance round_robin
}
附录 C:dnsConfig 优化模板
# 主要调用外部 API 的工作负载
apiVersion: v1
kind: Pod
spec:
dnsPolicy: ClusterFirst
dnsConfig:
options:
- name: ndots
value: "2"
- name: single-request-reopen# 完全不需要集群 DNS 的工作负载(如批处理任务)
apiVersion: v1
kind: Pod
spec:
dnsPolicy: "None"
dnsConfig:
nameservers:
- 8.8.8.8
- 8.8.4.4
options:
- name: ndots
value: "1"参考资料
- Kubernetes DNS Specification
- CoreDNS Manual: Plugins
- CoreDNS autopath Plugin
- NodeLocal DNSCache
- ExternalDNS GitHub Repository
- RFC 1035: “Domain Names - Implementation and Specification”
- RFC 6762: “Multicast DNS”
- 5 – Why 5? Kubernetes DNS ndots Explained
- Racy conntrack and DNS Lookup Timeouts
- Kubernetes 网络模型
- Service 与 kube-proxy