CoreDNS 调优:NDots 设不对,每次 DNS 查询都多走一步
CoreDNS 调优NDots 设不对每次 DNS 查询都多走一步一、从 50ms 到 5ms一次 DNS 解析的代价到底有多高Kubernetes 集群中一个 Pod 访问同命名空间下的另一个 ServiceDNS 解析应该几乎是瞬时的。但实际监控显示很多集群的 DNS P99 延迟高达 50-100ms。排查到最后元凶往往是ndots参数的默认值。在标准的 Kubernetes Pod 配置中/etc/resolv.conf的options ndots:5是默认行为。这个值的意思很明确域名中包含的点的数量少于 5 个时DNS 解析器先把域名当作相对域名处理依次追加resolv.conf中search域名的后缀进行查询。只有当所有后缀都查不到时才把它当作绝对域名FQDN再查一次。一个典型的 Kubernetes Pod 的resolv.conf长这样nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5现在看看一个 Pod 访问my-service时发生了什么。my-service只包含 0 个点0 5。DNS 解析器执行以下查询序列my-service.default.svc.cluster.local.→ NXDOMAIN这个 Service 不存在于 default 命名空间假设 Pod 在其他命名空间my-service.svc.cluster.local.→ 成功虽然第 2 次查询就命中了但第 1 次查询已经发出了。更糟糕的场景访问redis-cache.production这个域名——包含 1 个点1 5。解析器先查redis-cache.production.default.svc.cluster.localNXDOMAIN再查redis-cache.production.svc.cluster.localNXDOMAIN最后才查redis-cache.production成功。3 次查询前 2 次都是浪费。对一个每天有数千万次 DNS 查询的集群来说额外多发的 1-2 次查询就是数万次无效的 CoreDNS 请求。基础设施不需要漂亮话——DNS 延迟每增加 1ms所有依赖它的微服务调用都以那个 1ms 为起点叠加。二、ndots 解析机制与搜索域扩展流程ndots 的决策逻辑由 glibc 的 DNS 解析器实现Go 的net包有自己的纯 Go 实现。无论哪种实现核心流程一致flowchart TD A[DNS 查询请求:br/my-service.production] -- B{域名中以 . 结尾?} B --|是| C[视为 FQDNbr/直接查询] B --|否| D{域名中点数br/ ndots?} D --|点数 5| E[先尝试绝对查询] E -- F{绝对查询成功?} F --|是| G[返回结果] F --|否| H[追加 search 域重试] D --|点数 5| H H -- I{还有 search 域?} I --|有| J[追加下一个 search 后缀br/查询] J -- K{查询成功?} K --|是| G K --|否| I I --|无更多 search 域| L[作为绝对域名br/最后尝试一次] L -- M{查询成功?} M --|是| G M --|否| N[返回 NXDOMAIN] C -- F例子Pod 位于ai-training命名空间search域为ai-training.svc.cluster.local svc.cluster.local cluster.localndots5。查询vector-store.ai-training1 个点1 5第 1 次vector-store.ai-training.ai-training.svc.cluster.local.→ NXDOMAIN第 2 次vector-store.ai-training.svc.cluster.local.→ NXDOMAIN第 3 次vector-store.ai-training→ 成功假设是外部域名查询google.com1 个点1 5同样触发 3 次查询前 2 次全部 NXDOMAIN。这就是问题的核心对于外部域名ndots 默认值 5 导致几乎所有查询都需要先浪费 2 次集群内部的 NXDOMAIN 查询然后才真正查外部 DNS。三、CoreDNS 调优的完整实践3.1 修改 Pod 的 ndots 配置最直接的手段是通过 Pod 的dnsConfig覆盖默认 ndotsapiVersion: v1 kind: Pod metadata: name: inference-gateway spec: dnsConfig: options: - name: ndots value: 1 dnsPolicy: ClusterFirst containers: - name: gateway image: inference-gateway:latest对于大量访问外部服务的 Pod如调用 OpenAI API 或 HuggingFace Hubndots:1是最优选择。任何包含至少 1 个点的域名都直接作为绝对域名查询避免了 search 域的无效重试。3.2 CoreDNS 的缓存与负缓存配置减少查询次数的同时也要确保 CoreDNS 能高效响应。CoreDNS 的cache插件配置直接决定命中率apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: 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 { success 9984 60 denial 9984 10 } loop reload loadbalance round_robin }关键参数解释success 9984 60缓存成功响应的条目数上限为 9984TTL 取 min(60 秒, DNS 响应中的 TTL)。9984 这个数字是因为 CoreDNS 内部用 256 个 shard每个 shard 39 条 9984。调大这个值会增加内存占用但在大集群中能显著提升缓存命中率。denial 9984 10缓存 NXDOMAIN 响应的条目数。默认 CoreDNS 只缓存 NXDOMAIN 5 秒。对于 ndots 带来的大量 NXDOMAIN 查询增加到 10 秒可以减少部分无效重试的开销但不应设太大——长期缓存 NXDOMAIN 可能掩盖实际的服务注册延迟。3.3 使用 NodeLocal DNSCache 减少 iptables 跳转DNS 查询在 Kubernetes 中的另一个隐性延迟来源是 iptables DNAT。Pod 发出的 DNS 请求目标 IP 是 CoreDNS 的 ClusterIP如10.96.0.10需要经过 iptables 的 DNAT 规则转换为 CoreDNS Pod 的实际 IP。NodeLocal DNSCache 通过在每台节点上运行一个本地 DNS 缓存 DaemonSet让 Pod 的 DNS 请求直接走本地 IP如169.254.20.10绕过 iptablesapiVersion: v1 kind: ConfigMap metadata: name: node-local-dns namespace: kube-system data: Corefile: | cluster.local:53 { errors cache { success 9984 60 denial 9984 10 } reload loop bind 169.254.20.10 forward . 10.96.0.10 { force_tcp } prometheus :9253 health 169.254.20.10:8080 } in-addr.arpa:53 { errors cache 30 reload loop bind 169.254.20.10 forward . 10.96.0.10 { force_tcp } prometheus :9253 } .:53 { errors cache 30 reload loop bind 169.254.20.10 forward . /etc/resolv.conf prometheus :9253 }NodeLocal DNSCache 的bind 169.254.20.10绑定在 link-local 地址上Pod 的dnsPolicy设置为None后nameserver就可以直接指向它spec: dnsPolicy: None dnsConfig: nameservers: - 169.254.20.10 searches: - default.svc.cluster.local - svc.cluster.local - cluster.local options: - name: ndots value: 1最终效果Pod → 本地 node-cacheUDP无需 iptables→ CoreDNS仅在缓存未命中时 TCP 转发。四、ndots 调优的副作用跨命名空间服务发现的风险。将 ndots 设为 1 后同命名空间下的短域名访问如my-service不含点仍然会走 search 域扩展——0 1。但如果集群中某个 Service 的全限定域名包含少于 ndots 个点比如默认的my-service.default.svc——1 个点而 ndots 恰好在两个值之间解析行为会变化。在实践中Kubernetes Service 的 FQDN 格式是svc.ns.svc.cluster.local至少 3 个点所以 ndots1 不会影响集群内服务发现。Go 应用的特例。Go 的net包实现了自己的 DNS 解析器Go 1.8 默认使用纯 Go 解析器而非 cgo 调用 glibc但它的 ndots 行为与 glibc 一致。唯一区别是 Go 解析器不支持single-request和single-request-reopen选项。如果你的应用使用 cgo 构建且依赖 glibc 的特定 DNS 行为需要注意这一差异。ndots:0 的风险。设置 ndots0 意味着所有域名——无论是否含点——都先按 search 域尝试。这对集群内服务发现是友好的但对外部域名查询会引发大量无效的 NXDOMAIN 查询。不推荐使用。五、总结CoreDNS 调优的核心不是换一个更强的 DNS 软件而是减少每一次不必要的解析。ndots 默认值 5 是 Kubernetes 为兼容 Docker 的 DNS 行为遗留的保守设置在大多数生产集群中这是明显偏高的。推荐的 DNS 调优路线优先修改 ndots 为 1这是投入产出比最高的优化一行dnsConfig配置就能消除大量 NXDOMAIN 查询。部署 NodeLocal DNSCache消除 iptables DNAT 的延迟在 100 节点以上集群中收益明显。调整 CoreDNS cache 容量增加 success 和 denial 缓存的条目数减少对上游 DNS 服务器的依赖。监控 DNS 延迟指标CoreDNS 暴露的 Prometheus 指标coredns_dns_request_duration_seconds可以直接反映调优效果。一次 DNS 查询多走一步一千万次查询就是一千万步。ndots 这个参数很小但它的影响乘以集群规模后就是可观测的延迟账单。