土法炼钢兴趣小组的算法知识备份

CoreDNS 深度拆解:ndots、search domain 与那些坑人的 DNS 行为

目录

你的微服务调用外部 API,平均延迟 200ms。你优化了连接池、调了超时参数、换了更快的序列化库,延迟纹丝不动。最后你抓了个包,发现每次 HTTP 请求之前,都有四条 DNS 查询在排队——其中三条注定失败,只有最后一条能拿到结果。

罪魁祸首:ndots:5

这个不起眼的 /etc/resolv.conf 配置项,是 Kubernetes 集群里最常见的性能暗坑之一。它把一条简单的外部域名解析,膨胀成了四到五次 DNS 查询。而绝大多数开发者甚至不知道这个配置的存在。

本文拆解 K8s 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 中设置了 hostnamesubdomain,并且存在对应的 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: nginx
my-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

关键认知:cachekubernetes 之前执行。这意味着集群内 DNS 查询也会被缓存,重复查询不会每次都走 API Server。

kubernetes 插件的实现原理

kubernetes 插件是 CoreDNS 与 K8s API Server 的桥梁。它的工作方式:

  1. 启动时建立 Watch:通过 K8s Informer 机制,Watch Service 和 Endpoints(或 EndpointSlice)资源
  2. 构建内存索引:把 Service/Endpoints 数据构建成按域名索引的内存数据结构
  3. 查询时直接查内存:收到 DNS 查询时不再调用 API Server,直接查内存索引
  4. 增量更新: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.arpaip6.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)
DNS 查询流程:search domain 展开与 ndots:5 的查询放大

四次查询,三次浪费。 而且因为 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.com

tcpdump 的输出(简化):

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.yaml

NodeLocal 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
}

它的工作原理:

  1. 通过 DaemonSet 在每个节点上运行一个 DNS 缓存进程
  2. 监听 169.254.20.10(link-local 地址)的 53 端口
  3. kubelet 修改 Pod 的 /etc/resolv.conf,把 nameserver 指向 169.254.20.10
  4. 缓存命中时直接返回,不经过 CNI 网络
  5. 缓存未命中时,通过 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):

  1. Pod 发送第一个查询 api.github.com.default.svc.cluster.local.
  2. CoreDNS 的 autopath 插件识别出这是 search domain 展开的第一步
  3. autopath 在服务端依次尝试所有 search domain,发现都是 NXDOMAIN
  4. 最终解析 api.github.com. 拿到结果
  5. 把结果打包成一个 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 的局限性

  1. 需要 pods verifiedpods insecure:autopath 需要根据源 IP 查找 Pod 的 namespace,这要求 kubernetes 插件能解析 Pod IP。pods verified 会增加 API Server 查询,pods insecure 不验证但性能更好。

  2. CNAME 链可能与某些客户端不兼容:少数 DNS 客户端不能正确处理 CNAME 链。在实际生产中这种情况很少见,但如果遇到奇怪的解析失败,需要排查这一点。

  3. 不能与 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-app

ExternalDNS 会自动创建 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=5

cluster-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          递归解析后返回

关键要点:

  1. ndots:5 是性能杀手:外部域名每次解析都会触发 4-5 倍查询放大。在高频调用外部 API 的场景下,DNS 开销可能占请求延迟的 30% 以上。

  2. 优化方案有层次

    • 最简单:dnsConfig 中降低 ndots 到 2,配合应用代码中使用 FQDN
    • 中等:部署 NodeLocal DNSCache,利用本地缓存减少延迟和 CoreDNS 负载
    • 最彻底:启用 autopath 插件,从服务端消除查询放大
  3. conntrack 竞态是真实存在的 bug:如果看到间歇性 5 秒 DNS 超时,第一反应应该是检查 single-request-reopen 配置或启用 NodeLocal DNSCache。

  4. 排查路径是固定的:nslookup → dig → CoreDNS log → tcpdump → Prometheus 指标。按这个顺序走,大多数 DNS 问题都能定位。

  5. 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"

参考资料


By .