Kubernetes 基础体系 · 第 28/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes DNS:CoreDNS、Service Discovery、Search、缓存和故障

Kubernetes 中的 DNS 不是一个独立于 Service 的“主机名系统”。它是 Service、EndpointSlice、Pod 网络、容器内解析器和 CoreDNS 共同组成的服务发现路径:

应用程序
  │ getaddrinfo()/DNS 查询
  ▼
容器内 /etc/resolv.conf
  │ nameserver <集群 DNS Service IP>
  ▼
CoreDNS
  ├─ 根据 Kubernetes API 中的 Service、EndpointSlice、Pod 状态生成集群域名记录
  ├─ 根据 Corefile 决定是否转发集群外域名
  └─ 使用缓存减少重复查询
  ▼
Service ClusterIP 或 Pod IP
  │
  ├─ ClusterIP:kube-proxy 或其他实现转发到后端 Endpoint
  └─ Headless Service:DNS 直接返回 Endpoint 地址

这条链路中,每层解决的问题不同:

  • Service Discovery 定义“服务使用什么稳定名称,以及名称对应哪些地址”。
  • CoreDNS 将 Kubernetes 对象状态转换为 DNS 响应。
  • Search 让应用可以使用短名称,例如 redis,但也可能导致多个额外查询。
  • 缓存 缩短响应时间、降低 API 和上游 DNS 压力,但会延迟状态变化的可见性。
  • 故障 可能发生在解析器、Pod 到 DNS Service 的网络、CoreDNS、Kubernetes API、上游 DNS 或 Service 后端,而不一定是“DNS 挂了”。

下文使用默认集群域名 cluster.local 举例。集群域名由 kubelet 的 --cluster-domain 配置决定,并不保证所有集群都是这个值。


1. Kubernetes DNS 的边界:DNS 只负责得到地址,不负责完成连接

先区分两个动作:

  1. DNS 查询:例如把 orders.default.svc.cluster.local 解析为 10.96.42.17
  2. 连接建立:应用再向 10.96.42.17:8080 发起 TCP、TLS 或 HTTP 请求。

DNS 成功不代表应用一定能连接:

  • Service 可能没有可用 Endpoint;
  • NetworkPolicy 可能阻止流量;
  • kube-proxy、eBPF Service 实现或 CNI 可能故障;
  • 后端 Pod 可能监听了错误端口;
  • 应用可能把 HTTP、TLS、协议版本配置错了。

反过来,DNS 失败也不代表后端地址完全不可达。若已经知道某个 Pod IP,直接连接它可能成功,但这绕过了 Service 的稳定地址和负载分发,不应作为正常服务发现方案。

因此,排障时应把问题拆成:

名字是否能解析?
    ↓
解析出的地址是否正确?
    ↓
到该地址的网络是否可达?
    ↓
Service 是否有后端?
    ↓
后端端口和应用协议是否正确?

2. CoreDNS 如何成为 Kubernetes 的 DNS 服务器

2.1 Pod 为什么知道 DNS 服务器地址

Pod 创建时,kubelet 会根据 Pod 的 dnsPolicy 生成容器内的 /etc/resolv.conf。一个常见结果类似:

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

这里的 10.96.0.10 通常是名为 kube-dns 的 Service 的 ClusterIP。尽管 Service 名字仍然叫 kube-dns,实际后端通常是 CoreDNS Pod;这是历史兼容命名,不表示必须运行名为 kube-dns 的组件。

nameserver 是 DNS 服务入口,不是 CoreDNS Pod 的固定 IP。CoreDNS Pod 可以重建、扩缩容或迁移节点,Service 仍提供稳定的虚拟地址。

查看本集群实际值:

kubectl -n kube-system get svc kube-dns \
  -o wide

kubectl -n kube-system get pods \
  -l k8s-app=kube-dns \
  -o wide

预期可以看到:

  • kube-dns Service 的 ClusterIP;
  • 一个或多个 CoreDNS Pod;
  • CoreDNS Pod 的 Pod IP 不一定等于 kube-dns 的 ClusterIP。

如果某个业务 Pod 的 nameserver 指向一个不存在的地址,或者该地址对应的 Service 没有可用 Endpoint,那么所有集群内名称解析都可能超时。


2.2 CoreDNS 的两个关键输入

CoreDNS 处理 Kubernetes 域名时,主要依赖两类输入:

  1. Kubernetes API 对象

    • Service;
    • EndpointSlice;
    • Pod;
    • Namespace 等。
  2. Corefile 配置

    • 哪个域名由 kubernetes 插件处理;
    • 集群外域名是否转发给上游 DNS;
    • 是否启用缓存、健康检查、指标等插件。

典型 Corefile 结构如下,实际内容由发行版和安装方式决定:

.:53 {
    errors
    health
    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
    cache 30
    loop
    reload
    loadbalance
}

这里需要区分规范和实现:

  • Kubernetes 规定了集群 DNS 名称和 Service Discovery 的语义;
  • CoreDNS 的插件链、Corefile 写法、默认 TTL 和默认转发方式属于 CoreDNS 实现及发行版配置;
  • 云厂商托管集群可能修改 CoreDNS 镜像、配置、NodeLocal DNSCache 部署方式或监控端口。

