CoreDNS 与服务发现:插件链、NDOTS 与 Headless Service

讲透 Kubernetes 集群 DNS:CoreDNS 的 Corefile 与插件链执行顺序、Service/Pod/Headless 三类 DNS 记录的命名规则、resolv.conf 中 search 域与 ndots:5 导致的解析放大问题、NodeLocal DNSCache 的部署与收益、CoreDNS 高可用与性能调优,以及 DNS 超时与 SERVFAIL 的排障方法。

在 Kubernetes 里,服务发现的第一层不是 Service 也不是 EndpointSlice,而是 DNS。Pod 里写 http://backend:8080 能通,靠的是 kubelet 注入的 resolv.conf 与 CoreDNS 的解析;而绝大多数「偶发连接超时」「DNS 解析慢 5 秒」「间歇性 SERVFAIL」的根因,也都藏在这条链路上。本文从 CoreDNS 的插件链讲起,覆盖 DNS 记录命名规则、ndots:5 引发的解析放大、NodeLocal DNSCache 的取舍,以及一套可复用的排障命令。

1. 集群 DNS 的定位与解析链路

1.1 为什么需要集群 DNS

Pod 的 IP 是动态的、随重建而变的,而 Service 的 ClusterIP 虽然稳定,但写死 IP 既不可读也不可迁移。DNS 提供了稳定名字 → 动态地址的映射:

应用配置:DATABASE_HOST=postgres.default.svc.cluster.local
                ↓ 解析
        Service ClusterIP(稳定)
                ↓ kube-proxy / eBPF 转发
        Endpoint(Pod IP,动态)

1.2 一次解析的完整路径

Pod 内进程调用 getaddrinfo()
  ↓ 读取 /etc/resolv.conf
nameserver 10.96.0.10              # kube-dns Service 的 ClusterIP
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
  ↓
请求发往 CoreDNS(Service → Endpoint)
  ↓
CoreDNS 插件链处理(kubernetes 插件查 Service/EndpointSlice)
  ↓
返回 A/AAAA 记录

1.3 glibc 与 musl 的差异

这个细节能解释一半的「DNS 诡异问题」:

libc行为影响
glibc(Debian/Ubuntu 基础镜像)支持 search 与 ndots,串行尝试符合预期
musl(Alpine 基础镜像)也读 resolv.conf,但并发查询 + 只取首个响应在高并发下行为不同

Alpine 镜像里的 musl 对 search 域处理更激进,某些版本还会忽略 ndots 的部分语义。生产环境若遇到「同一个域名在 Debian 镜像能解析、Alpine 镜像不行」,先怀疑 libc 差异,并考虑改用 debian-slim 基础镜像或显式使用 FQDN。

2. CoreDNS 架构与插件链

2.1 CoreDNS 是什么

CoreDNS 是一个插件式 DNS 服务器,每个功能都是一个插件,串成一条链(plugin chain)。Kubernetes 通过一个名为 coredns 的 Deployment 运行它,Service 名 kube-dns(历史命名),ClusterIP 通常 10.96.0.10。

kubectl get svc -n kube-system kube-dns
kubectl get deploy -n kube-system coredns
kubectl get configmap -n kube-system coredns -o yaml

2.2 Corefile 逐行解读

.:53 {
    errors                          # 把错误记录到 stdout
    health {                        # 健康检查端点 :8080/health
        lameduck 5s
    }
    ready                           # readiness 端点 :8181/ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure               # 允许解析 Pod IP 风格记录
        fallthrough in-addr.arpa ip6.arpa
        ttl 30
    }
    prometheus :9153                # 暴露指标
    forward . /etc/resolv.conf {    # 上游 DNS(节点 resolv.conf)
        max_concurrent 1000
    }
    cache 30                        # 缓存 30 秒
    loop                            # 检测转发环路,检测到就崩溃退出
    reload                          # Corefile 变更自动重载
    loadbalance                     # 对多 A 记录做轮询
}

2.3 插件链的执行顺序

CoreDNS 的插件不是按 Corefile 书写顺序执行,而是按编译时固定的顺序(plugin.cfg)。这解释了一个常见困惑:「我把 cache 写在 kubernetes 前面,为什么缓存还是生效在解析之后?」

实际执行顺序(部分):
  metadata → cancel → errors → log → health → ready
  → prometheus → cache → ... → kubernetes → ... → forward → reload

