CoreDNS 调优:NDots 设不对,每次 DNS 查询都多走一步

📅 2026/7/9 2:36:47 👁️ 阅读次数 📝 编程学习
CoreDNS 调优:NDots 设不对,每次 DNS 查询都多走一步

CoreDNS 调优:NDots 设不对,每次 DNS 查询都多走一步

一、从 50ms 到 5ms:一次 DNS 解析的代价到底有多高

Kubernetes 集群中一个 Pod 访问同命名空间下的另一个 Service,DNS 解析应该几乎是瞬时的。但实际监控显示,很多集群的 DNS P99 延迟高达 50-100ms。排查到最后,元凶往往是ndots参数的默认值。

在标准的 Kubernetes Pod 配置中,/etc/resolv.confoptions ndots:5是默认行为。这个值的意思很明确:域名中包含的点的数量少于 5 个时,DNS 解析器先把域名当作"相对域名"处理,依次追加resolv.confsearch域名的后缀进行查询。只有当所有后缀都查不到时,才把它当作绝对域名(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 解析器执行以下查询序列:

  1. my-service.default.svc.cluster.local.→ NXDOMAIN(这个 Service 不存在于 default 命名空间,假设 Pod 在其他命名空间)
  2. my-service.svc.cluster.local.→ 成功

虽然第 2 次查询就命中了,但第 1 次查询已经发出了。更糟糕的场景:访问redis-cache.production这个域名——包含 1 个点,1 < 5。解析器先查redis-cache.production.default.svc.cluster.local(NXDOMAIN),再查redis-cache.production.svc.cluster.local(NXDOMAIN),最后才查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[视为 FQDN<br/>直接查询] 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.localndots=5

  • 查询vector-store.ai-training(1 个点,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.com(1 个点,1 < 5):

    • 同样触发 3 次查询,前 2 次全部 NXDOMAIN。

这就是问题的核心:对于外部域名,ndots 默认值 5 导致几乎所有查询都需要先"浪费" 2 次集群内部的 NXDOMAIN 查询,然后才真正查外部 DNS。

三、CoreDNS 调优的完整实践

3.1 修改 Pod 的 ndots 配置

最直接的手段是通过 Pod 的dnsConfig覆盖默认 ndots:

apiVersion: 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 Hub),ndots: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:缓存成功响应的条目数上限为 9984,TTL 取 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),绕过 iptables:

apiVersion: 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-cache(UDP,无需 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 个点),所以 ndots=1 不会影响集群内服务发现。

Go 应用的特例。Go 的net包实现了自己的 DNS 解析器(Go 1.8+ 默认使用纯 Go 解析器而非 cgo 调用 glibc),但它的 ndots 行为与 glibc 一致。唯一区别是 Go 解析器不支持single-requestsingle-request-reopen选项。如果你的应用使用 cgo 构建且依赖 glibc 的特定 DNS 行为,需要注意这一差异。

ndots:0 的风险。设置 ndots=0 意味着所有域名——无论是否含点——都先按 search 域尝试。这对集群内服务发现是友好的,但对外部域名查询会引发大量无效的 NXDOMAIN 查询。不推荐使用。

五、总结

CoreDNS 调优的核心不是换一个更强的 DNS 软件,而是减少每一次不必要的解析。ndots 默认值 5 是 Kubernetes 为兼容 Docker 的 DNS 行为遗留的保守设置,在大多数生产集群中这是明显偏高的。

推荐的 DNS 调优路线:

  1. 优先修改 ndots 为 1:这是投入产出比最高的优化,一行dnsConfig配置就能消除大量 NXDOMAIN 查询。

  2. 部署 NodeLocal DNSCache:消除 iptables DNAT 的延迟,在 100 节点以上集群中收益明显。

  3. 调整 CoreDNS cache 容量:增加 success 和 denial 缓存的条目数,减少对上游 DNS 服务器的依赖。

  4. 监控 DNS 延迟指标:CoreDNS 暴露的 Prometheus 指标(coredns_dns_request_duration_seconds)可以直接反映调优效果。

一次 DNS 查询多走一步,一千万次查询就是一千万步。ndots 这个参数很小,但它的影响乘以集群规模后,就是可观测的延迟账单。