在 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 | 转发到上游 DNS | max_concurrent、policy |
| cache | 结果缓存 | 缓存秒数、success/denial |
| loop | 检测转发环 | 无参数,检测到即 fatal |
| autopath | 减少 search 域查询次数 | 需配合 pods |
| template | 自定义响应 | 用于特殊域名 |
| hosts | 静态 hosts | 用于内网固定域名 |
3. DNS 记录与命名规则
3.1 Service 记录
对一个名为 backend、命名空间 prod 的 Service(ClusterIP 型):
| 记录类型 | 名称 | 解析结果 |
|---|---|---|
| A | backend.prod.svc.cluster.local | Service 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 Service | Headless 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 放大效应
| 场景 | 实际查询数 |
|---|---|
集群内短名 backend | 1(命中 search 域第一条) |
| 集群内 FQDN | 1 |
外部域名 api.github.com | 4(3 次 NXDOMAIN + 1 次成功) |
| 外部域名 + 高 QPS | 4 × 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,存在三个问题:
- conntrack 竞争:UDP DNS 在高并发下容易触发 conntrack 表满与丢包(经典 5 秒超时);
- 额外跳数:每个请求多一次 iptables/iptables 转发;
- 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
三个要点:
- 副本数:
max(2, ceil(节点数 / 10)),且必须 ≥ 2; - 不要设 CPU limits:DNS 是延迟敏感型,CPU 被 CFS 节流会导致解析超时(这是最常见的「DNS 偶发慢」原因之一);
- 拓扑打散:用
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 加副本 |
| 间歇 SERVFAIL | CoreDNS 副本不足 / 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 秒超时、短名失败这三类症状,各自对应完全不同的根因,按分层命令逐跳验证比猜测快得多。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。