关键含义:

  • cache 在 kubernetes 之前执行,因此命中缓存时不会走 kubernetes 插件;
  • forward 在链尾,前面所有插件都 fallthrough 后才会转发给上游;
  • 想禁用某插件,只能不写它(顺序不可改)。

2.4 常用插件速查

插件作用关键配置
kubernetes解析 Service/Pod 记录pods、ttl、fallthrough
forward转发到上游 DNSmax_concurrent、policy
cache结果缓存缓存秒数、success/denial
loop检测转发环无参数,检测到即 fatal
autopath减少 search 域查询次数需配合 pods
template自定义响应用于特殊域名
hosts静态 hosts用于内网固定域名

3. DNS 记录与命名规则

3.1 Service 记录

对一个名为 backend、命名空间 prod 的 Service(ClusterIP 型):

记录类型名称解析结果
Abackend.prod.svc.cluster.localService ClusterIP
SRV_http._tcp.backend.prod.svc.cluster.local端口与目标
PTR反向记录ClusterIP → 名字

短名的补全由 search 域完成:

写 backend              → 依次尝试 backend.prod.svc.cluster.local(同命名空间)
                          backend.svc.cluster.local
                          backend.cluster.local
写 backend.prod          → backend.prod.svc.cluster.local
写 backend.prod.svc.cluster.local  → FQDN,直接查询

3.2 Pod 记录

kubernetes 插件的 pods 参数控制 Pod 记录的形态:

pods insecure      # 允许解析 <ip-dashed>.ns.pod.cluster.local(按 IP 反查)
pods verified      # 仅当 Pod IP 存在时返回(更安全,但查询更慢)
pods disabled      # 完全禁用(推荐,可省大量内存与查询)
记录格式:10-244-1-5.default.pod.cluster.local  → 10.244.1.5

建议生产设为 pods disabled:按 IP 反查 Pod 名几乎没有实际用途,却让 CoreDNS 需要缓存所有 Pod IP,大集群下内存开销显著。

3.3 Headless Service

当 clusterIP: None 时,Service 变成 Headless Service(无头服务),DNS 行为完全不同:

apiVersion: v1
kind: Service
metadata:
  name: postgres
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - port: 5432
查询ClusterIP ServiceHeadless Service
postgres.prod.svc A 记录返回 ClusterIP返回所有就绪 Pod IP
客户端行为连 VIP,由 kube-proxy 转发自己选一个 Pod(或全部)
典型用途普通微服务StatefulSet、数据库集群、gRPC 客户端负载均衡

对有 hostname + subdomain 的 Pod(StatefulSet 的标配),还会生成稳定的每 Pod 记录:

postgres-0.postgres.prod.svc.cluster.local → 10.244.1.5
postgres-1.postgres.prod.svc.cluster.local → 10.244.2.7

这是 Kubernetes StatefulSet 能实现「稳定网络标识」的基石——节点重启、Pod 重建后名字不变,对端仍能按名字找到它。

一句话:Headless Service = 把「一个 VIP」换成「一组 Pod IP」,它是 StatefulSet、数据库集群与 gRPC 客户端侧负载均衡的 DNS 基础。

4. NDOTS 与 resolv.conf 的性能陷阱

4.1 ndots:5 是什么

kubelet 注入的 resolv.conf 默认带 options ndots:5,含义是:若查询的名字中「点的数量 < 5」,先按 search 域补全尝试,再查原始名字。

查询 api.external.com(1 个点 < 5)→ 依次尝试:
  api.external.com.default.svc.cluster.local   ← 无效
  api.external.com.svc.cluster.local           ← 无效
  api.external.com.cluster.local               ← 无效
  api.external.com                            ← 终于成功

一次外部域名解析 = 4 次 DNS 查询,前 3 次都是无谓的 NXDOMAIN。这是集群 DNS 负载被放大的头号原因。

4.2 放大效应

场景实际查询数
集群内短名 backend1(命中 search 域第一条)
集群内 FQDN1
外部域名 api.github.com4(3 次 NXDOMAIN + 1 次成功)
外部域名 + 高 QPS4 × QPS,CoreDNS 压力 4 倍

4.3 优化手段

手段一:写 FQDN(末尾加点)

env:
  - name: UPSTREAM_URL
    value: "https://api.github.com."   # 末尾的 . 表示绝对域名

加了末尾点后,ndots 规则不再适用,直接查该名字,1 次查询搞定。