查看实际配置,不要凭默认值猜测:

kubectl -n kube-system get configmap coredns \
  -o yaml

kubectl -n kube-system get deployment coredns \
  -o yaml

CoreDNS 通常通过 informer 监听 Kubernetes API 的对象变化,在本地维护 Service、EndpointSlice 等状态。查询到达时,它优先从这份本地状态生成响应,而不是每次 DNS 查询都访问 Kubernetes API。

这带来两个重要结果:

  • 正常查询延迟不应等于一次 Kubernetes API 请求的延迟;
  • API、RBAC、网络或 CoreDNS informer 出现问题时,DNS 状态可能停止更新,即使旧记录仍能从缓存或本地状态中返回。

3. Service Discovery:不同 Service 类型产生不同 DNS 语义

3.1 普通 Service:名称解析到 ClusterIP

假设创建一个 Service:

apiVersion: v1
kind: Service
metadata:
  name: orders
  namespace: default
spec:
  selector:
    app: orders
  ports:
    - name: http
      port: 80
      targetPort: 8080

通常可以使用以下完整名称访问:

orders.default.svc.cluster.local

其 A 记录通常返回 Service 的 ClusterIP:

orders.default.svc.cluster.local.  A  10.96.42.17

如果集群启用了 IPv6 或双栈配置,也可能返回 AAAA 记录。DNS 返回的是 Service 地址,而不是某个后端 Pod 地址。

数据流是:

应用查询 orders.default.svc.cluster.local
        ↓
CoreDNS 返回 ClusterIP 10.96.42.17
        ↓
应用连接 10.96.42.17:80
        ↓
Service 转发到某个 Endpoint 的 8080 端口

这里的 port: 80 是 Service 对外提供的端口,targetPort: 8080 是后端 Pod 端口。DNS 只返回地址,不编码端口;应用仍必须使用 Service 的端口 80。

检查 Service 和 EndpointSlice:

kubectl -n default get svc orders -o wide

kubectl -n default get endpointslice \
  -l kubernetes.io/service-name=orders \
  -o yaml

EndpointSlice 是当前 Kubernetes 推荐的后端地址表示方式。旧的 Endpoints API 在大规模场景中扩展性较差,并且已被弃用;排查新集群时应优先查看 EndpointSlice。


3.2 没有后端时,Service 名称仍可能存在

Service 对象存在与否,和它是否有可用后端是两个状态。

如果 orders Service 存在,但没有匹配 app=orders 的 Pod:

  • CoreDNS 仍可能把 orders.default.svc.cluster.local 解析为 ClusterIP;
  • 连接 ClusterIP 时可能没有可转发的后端;
  • 具体表现可能是连接超时、连接拒绝或由 Service 实现返回其他网络结果,取决于 kube-proxy、CNI 和规则状态。

因此:

kubectl -n default get svc orders
kubectl -n default get endpointslice \
  -l kubernetes.io/service-name=orders

必须一起看。仅执行 nslookup orders 不能证明业务服务可用。


3.3 Headless Service:DNS 返回 Endpoint 地址

当 Service 设置:

spec:
  clusterIP: None

它就是 Headless Service。它没有虚拟 ClusterIP,DNS 通常直接返回关联 Endpoint 的地址。

例如:

apiVersion: v1
kind: Service
metadata:
  name: db
  namespace: default
spec:
  clusterIP: None
  selector:
    app: db
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432

查询:

db.default.svc.cluster.local

可能返回多个 A 记录:

db.default.svc.cluster.local.  A  10.244.1.21
db.default.svc.cluster.local.  A  10.244.2.18
db.default.svc.cluster.local.  A  10.244.3.09

此时 DNS 本身不提供普通 ClusterIP Service 那种统一虚拟地址。客户端如何选择地址、是否轮询、是否重试,取决于客户端解析器和应用实现。

这解释了一个常见误解:

“DNS 返回了多个 Pod IP,所以 Kubernetes 一定会自动均匀负载均衡。”

不一定。DNS 只返回地址集合;客户端可能只使用第一个地址,也可能缓存整个集合,也可能自行实现轮询。连接失败后的重试策略同样由客户端决定。


3.4 EndpointSlice 中的 ready、serving 和 terminating

EndpointSlice 不只是一个 IP 列表。Endpoint 条目可能包含:

  • addresses:地址;
  • conditions.ready:是否就绪;
  • conditions.serving:是否仍能提供服务;
  • conditions.terminating:是否正在终止。

CoreDNS 和 Service 流量实现会结合这些状态决定普通服务发现和流量转发行为。具体细节会随 Kubernetes 版本和实现演进,但生产排障时不能只看 Pod 是否处于 Running

  • Running 不等于 Ready
  • Pod 通过 readiness probe 后,才通常会成为普通 Service 的可用后端;
  • Pod 删除过程中可能仍有连接排空相关状态;
  • publishNotReadyAddresses: true 会改变未就绪 Endpoint 的发布语义,常用于某些需要先发现彼此、再完成选主或初始化的系统。

