Pod 短名称解析和 ndots:5 的查询放大
目录
Pod 里 curl http://nginx-service 能通,curl http://order-service.production 也能通,配置和代码里很少有人写全 nginx-service.default.svc.cluster.local.。把短名称补全成 FQDN 的是容器里 C 库的解析器:kubelet 给 Pod 写好 /etc/resolv.conf,解析器按里面的 search 和 ndots 决定一个名字先拼哪个后缀、什么时候按原样查。同一套规则作用在外部域名上,就会多出一串注定失败的查询。
下面的输出来自 kind 起的 Kubernetes 1.37.0 单节点集群。客户端 Pod 主要用 debian:trixie-slim(glibc 2.41),对照组用 alpine:3.22(musl 1.2.5),都用 getent ahosts 解析,它走 getaddrinfo,每一轮 A 和 AAAA 各发一个查询。CoreDNS 打开了 log 插件并缩到 1 个副本,日志的顺序就是查询的顺序,下面的日志只留了查询类型、名字、返回码、标志位和耗时。集群里建了两个 Service:default 下的 nginx-service 和 production 下的 order-service。
kubelet 写进 Pod 的 resolv.conf#
Pod 不写 dnsPolicy 时用的是 ClusterFirst(DNS for Services and Pods),kubelet 按它生成 /etc/resolv.conf:
kubectl exec client -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
nameserver是 kube-dns Service 的 ClusterIP,这个集群里是10.96.0.10。search的三个后缀依次是本 namespace、svc、集群域。节点自己的/etc/resolv.conf里如果有 search 域,kubelet 会把它们接在这三个后面(generateSearchesForDNSClusterFirst)。kind 节点没有,所以这里只有三个;EKS 节点有,后面单独看。ndots:5是一个阈值。按 resolv.conf(5),名字里的点数达到 n 才会先按原样(绝对域名)查一次;少于 n 就先依次拼 search 域,都查不到再按原样查。
同 namespace 和跨 namespace 的短名称#
nginx-service 没有点,少于 5,解析器从第一个 search 域开始拼,第一轮就查到:
AAAA nginx-service.default.svc.cluster.local. NOERROR qr,aa,rd 0.000491708s
A nginx-service.default.svc.cluster.local. NOERROR qr,aa,rd 0.000593334s
order-service.production 有 1 个点,同样先拼第一个后缀。拼出来的 order-service.production.default.svc.cluster.local. 不存在,CoreDNS 回 NXDOMAIN,解析器换第二个后缀 svc.cluster.local 才查到:
AAAA order-service.production.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000145417s
A order-service.production.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000184625s
AAAA order-service.production.svc.cluster.local. NOERROR qr,aa,rd 0.000046125s
A order-service.production.svc.cluster.local. NOERROR qr,aa,rd 0.000067875s
跨 namespace 的短名称,每次解析都要先多走一轮 NXDOMAIN。
写成完整域名但不带末尾的点,查询反而更多。order-service.production.svc.cluster.local 有 4 个点,仍然少于 5,解析器照样先拼 search 域,三个后缀都失败以后才按原样查到,一共 8 个查询:
AAAA order-service.production.svc.cluster.local.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000104167s
A order-service.production.svc.cluster.local.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000108291s
AAAA order-service.production.svc.cluster.local.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000037583s
A order-service.production.svc.cluster.local.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000035542s
AAAA order-service.production.svc.cluster.local.cluster.local. NXDOMAIN qr,aa,rd 0.000027s
A order-service.production.svc.cluster.local.cluster.local. NXDOMAIN qr,aa,rd 0.000023292s
AAAA order-service.production.svc.cluster.local. NOERROR qr,aa,rd 0.000033667s
A order-service.production.svc.cluster.local. NOERROR qr,aa,rd 0.00002175s
要跳过 search 域,得在末尾再加一个点,后面「名字末尾加点」一节再说。
外部域名先走完所有 search 后缀#
api.github.com 有 2 个点,也少于 5,三个集群后缀都要拼一遍,查完 6 个 NXDOMAIN 才轮到原名:
AAAA api.github.com.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000101042s
A api.github.com.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000118042s
A api.github.com.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000035917s
AAAA api.github.com.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000180334s
A api.github.com.cluster.local. NXDOMAIN qr,aa,rd 0.000098584s
AAAA api.github.com.cluster.local. NXDOMAIN qr,aa,rd 0.000140042s
A api.github.com. NOERROR qr,rd,ra 0.079975958s
AAAA api.github.com. NOERROR qr,rd,ra 0.080159292s
一次外部解析,CoreDNS 收到 8 个查询,6 个没用。这 6 个名字都落在 cluster.local 下,由 CoreDNS 自己回答(标志位里有 aa,每个不到 0.2 ms),只有最后一轮转发到了上游,耗时约 80 ms。所以在这个集群里,多出来的主要是 CoreDNS 的查询量,单次解析的延迟变化不大。
EKS 节点的 search 域更多。EKS 最佳实践指南列出的默认搜索顺序,在三个集群后缀之后还有一个 <region>.compute.internal。在 kind 里给 Pod 的 dnsConfig.searches 加上 ap-southeast-1.compute.internal,模拟出同样的 search 列表:
search default.svc.cluster.local svc.cluster.local cluster.local ap-southeast-1.compute.internal
nameserver 10.96.0.10
options ndots:5
同一次解析变成 10 个查询:
AAAA api.github.com.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000131583s
A api.github.com.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000127625s
AAAA api.github.com.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000025333s
A api.github.com.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000167459s
A api.github.com.cluster.local. NXDOMAIN qr,aa,rd 0.000093541s
AAAA api.github.com.cluster.local. NXDOMAIN qr,aa,rd 0.000147084s
A api.github.com.ap-southeast-1.compute.internal. NXDOMAIN qr,rd,ra 0.024973917s
AAAA api.github.com.ap-southeast-1.compute.internal. NXDOMAIN qr,rd,ra 0.025326375s
A api.github.com. NOERROR qr,rd,ra 0.032535625s
AAAA api.github.com. NOERROR qr,rd,ra 0.032716874s
多出来的一对不在 cluster.local 下,CoreDNS 自己答不了,按 Corefile 里的 forward . /etc/resolv.conf 转给上游,日志里没有 aa,每个约 25 ms。kind 的上游是宿主机的 DNS,换到别的集群,耗时取决于 CoreDNS 转发到的上游。
为什么默认是 5#
ndots:5 是 2015 年定下来的。在 kubelet 往 resolv.conf 里加 ndots 的 PR #10266(2015-06-25 合并)里,Tim Hockin 的说法是,要让 _dns._udp.kube-dns.default.svc 这种 SRV 名字能解析,ndots 就得设成 5(原评论)。这个名字有 4 个点,ndots 要比 4 大,它才会先走 search 域。三个月后他在 #14051 里补了两条下限:mysvc.myns 要求 ndots 至少是 2,mysvc.myns.svc 至少是 3;同时也承认,ndots 一旦到 2,google.com 这类名字就都会走 search 扩展,DNS 查询会多出几倍。
调小 ndots、靠「按原样查不到再回落到 search 域」兜底行不行?2016 年的 #33554 讨论过这一点:glibc 和 Go 的解析器会回落,libbind/netresolv 和 musl 不会(讨论)。Tim Hockin 的结论是「So we can not depend on this behavior.」(原评论)。下面调低 ndots 的实验里,glibc 和 musl 的差别能直接看到。
减少无效查询的四种做法#
四种做法的作用范围不同:名字末尾加点只改一个名字,dnsConfig 改一个 Pod,NodeLocal DNSCache 和 CoreDNS 的 autopath 改整个集群。
名字末尾加点#
末尾带点的名字是绝对域名,解析器跳过 search 域直接查:
A api.github.com. NOERROR qr,aa,rd,ra 0.000048542s
AAAA api.github.com. NOERROR qr,aa,rd,ra 0.000012583s
只剩一轮。EKS 最佳实践指南也把它和调低 ndots 并列为减少查询的两种办法。
这个点会跟着名字进入应用层。在 client Pod 里装上 curl 8.14.1 和 Python 3.13.5 试了一下:
curl -sS -o /dev/null -w "http=%{http_code}\n" https://api.github.com./
curl -sv -o /dev/null https://api.github.com./ 2>&1 | grep -E "^> Host:|subjectAltName|verify ok"
http=400
* subjectAltName: host "api.github.com." matched cert's "*.github.com"
* SSL certificate verify ok.
> Host: api.github.com.
curl 的证书校验能过,但 Host 头带着这个点,GitHub 回了 400。Python 的 urllib 在 TLS 握手时就失败了,报的是 Hostname mismatch, certificate is not valid for 'api.github.com.'。所以 HTTP(S) 地址加点之前,要先确认客户端和服务端都接受;外部 HTTPS 地址用下一节的 dnsConfig 更稳妥。
调低 Pod 的 ndots#
外部调用多的 Pod,可以用 dnsConfig 单独调低阈值:
apiVersion: v1
kind: Pod
metadata:
name: client-ndots2
spec:
dnsConfig:
options:
- name: ndots
value: "2"
containers:
- name: c
image: debian:trixie-slim
command: ["sleep", "3600"]
api.github.com 的点数达到 2,直接按原样查,只有一轮。order-service.production 只有 1 个点,仍然先走 search 域,日志和 ndots:5 时完全一样。
受影响的是点数达到 2 的集群内部名字,比如 order-service.production.svc:
A order-service.production.svc. NXDOMAIN qr,rd,ra 0.032535833s
AAAA order-service.production.svc. NXDOMAIN qr,rd,ra 0.032935166s
A order-service.production.svc.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000105208s
AAAA order-service.production.svc.default.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000159542s
A order-service.production.svc.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000038334s
AAAA order-service.production.svc.svc.cluster.local. NXDOMAIN qr,aa,rd 0.000054541s
AAAA order-service.production.svc.cluster.local. NOERROR qr,aa,rd 0.000046958s
A order-service.production.svc.cluster.local. NOERROR qr,aa,rd 0.000028208s
glibc 先按原样查 order-service.production.svc.。这个名字不在 cluster.local 下,CoreDNS 把它转给了上游(标志位里没有 aa,耗时约 32 ms),拿到 NXDOMAIN 后解析器再从第一个 search 域重新拼,到第三个后缀才查到。一共 8 个查询,还把一个集群内部的名字发到了集群外的 DNS。
musl 不回落。同样是 ndots:2,Alpine 3.22(musl 1.2.5)里的 order-service.production.svc 只按原样查了一轮,拿到 NXDOMAIN 就结束了,getent 的退出码是 2:
AAAA order-service.production.svc. NXDOMAIN qr,rd,ra 0.002182875s
A order-service.production.svc. NXDOMAIN qr,rd,ra 0.002410875s
这和 musl 的说明一致:点数达到 ndots 的名字只按原样查,不会回落到 search 域。Go 程序在 Unix 上优先用纯 Go 解析器(net 包文档),它的 nameList 和 glibc 一样,点数够了先查原名,查不到再拼 search 域。
EKS 最佳实践指南建议对外部调用多的工作负载把 ndots 设成 2,同时提醒 ndots 太低、或者域名写得不够完整时,解析可能失败,要先测试。上面 musl 的结果就是这种失败。调低 ndots 之前,先把代码和配置里点数达到新阈值的内部名字改成 <svc>.<namespace>,或者写成末尾带点的完整 FQDN。
NodeLocal DNSCache#
NodeLocal DNSCache 在每个节点跑一个 DaemonSet 做 DNS 缓存,Pod 的查询先到本节点。它不减少 Pod 发出的查询个数,减少的是到 CoreDNS 的请求和跨节点的 UDP 往返。Pod 里的 nameserver 要不要改,看 kube-proxy 的模式:iptables 模式下 node-local-dns 同时监听 CoreDNS 的 Service IP,nameserver 不用改;IPVS 模式下要把 kubelet 的 --cluster-dns 改成 node-local-dns 监听的本地地址。
CoreDNS 的 autopath#
autopath 把遍历 search 域这件事挪到服务端。CoreDNS 收到拼了第一个后缀的查询,就按发起查询的 Pod 所在 namespace 自己往下查,把第一个不是 NXDOMAIN 的结果连同一条 CNAME 一起返回,客户端第一轮就拿到答案。它要按来源 IP 找到 Pod 和 namespace,所以 kubernetes 插件要从 pods insecure 改成 pods verified。kind 里改过的 Corefile:
.:53 {
log
autopath @kubernetes
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods verified
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}
glibc 的 client 再解析 api.github.com,只剩一轮:
A api.github.com.default.svc.cluster.local. NOERROR qr,rd,ra 0.001901583s
AAAA api.github.com.default.svc.cluster.local. NOERROR qr,rd,ra 0.010351291s
order-service.production 也只剩一轮,musl 的 Pod 解析 api.github.com 同样只有一轮。在 client 里装上 dnsutils,直接查第一个拼出来的名字,能看到 CoreDNS 返回的 CNAME:
kubectl exec client -- dig +noall +answer +comments api.github.com.default.svc.cluster.local A | grep -E 'status|IN'
;; WARNING: .local is reserved for Multicast DNS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59662
api.github.com.default.svc.cluster.local. 30 IN CNAME api.github.com.
api.github.com. 30 IN A 140.82.112.6
代价在文档里写得很清楚。pods verified 要 watch 全部 Pod,kubernetes 插件文档说它比 pods insecure 需要多得多的内存。同一份文档还要求 CoreDNS 收到的 DNS 包,来源 IP 就是发起查询的 Pod 的 IP;NodeLocal DNSCache 的 DaemonSet 跑在宿主机网络上(nodelocaldns.yaml 里是 hostNetwork: true),由它转发给 CoreDNS 的查询带的是节点地址。按这两条,开了 NodeLocal DNSCache 的集群里,autopath 对应不到发起查询的 Pod,两者只能二选一。autopath 文档的 Bugs 一节列了三个已知问题:它靠 Pod 缓存把来源 IP 对应到 namespace,一个 IP 刚被另一个 namespace 的新 Pod 复用时,可能按旧 namespace 拼出错误的 search 列表;不支持 Windows 节点上的 Pod;服务端查到最后仍是 NXDOMAIN 的名字,客户端还会自己把所有后缀再查一遍,这部分省不下来。
小结#
Pod 里的短名称由解析器按 /etc/resolv.conf 补全,点数少于 5 时依次拼 search 域。同 namespace 一轮查到,跨 namespace 多一轮 NXDOMAIN,外部域名要把每个后缀都查一遍:kind 里一次 api.github.com 解析让 CoreDNS 收到 8 个查询,加上 EKS 节点的 <region>.compute.internal 是 10 个,多出来的那一对还要转发到上游。ndots 定成 5,是为了让 4 个点的 SRV 名字也走 search 域,又不能指望所有解析器都会回落,所以默认值一直没改。
减少查询的四种做法各有代价。末尾加点最直接,但点会进 Host 头和证书校验;调低 ndots 之前要先处理点数达到新阈值的内部名字,musl 下它们会直接解析失败;NodeLocal DNSCache 减少的是到 CoreDNS 的往返;autopath 把遍历挪到服务端,代价是 pods verified 的内存和文档里列的几个已知问题,而且和 NodeLocal DNSCache 只能二选一。
参考资料#
- Kubernetes: DNS for Services and Pods:
dnsPolicy、dnsConfig和 Pod 的 resolv.conf - resolv.conf(5):
search和ndots的定义 - kubelet
dns.go(v1.37.0):ClusterFirst下的 search 列表怎么拼 - kubernetes/kubernetes#10266、#14051、#33554:ndots:5 的由来和各解析器回落行为的讨论
- EKS Best Practices: Reduce external queries by lowering ndots:EKS 上的默认 search 列表和 ndots 建议
- musl: Functional differences from glibc:musl 对
ndots和 search 域的处理 - Go
net包:Name Resolution、dnsclient_unix.go(go1.25.0):纯 Go 解析器的选择条件和 search 顺序 - Kubernetes: Using NodeLocal DNSCache、
nodelocaldns.yaml(v1.37.0):iptables 和 IPVS 模式下的部署差异,DaemonSet 的hostNetwork - CoreDNS
autopath插件、kubernetes插件:服务端 search 和pods verified的内存代价 - CoreDNS
log插件