手段二:按 Pod 覆盖 dnsConfig

spec:
  dnsPolicy: ClusterFirst
  dnsConfig:
    options:
      - name: ndots
        value: "2"          # 只有 2 个点以上才走 search 域

手段三:启用 autopath 插件(减少 search 域查询,但需要 CoreDNS 与 kubelet 配合,且有性能代价,v1.28 后不再推荐)。

手段四:部署 NodeLocal DNSCache(见下一节)。

4.4 ndots 的权衡

降低 ndots 会带来反向代价:集群内短名解析可能失败。例如 ndots:1 时,查询 backend(0 个点 < 1)仍会走 search 域,没问题;但如果应用写 backend.prod(1 个点,不 < 1),就会直接查 backend.prod. 而失败。

经验值:ndots:2 或 ndots:3 是多数团队的安全选择——既能显著减少外部域名查询,又不至于破坏集群内短名解析。

一句话:ndots:5 是为「集群内短名」优化的默认值,代价是外部域名解析被放大 4 倍——写 FQDN 或降到 ndots:2,是性价比最高的两招。

5. NodeLocal DNSCache

5.1 要解决的问题

默认情况下,Pod 的 DNS 请求走 ClusterIP → kube-proxy → CoreDNS Pod,存在三个问题:

  1. conntrack 竞争:UDP DNS 在高并发下容易触发 conntrack 表满与丢包(经典 5 秒超时);
  2. 额外跳数:每个请求多一次 iptables/iptables 转发;
  3. CoreDNS 集中压力:所有节点都打同一批 CoreDNS Pod。

5.2 架构

NodeLocal DNSCache(nodelocaldns) 在每个节点上跑一个 DNS 缓存 Pod(DaemonSet,用 hostNetwork + 本地 IP 169.254.20.10):

Pod 的 resolv.conf:nameserver 169.254.20.10
   ↓(本地回环,无 conntrack、无 kube-proxy)
节点上的 node-cache Pod(本地缓存)
   ↓ 未命中时
CoreDNS Service(cluster.local 相关查询)
   ↓ 非 cluster.local
上游 DNS

5.3 部署与关键配置

# 官方部署脚本(会改写 kubelet 的 --cluster-dns 与 iptables 规则)
wget https://raw.githubusercontent.com/kubernetes/kubernetes/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml
# 替换 __PILLAR__DNS__SERVER__、__PILLAR__LOCAL__DNS__、__PILLAR__DNS__DOMAIN__
kubectl apply -f nodelocaldns.yaml
# node-cache 的核心配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: nodelocaldns
  namespace: kube-system
data:
  Corefile: |
    cluster.local:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . __PILLAR__CLUSTER__DNS__ {
            force_tcp
        }
        prometheus :9253
    }
    .:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . /etc/resolv.conf
        prometheus :9253
    }

5.4 收益与代价

维度收益代价
延迟本地命中,省一次网络跳首次查询仍走 CoreDNS
稳定性规避 conntrack UDP 竞争每节点多一个 Pod 的资源
CoreDNS 压力大幅下降(本地缓存吸收)缓存一致性有 TTL 窗口
复杂度部署一次基本无感升级 kubelet 时需注意 --cluster-dns

适用判断:节点数 > 20、DNS QPS > 5000、或频繁出现 DNS 5 秒超时的集群,强烈建议部署。

6. 高可用与性能调优

6.1 CoreDNS 副本与拓扑

# CoreDNS Deployment 关键片段
spec:
  replicas: 3                    # 大集群按节点数 / 10 估算
  template:
    spec:
      topologySpreadConstraints: # 按可用区打散,避免同 AZ 全挂
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              k8s-app: kube-dns
      containers:
        - name: coredns
          resources:
            requests: { cpu: 100m, memory: 70Mi }
            limits:   { memory: 170Mi }   # 注意:不要给 CPU limits

三个要点:

  1. 副本数:max(2, ceil(节点数 / 10)),且必须 ≥ 2;
  2. 不要设 CPU limits:DNS 是延迟敏感型,CPU 被 CFS 节流会导致解析超时(这是最常见的「DNS 偶发慢」原因之一);
  3. 拓扑打散:用 topologySpreadConstraints 或反亲和,避免节点故障导致 DNS 全灭。

6.2 关键指标