如果一个新 Pod 已经 Running,但 DNS 的 Headless Service 结果中暂时没有它,可能是:

  1. readiness 尚未通过;
  2. EndpointSlice 尚未更新;
  3. CoreDNS informer 尚未观察到变化;
  4. DNS 客户端或 CoreDNS 缓存仍保存旧结果。

这不是单纯的“DNS 随机丢记录”。


3.5 Service 的 DNS 记录形式

常见记录形式如下。

普通 Service

<service>.<namespace>.svc.<cluster-domain>

通常返回 ClusterIP 的 A 或 AAAA 记录。

Headless Service

同样的名称通常返回一个或多个 Endpoint 地址。

按端口命名的 SRV 记录

若端口有名称,例如:

ports:
  - name: http
    port: 80
    targetPort: 8080

可能存在类似:

_http._tcp.orders.default.svc.cluster.local

SRV 记录包含端口和目标名称,适合支持 SRV 的客户端。它不是所有应用都会自动使用的机制;应用若只调用普通的 getaddrinfo(),通常不会因为存在 SRV 记录而自动获得端口。

对于 Headless Service,SRV 结果可指向具体 Pod 的 DNS 名称;实际结果受 Endpoint、端口名和 CoreDNS 行为影响,必须用 dig 验证,不应根据名称形式臆测。

ExternalName Service

例如:

apiVersion: v1
kind: Service
metadata:
  name: payments
  namespace: default
spec:
  type: ExternalName
  externalName: payments.example.com

查询:

payments.default.svc.cluster.local

通常得到指向 payments.example.com 的 CNAME,而不是 ClusterIP。

ExternalName 不创建代理、Endpoint 或负载均衡规则。它只是 DNS 层的别名,因此存在边界:

  • 客户端必须正确处理 CNAME;
  • TLS 证书名、HTTP Host 和外部服务身份仍需正确配置;
  • 它不提供 Kubernetes NetworkPolicy 意义上的出口控制;
  • 外部域名变化会受 DNS 缓存影响。

3.6 Pod DNS 名称不是普通 Service 的替代品

Kubernetes 可以为 Pod 提供若干形式的 DNS 名称,但它们有前置条件和版本边界,不能把所有 Pod IP 都想当然地映射成稳定域名。

使用 StatefulSet 时,常见组合是:

  • 一个 Headless Service;
  • StatefulSet.spec.serviceName 指向该 Service;
  • Pod 设置稳定的 ordinal 身份。

例如 Pod 可能使用:

db-0.db.default.svc.cluster.local

这里的 db-0 是 StatefulSet 的稳定序号身份,db 是 Headless Service。Pod 重建后 IP 可以变化,但该逻辑身份仍可保持。

较早的 Kubernetes 文档还描述过基于 Pod IP 的特殊 DNS 名称,例如把 IPv4 地址中的点替换为连字符。这类行为与 pods 插件配置、版本和发行版有关,当前生产设计不应依赖未验证的 Pod-IP 反向编码名称。稳定服务访问应优先使用 Service;稳定实例身份应使用 StatefulSet 加 Headless Service。


4. Search:短名称为什么能工作,又为什么会产生额外查询

4.1 名称的逐级缩短

default Namespace 中,Pod 常见的 search 列表是:

default.svc.cluster.local
svc.cluster.local
cluster.local

因此应用查询短名称:

orders

典型解析器会尝试把它扩展为:

orders.default.svc.cluster.local
orders.svc.cluster.local
orders.cluster.local
orders

最终 orders.default.svc.cluster.local 成功后,应用就能访问同 Namespace 的 Service。

跨 Namespace 访问时,推荐至少使用:

orders.production

它会基于当前 Pod 的 search 列表尝试:

orders.production.default.svc.cluster.local
orders.production.svc.cluster.local
orders.production.cluster.local
orders.production

其中真正有效的通常是:

orders.production.svc.cluster.local

如果直接使用完整限定名:

orders.production.svc.cluster.local.

末尾的点表示 DNS 根,名称已经是绝对名称,通常不会再套用 search 列表。


4.2 ndots:5 如何影响查询顺序

ndots 表示名称中至少包含多少个点时,解析器倾向于先把它当作绝对名称处理。Kubernetes 常见配置是:

options ndots:5

以典型 glibc resolver 为例:

查询 orders

点数为 0,小于 5:

  1. 尝试 orders.default.svc.cluster.local
  2. 尝试其他 search 后缀
  3. 最后尝试绝对名称 orders.

查询 orders.default.svc.cluster.local

点数为 3,小于 5:

  1. 可能先尝试 orders.default.svc.cluster.local.default.svc.cluster.local
  2. 再尝试其他 search 拼接结果
  3. 最后尝试真正的 orders.default.svc.cluster.local

这些前置查询大多会得到 NXDOMAIN 或其他失败响应,然后才到达正确名称。

查询 orders.default.svc.cluster.local.

末尾点表示绝对名称:

  1. 直接查询 orders.default.svc.cluster.local
  2. 不拼接 search 后缀