# CoreDNS 暴露的 Prometheus 指标
coredns_dns_requests_total{zone,type,proto}         # 请求量
coredns_dns_responses_total{rcode}                  # 响应码(SERVFAIL/NXDOMAIN)
coredns_dns_request_duration_seconds                # 延迟分布
coredns_cache_hits_total / coredns_cache_misses_total
coredns_forward_healthcheck_failures_total
告警项阈值
rcode="SERVFAIL" 比例> 0.1%
p99 延迟> 100ms
缓存命中率< 50%(说明 TTL 太短或域名太分散)
forward 健康检查失败> 0

6.3 上游与转发调优

forward . /etc/resolv.conf {
    max_concurrent 1000        # 默认 1000,高 QPS 可调大
    policy sequential          # 或 random / round_robin
    health_check 5s
}

若上游是节点本地 resolv.conf 指向的 DNS,注意多节点上游不一致会导致「部分 Pod 解析成功、部分失败」。生产建议显式指定统一的上游 DNS 服务器(如企业内网 DNS),而不是依赖 /etc/resolv.conf。

7. 排障实战

7.1 分层排查命令

# 1) DNS 服务本身是否健康
kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide
kubectl get svc -n kube-system kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50

# 2) 在 Pod 内验证解析(用带工具的临时 Pod)
kubectl run dnstest --image=registry.k8s.io/e2e-test-images/agnhost:2.45 \
  --restart=Never -- sleep 3600
kubectl exec -it dnstest -- cat /etc/resolv.conf
kubectl exec -it dnstest -- nslookup kubernetes.default.svc.cluster.local
kubectl exec -it dnstest -- nslookup backend.prod.svc.cluster.local

# 3) 直接查 CoreDNS(绕过 Service,定位是 CoreDNS 问题还是 Service/网络问题)
kubectl exec -it dnstest -- nslookup backend.prod.svc.cluster.local <coredns-pod-ip>

# 4) 抓包看请求是否到达
kubectl exec -it <coredns-pod> -n kube-system -- \
  tcpdump -i any -n port 53 -c 20

7.2 常见故障对照

症状根因处理
解析慢 5 秒(超时后成功)UDP 丢包 + conntrack 竞争部署 nodelocaldns、CoreDNS 加副本
间歇 SERVFAILCoreDNS 副本不足 / CPU 被节流去 CPU limits、扩副本
外部域名慢ndots:5 放大写 FQDN 或 ndots:2
短名解析失败search 域不对 / 跨命名空间用 FQDN 或补全命名空间
全集群 DNS 挂CoreDNS 全副本单点拓扑打散 + PDB
特定节点解析失败该节点 kube-proxy/CNI 异常查节点网络,参考 CNI 专题
转发环路CoreDNS 与节点 DNS 互相指loop 插件会 fatal,检查上游

7.3 与周边组件的协同

DNS 与多个子系统耦合:Service Mesh 的数据面常接管 DNS(ServiceEntry + DNS 代理),相关取舍见 服务网格 ;而拓扑感知路由让客户端优先解析到同 AZ 的 Endpoint,见 拓扑感知路由 。理解这三者的叠加关系,才能解释「为什么加了 Mesh 之后 DNS 行为变了」。

8. 小结

主题关键结论
插件链按编译期固定顺序执行,Corefile 顺序不影响执行
记录ClusterIP 返回 VIP,Headless 返回全部 Pod IP
Pod 记录建议 pods disabled,省内存省查询
ndots默认 5 会放大外部域名查询 4 倍,降 2 或写 FQDN
NodeLocal节点本地缓存,规避 conntrack 竞争,大集群必上
调优不要给 CoreDNS 设 CPU limits,副本 ≥ 2 且拓扑打散
排障分层验证:resolv.conf → Pod 内解析 → 直连 CoreDNS → 抓包

一句话记住:集群 DNS 是「隐形的第一跳」——它不出问题时没人注意,一出问题就是「全站不可用」。把 ndots 降下来、把 CoreDNS 的 CPU limits 去掉、把 NodeLocal DNSCache 部署上,这三件事能消灭绝大多数「偶发解析超时」;而 SERVFAIL、5 秒超时、短名失败这三类症状,各自对应完全不同的根因,按分层命令逐跳验证比猜测快得多。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「云原生」更多文章

  1. 证书管理与自动轮换:cert-manager 与 kubelet 证书轮换
  2. Sidecar 模式与原生边车容器:init 容器、生命周期与重启策略
  3. etcd 运维与性能调优:备份恢复、碎片整理与配额