因此,在高频、跨 Namespace 或集群外域名访问场景中,合理使用完整名称和末尾点可以减少无效查询。但不能假定所有语言运行时、操作系统和 libc 完全遵循同一顺序;Go 的纯 Go resolver、glibc、musl 和应用自带 DNS 客户端可能存在差异。


4.3 Search 的代价不只是“多几毫秒”

设 search 列表有 ss 个后缀,应用查询的名称未达到 ndots 阈值,并且正确答案位于第 kk 个候选名称。若每个候选需要一次请求,则最多可能产生:

Q=kQ = k

次请求;若所有候选都失败,还可能继续查询绝对名称,使请求数接近:

Q=s+1Q = s + 1

实际请求数还会受到以下因素影响:

  • A 和 AAAA 查询;
  • 应用是否并行发起 IPv4/IPv6 查询;
  • resolver 是否重试;
  • UDP 截断后是否改用 TCP;
  • CoreDNS 是否命中缓存;
  • 上游 DNS 是否返回 NXDOMAIN、SERVFAIL 或超时。

例如一个应用每秒发起 1000 次对 api.external.example.com 的请求,而该名称有 4 个点、低于 ndots:5。典型解析器可能先查询多个带集群 search 后缀的名称,再查询外部真实名称。结果是:

  • CoreDNS 收到更多无效请求;
  • 上游企业 DNS 或云 DNS 承受额外压力;
  • 延迟和超时概率增加;
  • 负缓存可能保存这些无效名称。

可验证实际行为:

kubectl run -it --rm dns-debug \
  --image=registry.k8s.io/e2e-test-images/dnsutils:1.3 \
  --restart=Never -- sh

进入容器后:

cat /etc/resolv.conf

dig orders.default.svc.cluster.local
dig orders.default.svc.cluster.local.
dig +search orders
dig +short api.example.com

dig +search orders 显式使用 search 列表;带末尾点的名称则不会因 search 列表扩展。抓包时可进一步确认解析器真实发出的查询顺序:

kubectl sniff <pod-name> -n <namespace> -f "udp port 53 or tcp port 53"

kubectl sniff 需要额外插件和抓包权限,生产环境不应默认安装到所有节点;也可以在节点、DNS Pod 或 NodeLocal DNSCache 路径上使用受控的 tcpdump


4.4 Pod 的 DNS 策略会改变 search 行为

常见 dnsPolicy 有:

  • ClusterFirst:默认策略,集群内名称优先交给集群 DNS;
  • ClusterFirstWithHostNet:适用于 hostNetwork: true 的 Pod;
  • Default:继承运行该 Pod 的节点 DNS 配置;
  • None:不使用默认策略,必须通过 dnsConfig 明确提供配置。

例如:

spec:
  dnsPolicy: None
  dnsConfig:
    nameservers:
      - 10.96.0.10
    searches:
      - default.svc.cluster.local
      - svc.cluster.local
      - cluster.local
    options:
      - name: ndots
        value: "2"

dnsPolicy: None 的风险是配置错误会直接导致集群服务解析失败;nameservers、search 列表长度和单项长度还受操作系统及 DNS 配置限制。不要只修改容器镜像里的 /etc/resolv.conf,因为它通常由 kubelet 在 Pod 启动时注入,重建 Pod 后会恢复。

hostNetwork: true 的 Pod 若仍使用默认 ClusterFirst,可能无法按预期使用集群 DNS,通常需要显式选择 ClusterFirstWithHostNet。实际配置应检查 Pod 的 dnsPolicy 和最终文件,而不是只看 Deployment 模板。


5. 缓存:哪些状态会被记住,为什么修改后不会立即生效

5.1 Kubernetes DNS 中可能存在多层缓存

一次解析可能经过多层缓存:

应用自身缓存
  ↓
语言运行时或 libc 缓存
  ↓
NodeLocal DNSCache(可选)
  ↓
CoreDNS cache 插件
  ↓
上游 DNS 缓存

Kubernetes 不规定每个应用和发行版必须使用哪些缓存。CoreDNS 的 cache 插件是常见实现,但它不是 Kubernetes API 的一部分。

缓存的基本规则是:

  • 正缓存:记录存在,例如 Service A 记录;
  • 负缓存:记录不存在,例如 NXDOMAIN;
  • 记录带有 TTL,缓存通常在 TTL 到期前直接返回;
  • 过期后才重新查询权威来源或上游。

CoreDNS 的 kubernetes 插件负责生成集群域名响应,cache 插件负责缓存响应。Corefile 中的:

cache 30

表示一个常见的缓存配置示例,但不能把这个数字当成所有集群的保证值。应以实际 Corefile、CoreDNS 版本和插件行为为准。

kubernetes 插件还可能通过 ttl 配置控制其生成记录的 TTL,例如:

kubernetes cluster.local {
    ttl 30
}

TTL 为 0 可以禁止这类记录被缓存,但会增加查询和 CoreDNS 负载;较长 TTL 则降低负载,却延长变更传播时间。生产调优必须同时考虑 CoreDNS 缓存、NodeLocal DNSCache、应用缓存和上游缓存,单独修改一个 TTL 不一定改变最终效果。


5.2 一个 Service 变更的完整时间线

假设应用查询:

api.default.svc.cluster.local

初始结果是 ClusterIP 10.96.1.10

现在删除 Service 并重新创建同名 Service,新的 ClusterIP 为 10.96.2.20。可能发生以下过程:

  1. Kubernetes API 写入新对象;
  2. CoreDNS informer 收到 Service 变化;
  3. CoreDNS 本地状态更新;
  4. CoreDNS 新查询开始生成 10.96.2.20
  5. 但 NodeLocal DNSCache、CoreDNS cache 或应用缓存仍可能保存旧的 10.96.1.10
  6. 缓存 TTL 到期后,调用方才逐步看到新值。

因此,kubectl get svc 看到新 ClusterIP,并不能证明每个客户端立刻都拿到新地址。

验证时要绕开不同层级:

# 直接询问指定 DNS 服务器
dig @10.96.0.10 api.default.svc.cluster.local

# 在业务 Pod 内询问,观察该 Pod 实际使用的 nameserver
kubectl exec -n default deploy/client -- \
  getent ahostsv4 api.default.svc.cluster.local

dig @10.96.0.10 直接指定 DNS 服务器,便于确认 CoreDNS 或其 Service 的结果;getent 更接近应用通过系统 resolver 的实际行为,但可能受 libc 和本地缓存影响。


5.3 负缓存会让“刚创建的 Service”暂时解析失败

如果应用在 Service 创建前查询:

new-api.default.svc.cluster.local

CoreDNS 可能返回 NXDOMAIN,并由缓存保存该负结果。随后即使 Service 已创建,短时间内仍可能看到旧的 NXDOMAIN。

这类故障的特征是:

  • kubectl get svc new-api 已经成功;
  • 某些新启动的 Pod 能解析,旧进程仍失败;
  • 清理应用缓存或等待 TTL 后恢复;
  • 反复重试时结果从 NXDOMAIN 变成成功。

这不是 Kubernetes API 回滚,也不是 DNS “随机”。它是不同观察者处于不同缓存状态。


5.4 缓存不是一致性协议

DNS TTL 只提供“在这段时间内可以复用该结果”的约束,不提供强一致性。尤其不应把短 TTL 当作服务发布完成的事务边界:

  • Service 对象更新、EndpointSlice 更新和 CoreDNS informer 更新有传播路径;
  • 客户端可能缓存结果超过预期;
  • 某些应用忽略 TTL;
  • 某些连接池长期复用已解析的地址;
  • 已建立的 TCP 连接不会因为 DNS 记录改变而自动迁移。

如果应用需要无损切换后端,必须设计连接排空、重试、健康检查和生命周期协作,而不是只依赖 DNS TTL。


6. CoreDNS 到上游 DNS:集群域名和外部域名的分流

CoreDNS 通常负责:

cluster.local
svc.cluster.local
Pod/Service 相关反向解析区域

对其他名称,例如:

www.example.com
database.corp.example

则根据 Corefile 使用 forward 或其他插件转发。

常见配置:

.:53 {
    errors
    health
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa
    forward . /etc/resolv.conf
    cache 30
    loop
    reload
}

/etc/resolv.conf 在这里是 CoreDNS 容器自身看到的上游配置,不一定等于业务 Pod 的 /etc/resolv.conf。这一区别很重要。

6.1 常见的转发错误

如果节点的 /etc/resolv.conf 指向集群 DNS Service,而 CoreDNS 又把所有非集群域名转发到 /etc/resolv.conf,就可能形成环路:

CoreDNS
  → 节点 resolv.conf
  → 集群 DNS Service
  → CoreDNS
  → ...

CoreDNS 的 loop 插件通常用于检测这类配置问题,但不能替代人工检查。发生环路时,外部域名可能大量超时或返回 SERVFAIL,而集群内域名仍然看似正常。

检查:

kubectl -n kube-system exec deploy/coredns -- \
  cat /etc/resolv.conf

kubectl -n kube-system logs deploy/coredns

上游企业 DNS、云 VPC DNS 和节点 resolv.conf 的来源具有明显环境差异。不要直接把生产 Corefile 从一个集群复制到另一个集群,必须验证:

  • 上游 DNS 地址是否可达;
  • 是否需要搜索域或特殊转发区域;
  • 是否存在 UDP 53 被阻止、DNS over TLS/HTTPS 等企业策略;
  • 节点 resolv.conf 是否由 systemd-resolved 生成了 stub 地址;
  • CoreDNS Pod 是否能访问该 stub 地址。

7. 用一个最小实验验证 Service Discovery

下面创建一个后端和一个客户端。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: registry.k8s.io/e2e-test-images/agnhost:2.39
          args: ["netexec"]
          ports:
            - name: http
              containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: default
spec:
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: dns-client
  namespace: default
spec:
  restartPolicy: Never
  containers:
    - name: client
      image: registry.k8s.io/e2e-test-images/dnsutils:1.3
      command: ["sleep", "3600"]

应用配置:

kubectl apply -f example.yaml
kubectl -n default wait \
  --for=condition=available deployment/web \
  --timeout=120s
kubectl -n default wait \
  --for=condition=Ready pod/dns-client \
  --timeout=120s

查看 Service 和 EndpointSlice:

kubectl -n default get svc web -o wide
kubectl -n default get endpointslice \
  -l kubernetes.io/service-name=web \
  -o wide

执行解析:

kubectl -n default exec dns-client -- \
  nslookup web

kubectl -n default exec dns-client -- \
  nslookup web.default.svc.cluster.local

kubectl -n default exec dns-client -- \
  dig +short web.default.svc.cluster.local.

kubectl -n default exec dns-client -- \
  getent hosts web

预期结果:

  • web 可以通过 search 列表解析;
  • 完整名称解析到 Service ClusterIP;
  • getent 返回的地址应与 kubectl get svc web 中的 ClusterIP 对应;
  • EndpointSlice 中应出现两个后端地址。

测试连接:

kubectl -n default exec dns-client -- \
  wget -qO- http://web/

这个请求验证的不只是 DNS,还验证了:

  1. web 能解析;
  2. ClusterIP 可以访问;
  3. Service 端口 80 到 targetPort 8080 的映射有效;
  4. 至少一个后端能处理 HTTP 请求。

如果 nslookup 成功而 wget 失败,问题已经越过 DNS 层,应继续检查 Service、EndpointSlice、kube-proxy/CNI、NetworkPolicy 和应用监听端口。


8. 故障诊断:按响应类型和路径定位

8.1 先看 Pod 实际 DNS 配置

kubectl exec -n <namespace> <pod> -- cat /etc/resolv.conf

重点检查:

  • nameserver 是否为预期的集群 DNS 地址;
  • search 列表是否包含正确的 Namespace 和集群域;
  • options ndots 是否造成过多查询;
  • Pod 是否使用 dnsPolicy: DefaultNone
  • hostNetwork 是否与 DNS 策略匹配。

若只有某一个 Pod 失败,优先怀疑该 Pod 的策略、网络命名空间、镜像 resolver 或本地配置;若所有 Namespace 都失败,优先检查集群 DNS Service、CoreDNS 和 CNI。


8.2 检查 DNS Service 和 CoreDNS Endpoint

kubectl -n kube-system get svc kube-dns -o wide

kubectl -n kube-system get endpointslice \
  -l kubernetes.io/service-name=kube-dns \
  -o wide

kubectl -n kube-system get pods \
  -l k8s-app=kube-dns \
  -o wide

kubectl -n kube-system get deployment coredns

判断关系:

  • kube-dns Service 不存在或 ClusterIP 不符合 Pod 配置:Pod 可能使用了旧配置或集群安装异常;
  • Service 存在但 EndpointSlice 为空:CoreDNS Pod 没有被选中、未 Ready 或标签不匹配;
  • CoreDNS Pod CrashLoopBackOff:先看日志和 Corefile;
  • CoreDNS Ready,但 Pod 到 DNS Service 不通:继续查 Service 转发、CNI、NetworkPolicy 和节点路径。

8.3 直接从 Pod 查询集群域和外部域

kubectl exec -n <namespace> <pod> -- \
  nslookup kubernetes.default.svc.cluster.local

kubectl exec -n <namespace> <pod> -- \
  nslookup example.com

这两个查询用于区分:

集群域名 外部域名 更可能的范围
失败 失败 Pod 到 DNS Service、CoreDNS 本身、DNS 网络路径
成功 失败 CoreDNS 上游转发、节点 resolv.conf、外部 DNS 或出口网络
失败 成功 CoreDNS 的 kubernetes 插件、API 同步、集群域配置
成功 成功 DNS 基本路径可用,继续排查具体名称、缓存、Service 或应用

注意不要把 nslookup 的“Server”字段当成权威证据;它只表示客户端使用的 nameserver,不证明该服务器正确处理了目标区域。


8.4 区分 NXDOMAIN、SERVFAIL 和 timeout

NXDOMAIN

含义通常是“该名称不存在”。常见原因:

  • Service 名称拼写错误;
  • Namespace 错误;
  • 查询了不存在的记录类型;
  • Service 尚未创建;
  • 负缓存尚未过期;
  • CoreDNS 的集群域配置与 kubelet 配置不一致。

先验证对象:

kubectl get svc -A | grep '<service-name>'
kubectl get namespace

SERVFAIL

含义是服务器无法完成查询,通常不是“名称确定不存在”。可能原因:

  • CoreDNS 插件配置错误;
  • Kubernetes API 不可用或同步失败;
  • 上游 DNS 失败;
  • DNSSEC、转发或循环配置问题;
  • CoreDNS 内部资源耗尽或异常。

应查看 CoreDNS 日志、指标和 Corefile,而不是直接重启所有业务 Pod。

timeout

含义是请求没有在客户端等待时间内得到响应。常见原因:

  • Pod 到 DNS Service 的 UDP/TCP 53 不通;
  • NetworkPolicy 阻止 DNS;
  • Service 转发规则或 CNI 异常;
  • CoreDNS 进程无响应;
  • 上游 DNS 超时;
  • UDP 分片、MTU 或 conntrack 问题;
  • 高负载导致请求排队或丢弃。

使用 dig 观察响应和传输:

kubectl exec -n <namespace> <pod> -- \
  dig +time=2 +tries=1 kubernetes.default.svc.cluster.local

kubectl exec -n <namespace> <pod> -- \
  dig +tcp +time=2 +tries=1 kubernetes.default.svc.cluster.local

如果 UDP 失败而 TCP 成功,不能简单得出“DNS 正常”:这提示 UDP 路径、MTU、分片或防火墙可能有问题。DNS 查询并不只使用 UDP,响应较大时也可能因截断改用 TCP。


8.5 检查 NetworkPolicy 和 CNI

NetworkPolicy 经常只允许应用到应用的流量,却忘记允许 Pod 到 DNS 的 UDP/TCP 53:

egress:
  - to:
      - namespaceSelector:
          matchLabels:
            kubernetes.io/metadata.name: kube-system
    ports:
      - protocol: UDP
        port: 53
      - protocol: TCP
        port: 53

这段规则只是说明方向,不是对所有 CNI 的完整生产策略。某些环境中 DNS 通过 NodeLocal DNSCache 提供,目标地址可能是节点本地 IP,而不是 kube-dns Service IP;策略必须根据实际 nameserver 和 CNI 实现验证。

如果 DNS timeout 与节点、可用区、特定 CNI 路径相关,应进一步检查:

  • Pod 到 DNS Service IP 的路由;
  • Service 转换规则;
  • CNI dataplane;
  • MTU;
  • conntrack 表和丢包;
  • 节点防火墙;
  • NodeLocal DNSCache 的监听地址和日志。

这也是为什么 DNS 故障排查不能停在 kubectl get pods


9. CoreDNS 本身的容量与高并发问题

DNS 请求通常是短请求,但会产生突发流量。以下模式容易放大请求数量:

  • 应用频繁调用解析函数而不复用连接;
  • 使用短名称配合较高 ndots
  • 外部域名大量触发无效 search 查询;
  • 缓存被频繁失效或 TTL 过短;
  • CoreDNS 副本数不足;
  • 上游 DNS 响应慢;
  • DNS 查询和业务请求共用受限的网络路径。

CoreDNS 的常见指标和日志可以帮助判断:

kubectl -n kube-system logs deploy/coredns

kubectl -n kube-system port-forward \
  deploy/coredns 9153:9153

是否能访问指标端口取决于部署配置。常见 Prometheus 指标包括请求总数、响应码、请求延迟、缓存命中等,但指标名称和插件是否启用必须以实际 CoreDNS 版本为准。

在大型集群中,NodeLocal DNSCache 是常见的可选架构:每个节点运行本地 DNS 缓存,Pod 先访问节点本地地址,再由它访问 CoreDNS。它可以减少跨节点 UDP 路径和 conntrack 压力,但会增加一层组件和故障面:

Pod → 节点本地 DNSCache → CoreDNS Service/Pod → 上游 DNS

启用后,排障必须同时查看:

  • Pod /etc/resolv.conf 指向谁;
  • NodeLocal DNSCache 是否运行;
  • 本地监听地址是否存在;
  • 本地缓存到 CoreDNS 的路径;
  • CoreDNS 到上游的路径。

NodeLocal DNSCache 的部署方式不是所有 Kubernetes 安装都默认提供,不能只根据“集群使用 CoreDNS”判断它是否存在。


10. 修改 CoreDNS 配置时的状态和风险

CoreDNS 配置通常保存在:

kubectl -n kube-system get configmap coredns -o yaml

修改 ConfigMap 可能触发 CoreDNS 的 reload 插件重新加载,也可能需要由集群安装方式执行滚动更新。不要直接编辑正在运行的 CoreDNS Pod,因为 Pod 重建后修改会丢失。

修改后至少验证:

kubectl -n kube-system rollout status deployment/coredns

kubectl run -it --rm dns-check \
  --image=registry.k8s.io/e2e-test-images/dnsutils:1.3 \
  --restart=Never -- \
  nslookup kubernetes.default.svc.cluster.local

kubectl run -it --rm external-dns-check \
  --image=registry.k8s.io/e2e-test-images/dnsutils:1.3 \
  --restart=Never -- \
  nslookup example.com

生产风险包括:

  • 删除 kubernetes 插件导致集群内 Service 全部无法解析;
  • 修改 forward 目标导致外部域名全部超时;
  • 引入转发环路;
  • 使用不兼容插件语法导致 CoreDNS 启动失败;
  • TTL 过短造成查询量暴增;
  • fallthrough 范围错误,让本应返回 NXDOMAIN 的名称继续被错误转发;
  • 仅更新 ConfigMap,却没有确认 CoreDNS Pod 已加载新配置。

因此配置变更必须同时验证集群域、反向区域和外部域名,而不是只测一个 example.com


11. 常见误解与反例

误解一:Service 名称解析成功就代表服务正常

反例:

orders.default.svc.cluster.local → 10.96.42.17

但 EndpointSlice 为空。此时 DNS 完全可以成功,业务连接仍然失败。

正确检查:

kubectl -n default get svc orders
kubectl -n default get endpointslice \
  -l kubernetes.io/service-name=orders

误解二:Headless Service 会自动提供稳定的单一入口

Headless Service 返回的是 Endpoint 地址集合,不是虚拟 ClusterIP。Pod 重启后地址可能变化,客户端必须能处理多个地址、缓存和重试。

需要稳定实例身份时,应使用 StatefulSet 和 Headless Service,而不是把某个 Pod IP 写死。


误解三:改了 Service selector,DNS 应该立刻变化

selector 变化首先影响 EndpointSlice,随后 CoreDNS 需要观察到变化,最后客户端缓存才会过期。变化传播是多阶段过程:

Service selector
  → EndpointSlice controller
  → EndpointSlice
  → CoreDNS informer
  → CoreDNS cache
  → NodeLocal/app cache
  → 应用下一次查询

任何阶段延迟或故障,都可能造成旧结果仍被使用。


误解四:把 ndots 改小就一定更快

较小的 ndots 可能减少外部域名的 search 查询,但也会改变短名称和相对名称的解析顺序。应用、基础设施组件和外部 DNS 可能依赖现有行为。应先用 dig 或抓包确认查询数量,再针对特定工作负载调整,而不是全局盲改。


误解五:重启 CoreDNS 可以修复所有 DNS 故障

如果根因是:

  • Pod 到 DNS Service 的网络不通;
  • NetworkPolicy 阻断;
  • CoreDNS 上游不可达;
  • EndpointSlice 没有后端;
  • 应用缓存旧地址;

重启 CoreDNS 只能暂时清除部分内存状态,不能修复根因,甚至可能在重启期间扩大故障范围。


12. 一条可复用的排障路径

当业务报告“域名解析失败”时,可以按以下顺序缩小范围:

第一步:确认名称和 Namespace

kubectl get svc <name> -n <namespace>

确认应用使用的是:

<name>.<namespace>.svc.<cluster-domain>

而不是误把跨 Namespace Service 当成当前 Namespace 的短名称。

第二步:确认 Service 和后端状态

kubectl get svc <name> -n <namespace> -o yaml
kubectl get endpointslice \
  -n <namespace> \
  -l kubernetes.io/service-name=<name> \
  -o yaml

确认 ClusterIP、端口、selector、Endpoint 地址和 ready 状态。

第三步:确认 Pod resolver 配置

kubectl exec -n <namespace> <pod> -- cat /etc/resolv.conf

确认 nameserver、search、ndots 和 dnsPolicy。

第四步:从同一个网络命名空间查询

kubectl exec -n <namespace> <pod> -- \
  dig +short <name>.<namespace>.svc.<cluster-domain>.

使用末尾点避免 search 干扰,再测试短名称:

kubectl exec -n <namespace> <pod> -- \
  getent hosts <name>

第五步:对比集群域和外部域

kubectl exec -n <namespace> <pod> -- \
  nslookup kubernetes.default.svc.cluster.local

kubectl exec -n <namespace> <pod> -- \
  nslookup example.com

由响应组合判断问题在内部 DNS、上游 DNS 还是共同网络路径。

第六步:检查 CoreDNS 和 DNS Service

kubectl -n kube-system get svc kube-dns -o wide
kubectl -n kube-system get endpointslice \
  -l kubernetes.io/service-name=kube-dns -o wide
kubectl -n kube-system get pods -l k8s-app=kube-dns
kubectl -n kube-system logs deploy/coredns
kubectl -n kube-system get configmap coredns -o yaml

第七步:若为 timeout,再查网络路径

重点查看 UDP/TCP 53、Service 转发、CNI、NetworkPolicy、MTU、conntrack 和节点防火墙。若为 NXDOMAIN 或 SERVFAIL,则优先检查名称、Corefile、API 同步和缓存,而不是先抓包。


13. 生产设计中的取舍

Kubernetes DNS 的稳定性来自多个层次的组合,而不是某个单独参数:

  • 使用 Service 名称而非 Pod IP;
  • 需要实例身份时使用 StatefulSet 加 Headless Service;
  • 跨 Namespace 时使用明确名称;
  • 高频或外部域名查询关注 ndots 和 search 放大;
  • 设计连接池和重试时考虑 DNS 变化不会迁移已有连接;
  • 关注正缓存和负缓存造成的传播延迟;
  • 对 CoreDNS 上游转发设置清晰的失败边界;
  • 用 EndpointSlice 判断后端,而不是只看 Service 对象;
  • 将 CoreDNS、NodeLocal DNSCache、CNI 和 NetworkPolicy 纳入同一条故障路径;
  • 对 Corefile 修改进行集群内和集群外双向验证。

从规范角度看,Service 的 DNS 名称和相关服务发现语义是 Kubernetes 的核心能力;从实现角度看,CoreDNS 插件链、缓存默认值、NodeLocal DNSCache 和云厂商网络路径都可能不同。真正可靠的判断必须基于三份现场证据:

Pod 的 /etc/resolv.conf
CoreDNS 的实际 Corefile 与日志
Service/EndpointSlice 的实际状态

只有把这三者与实际 DNS 响应、Service 连接结果结合起来,才能区分“名称不存在”“解析结果过期”“DNS 路径不通”和“DNS 正常但服务后端故障”。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。